Skip to content

Commit

Permalink
feat: ✨ Added coupons
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulRill00 committed Jul 25, 2024
1 parent bd1adef commit 604c374
Show file tree
Hide file tree
Showing 11 changed files with 634 additions and 1 deletion.
57 changes: 57 additions & 0 deletions src/PrintOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { Batch, CreateBatch } from "~/models/Batch";
import { IBatch } from "~/models/_interfaces/IBatch";
import { BatchStatus } from "~/enums/BatchStatus";
import * as crypto from "crypto";
import { Coupon, CreateCoupon } from "~/models/Coupon";
import { ICoupon } from "~/models/_interfaces/ICoupon";

export type RequestHandler = new (
token: string,
Expand Down Expand Up @@ -523,6 +525,61 @@ export class PrintOne {
);
}

/**
* Create a coupon
* @param data The coupon data
*/
public async createCoupon(data: CreateCoupon): Promise<Coupon> {
const response = await this.client.POST<ICoupon>("coupons", {
name: data.name,
});

return new Coupon(this.protected, response);
}

/**
* Get all coupons.
* @param { PaginationOptions } options The options to use for pagination
* @param options.limit The maximum amount of coupons to return.
* @param options.page The page to return.
* @param options.sortBy The fields to sort by, can be "createdAt", "updatedAt".
* @param options.filter The filters to apply.
*/
public async getCoupons(
options: PaginationOptions<"name"> & {
filter?: {
name?: InFilter;
createdAt?: DateFilter;
};
} = {},
): Promise<PaginatedResponse<Coupon>> {
const params = {
...sortToQuery(options),
...inFilterToQuery("name", options.filter?.name),
};

const data = await this.client.GET<IPaginatedResponse<ICoupon>>("coupons", {
params: params,
});

return PaginatedResponse.safe(
this.protected,
data,
(data) => new Coupon(this.protected, data),
);
}

/**
* Get a coupon by its id.
* @param { string } id The id of the coupon.
* @throws { PrintOneError } If the coupon could not be found.
*/
public async getCoupon(id: string): Promise<Coupon> {
const data = await this.client.GET<ICoupon>(`coupons/${id}`);

return new Coupon(this.protected, data);
}

public validatedWebhook(
body: string,
headers: Record<string, string>,
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ export * from "./PrintOne";

// Models
export * from "./models/Address";
export * from "./models/Batch";
export * from "./models/Company";
export * from "./models/Coupon";
export * from "./models/CouponCode";
export * from "./models/CsvOrder";
export * from "./models/CustomFile";
export * from "./models/Order";
export * from "./models/PaginatedResponse";
export * from "./models/Preview";
export * from "./models/PreviewDetails";
export * from "./models/Template";
export * from "./models/Batch";

// Enums
export * from "./enums/CsvStatus";
Expand Down
97 changes: 97 additions & 0 deletions src/models/Coupon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ICoupon } from "~/models/_interfaces/ICoupon";
import { Protected } from "~/PrintOne";
import { CouponCode } from "~/models/CouponCode";
import { PaginatedResponse } from "~/models/PaginatedResponse";
import { IPaginatedResponse } from "~/models/_interfaces/IPaginatedResponse";
import { ICouponCode } from "~/models/_interfaces/ICouponCode";

export type CreateCoupon = {
name: string;
};

export class Coupon {
private _data: ICoupon;

constructor(
private readonly _protected: Protected,
_data: ICoupon,
) {
this._data = _data;
}

public get id(): string {
return this._data.id;
}

public get name(): string {
return this._data.name;
}

public get companyId(): string {
return this._data.companyId;
}

public get stats(): ICoupon["stats"] {
return this._data.stats;
}

/**
* Refreshes the coupon data
* @throws { PrintOneError } If the coupon could not be refreshed.
*/
public async refresh(): Promise<void> {
this._data = await this._protected.client.GET<ICoupon>(
`/coupons/${this.id}`,
);
}

/**
* Get all coupon codes for the coupon
*/
public async getCodes(): Promise<PaginatedResponse<CouponCode>> {
const data = await this._protected.client.GET<
IPaginatedResponse<ICouponCode>
>(`/coupons/${this.id}/codes`);

return PaginatedResponse.safe(
this._protected,
data,
(data) => new CouponCode(this._protected, data),
);
}

/**
* Get all coupon codes for the coupon
* @throws { PrintOneError } If the coupon code could not be found.
*/
public async getCode(codeId: string): Promise<CouponCode> {
const data = await this._protected.client.GET<ICouponCode>(
`/coupons/${this.id}/codes/${codeId}`,
);
return new CouponCode(this._protected, data);
}

/**
* Add coupon codes to coupon with a CSV file
* The CSV file should have a header column and the first column should be the coupon codes
* @throws { PrintOneError } If the coupon codes could not be added.
*/
public async addCodes(csv: ArrayBuffer): Promise<void> {
const formData = new FormData();
formData.append("file", new Blob([csv], { type: "text/csv" }), "codes.csv");

await this._protected.client.POST<void>(`/coupons/${this.id}`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
}

/**
* Delete the coupon
* @throws { PrintOneError } If the coupon could not be deleted.
*/
public async delete(): Promise<void> {
await this._protected.client.DELETE(`/coupons/${this.id}`);
}
}
62 changes: 62 additions & 0 deletions src/models/CouponCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Protected } from "~/PrintOne";
import { ICouponCode } from "~/models/_interfaces/ICouponCode";
import { Order } from "~/models/Order";
import { IOrder } from "~/models/_interfaces/IOrder";

export class CouponCode {
private _data: ICouponCode;

constructor(
private readonly _protected: Protected,
_data: ICouponCode,
) {
this._data = _data;
}

public get id(): string {
return this._data.id;
}

public get couponId(): string {
return this._data.couponId;
}

public get code(): string {
return this._data.code;
}

public get used(): boolean {
return this._data.used;
}

public get usedAt(): Date | null {
return this._data.usedAt ? new Date(this._data.usedAt) : null;
}

public get orderId(): string | null {
return this._data.orderId;
}

/**
* Refrsh the coupon code
* @throws { PrintOneError } If the coupon code could not be refreshed.
*/
public async refresh(): Promise<void> {
this._data = await this._protected.client.GET<ICouponCode>(
`/coupons/${this.couponId}/codes/${this.id}`,
);
}

/**
* Get the order the coupon was used on
* @throws { PrintOneError } If the order could not be fetched.
*/
public async getOrder(): Promise<Order | null> {
if (!this.orderId) return null;

const data = await this._protected.client.GET<IOrder>(
`/orders/${this.orderId}`,
);
return new Order(this._protected, data);
}
}
10 changes: 10 additions & 0 deletions src/models/_interfaces/ICoupon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type ICoupon = {
id: string;
name: string;
companyId: string;
stats: {
total: number;
used: number;
remaining: number;
};
};
8 changes: 8 additions & 0 deletions src/models/_interfaces/ICouponCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ICouponCode = {
id: string;
couponId: string;
code: string;
used: boolean;
usedAt: null | Date;
orderId: null | string;
};
121 changes: 121 additions & 0 deletions test/Coupon.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Coupon, PaginatedResponse, PrintOneError } from "../src";
import { client } from "./client";
import * as fs from "fs";
import * as path from "path";

let coupon: Coupon = null as unknown as Coupon;
let file: ArrayBuffer;

beforeAll(function () {
file = fs.readFileSync(path.join(__dirname, "assets/codes.csv"));
});

beforeEach(async function () {
coupon = await client.createCoupon({
name: `Test Coupon`,
});
});

describe("refresh", function () {
it("should refresh the coupon", async function () {
// precondition
expect(coupon.stats.total).toEqual(0);

// arrange
await coupon.addCodes(file);

// act
await coupon.refresh();

// assert
expect(coupon.stats.total).toBe(25);
}, 30000);
});

describe("delete", function () {
it("should delete the order", async function () {
// arrange

// act
await coupon.delete();

// assert
await expect(client.getCoupon(coupon.id)).rejects.toThrow(PrintOneError);
}, 30000);
});

describe("addCodes", function () {
it("should increase coupons total", async function () {
// arrange
await coupon.addCodes(file);

// act
await coupon.refresh();

// assert
expect(coupon.stats.total).toBe(25);
});
});

describe("getCodes", function () {
beforeEach(async function () {
await coupon.addCodes(file);
});

it("should return a paginated response", async function () {
// arrange

// act
const codes = await coupon.getCodes();

// assert
expect(codes).toBeDefined();
expect(codes).toEqual(expect.any(PaginatedResponse));

expect(codes.data).toBeDefined();
expect(codes.data.length).toBeGreaterThanOrEqual(1);

expect(codes.meta.total).toBeGreaterThanOrEqual(1);
expect(codes.meta.page).toEqual(1);
// Default page size is 10
expect(codes.meta.pageSize).toBeGreaterThanOrEqual(10);
expect(codes.meta.pages).toBeGreaterThanOrEqual(1);
});

it("should return a coupon code with all fields", async function () {
// act
const coupons = await coupon.getCodes();
const couponCode = coupons.data[0];

// assert
expect(couponCode.id).toEqual(expect.any(String));
expect(couponCode.couponId).toEqual(expect.any(String));
expect(couponCode.code).toEqual(expect.any(String));
expect(couponCode.used).toEqual(expect.any(Boolean));
expect(couponCode.usedAt).toEqual(null);
expect(couponCode.orderId).toEqual(null);
});
});

describe("getCode", function () {
it("should get code by id", async function () {
//arrange
await coupon.addCodes(file);
const codes = await coupon.getCodes();
const codeId = codes.data[0].id;

//act
const code = await coupon.getCode(codeId);

//assert
expect(code.couponId).toBe(coupon.id);
});

it("should throw error if code does not exist", async function () {
//act
const response = coupon.getCode("test");

//assert
await expect(response).rejects.toThrow(PrintOneError);
});
});
Loading

0 comments on commit 604c374

Please sign in to comment.