diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js new file mode 100644 index 0000000000..575b1e0a0d --- /dev/null +++ b/integration-tests/api/__tests__/admin/discount.js @@ -0,0 +1,161 @@ +const { dropDatabase } = require("pg-god"); +const path = require("path"); +const { Region, DiscountRule, Discount } = require("@medusajs/medusa"); + +const setupServer = require("../../../helpers/setup-server"); +const { useApi } = require("../../../helpers/use-api"); +const { initDb } = require("../../../helpers/use-db"); +const adminSeeder = require("../../helpers/admin-seeder"); + +jest.setTimeout(30000); + +describe("/admin/discounts", () => { + let medusaProcess; + let dbConnection; + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")); + dbConnection = await initDb({ cwd }); + medusaProcess = await setupServer({ cwd }); + }); + + afterAll(async () => { + await dbConnection.close(); + await dropDatabase({ databaseName: "medusa-integration" }); + + medusaProcess.kill(); + }); + + describe("POST /admin/discounts", () => { + beforeEach(async () => { + const manager = dbConnection.manager; + try { + await adminSeeder(dbConnection); + } catch (err) { + console.log(err); + throw err; + } + }); + + afterEach(async () => { + const manager = dbConnection.manager; + await manager.query(`DELETE FROM "discount"`); + await manager.query(`DELETE FROM "discount_rule"`); + await manager.query(`DELETE FROM "user"`); + }); + + it("creates a discount and updates it", async () => { + const api = useApi(); + + const response = await api + .post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", + }, + usage_limit: 10, + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err); + }); + + expect(response.status).toEqual(200); + expect(response.data.discount).toEqual( + expect.objectContaining({ + code: "HELLOWORLD", + usage_limit: 10, + }) + ); + + const updated = await api + .post( + `/admin/discounts/${response.data.discount.id}`, + { + usage_limit: 20, + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err); + }); + + expect(updated.status).toEqual(200); + expect(updated.data.discount).toEqual( + expect.objectContaining({ + code: "HELLOWORLD", + usage_limit: 20, + }) + ); + }); + }); + + describe("POST /admin/discounts/:discount_id/dynamic-codes", () => { + beforeEach(async () => { + const manager = dbConnection.manager; + try { + await adminSeeder(dbConnection); + await manager.insert(DiscountRule, { + id: "test-discount-rule", + description: "Dynamic rule", + type: "percentage", + value: 10, + allocation: "total", + }); + await manager.insert(Discount, { + id: "test-discount", + code: "DYNAMIC", + is_dynamic: true, + is_disabled: false, + rule_id: "test-discount-rule", + }); + } catch (err) { + console.log(err); + throw err; + } + }); + + afterEach(async () => { + const manager = dbConnection.manager; + await manager.query(`DELETE FROM "discount"`); + await manager.query(`DELETE FROM "discount_rule"`); + await manager.query(`DELETE FROM "user"`); + }); + + it("creates a dynamic discount", async () => { + const api = useApi(); + + const response = await api + .post( + "/admin/discounts/test-discount/dynamic-codes", + { + code: "HELLOWORLD", + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch((err) => { + console.log(err); + }); + + expect(response.status).toEqual(200); + }); + }); +}); diff --git a/integration-tests/api/helpers/cart-seeder.js b/integration-tests/api/helpers/cart-seeder.js index a29268879e..73f512dbf1 100644 --- a/integration-tests/api/helpers/cart-seeder.js +++ b/integration-tests/api/helpers/cart-seeder.js @@ -16,6 +16,24 @@ module.exports = async (connection, data = {}) => { tax_rate: 0, }); + await manager.insert(DiscountRule, { + id: "test-discount-rule", + description: "Dynamic rule", + type: "percentage", + value: 10, + allocation: "total", + }); + + await manager.insert(Discount, { + id: "test-discount", + code: "DYNAMIC", + rule_id: "test-discount-rule", + is_dynamic: true, + usage_count: 0, + usage_limit: 1, + is_disabled: false, + }); + const d = await manager.create(Discount, { id: "test-discount", code: "CREATED", @@ -29,8 +47,6 @@ module.exports = async (connection, data = {}) => { type: "fixed", value: 10000, allocation: "total", - usage_limit: 2, - usage_count: 2, }); d.rule = dr; diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index cc3dcb2a7b..c8d8bd4e36 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1174,9 +1174,9 @@ chalk "^4.0.0" "@medusajs/medusa@1.1.13-dev-1615987548667": - version "1.1.13" - resolved "https://registry.yarnpkg.com/@medusajs/medusa/-/medusa-1.1.13.tgz#1aad18445c062e298bfea2ac362ad2740a53fde6" - integrity sha512-yPQ+uA9qQsJiqME7nR8ggeg/qi2Kypqi6iNsFrdXbA99djjm3Cpkl3kmkTorRvzQ6jbKZ1QMLS+Dx5FwbKuiTw== + version "1.1.16" + resolved "https://registry.yarnpkg.com/@medusajs/medusa/-/medusa-1.1.16.tgz#692578b1eeced9d3603326fda172d43abe5cce95" + integrity sha512-SXK9YEBMxlfP7KgnIYfGTflFhUB2vx6aKI2MiVesCzB5wR2OKkOn6W+oIWeF0/pBttdSitCP4G2iJx2y8dYL5g== dependencies: "@babel/plugin-transform-classes" "^7.9.5" "@hapi/joi" "^16.1.8" @@ -1198,8 +1198,8 @@ joi "^17.3.0" joi-objectid "^3.0.1" jsonwebtoken "^8.5.1" - medusa-core-utils "^1.1.2" - medusa-test-utils "^1.1.5" + medusa-core-utils "^1.1.3" + medusa-test-utils "^1.1.6" morgan "^1.9.1" multer "^1.4.2" passport "^0.4.0" @@ -4314,28 +4314,28 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.2.tgz#3d9ccd37b052bc4701040fbf0618210f075f459a" - integrity sha512-YAGkLkS5DqCSHWlMz2Bfh0nKJQ8n35IfH/39q6J9DXfFUPYU+d5i8lSvIS8PNsXuSs9Es0dzY2ZS/sOIFKWzxw== +medusa-core-utils@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.1.3.tgz#e740de04a08655b9b037ef135afcc99914498f24" + integrity sha512-Xk7SuHEo4kBgJFHIyd6OkBvK0KO23hF5pPHY2R9Luf26vRvyD3mUZUBIHPnxuT2f576vgJJ7verMN7peizXXkg== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" medusa-interfaces@1.1.3-dev-1615987548667: - version "1.1.3" - resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.3.tgz#b1fb889c321433d31e6d998dbd7d47eb71a27589" - integrity sha512-6WNzOfcHDM7CaRFOerZBt5eRqje97gMbTcoEiFOxJy8a5B7OIq/wt7UBI1Et3C9lYFKpb5fSUwfDwRWY/yO3UA== + version "1.1.4" + resolved "https://registry.yarnpkg.com/medusa-interfaces/-/medusa-interfaces-1.1.4.tgz#f71e9eb885cd6f51105986d861f83a2b497da240" + integrity sha512-uMjfXbIkJSqkd87/wPGFFQ47ZmNdImW2sGWgrEa8pMGYfE8rGQxgLvGEEnn34ejJHWYGPQRH5Az/J76tY/Zs2g== dependencies: - medusa-core-utils "^1.1.2" + medusa-core-utils "^1.1.3" -medusa-test-utils@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.5.tgz#5a93c52117c7a8659058512abf939e2aaed619b7" - integrity sha512-oM6lIRdnq6T3VRi702hYOQ9m3T9Zy2IwZAp2nBnJRlXpXbWlNnIS3y0E638m5wsGbFzSKKxrDNdGtEpujxwzyQ== +medusa-test-utils@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.1.6.tgz#c9b2675532338be760a47bd8df7566b162c171d4" + integrity sha512-72k2DMKrxPDgm1JeCCjtNVTl6nFlbdp5HHtE3mWEpl0+88pZvNSp/HL41nENHHZ0YsK2w34FTbm0YMqU4EHg+Q== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "^1.1.2" + medusa-core-utils "^1.1.3" randomatic "^3.1.1" merge-descriptors@1.0.1: diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js index d4120fdf79..654c46ff99 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-region.js @@ -38,6 +38,8 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => { "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js index 02243c1928..05e521083d 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/add-valid-product.js @@ -38,6 +38,8 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => { "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js index cde96a2332..18536d7533 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/create-discount.js @@ -11,6 +11,7 @@ describe("POST /admin/discounts", () => { payload: { code: "TEST", rule: { + description: "Test", type: "fixed", value: 10, allocation: "total", @@ -33,6 +34,7 @@ describe("POST /admin/discounts", () => { expect(DiscountServiceMock.create).toHaveBeenCalledWith({ code: "TEST", rule: { + description: "Test", type: "fixed", value: 10, allocation: "total", @@ -51,6 +53,7 @@ describe("POST /admin/discounts", () => { payload: { code: "10%OFF", rule: { + description: "Test", value: 10, allocation: "total", }, diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js index 9f7c71e69a..9260e22679 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/get-discount.js @@ -9,6 +9,8 @@ const defaultFields = [ "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js index 438c5b1125..ec33ba1d17 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-region.js @@ -9,6 +9,8 @@ const defaultFields = [ "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js index e5fd724a61..7dbc6d2912 100644 --- a/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js +++ b/packages/medusa/src/api/routes/admin/discounts/__tests__/remove-valid-product.js @@ -9,6 +9,8 @@ const defaultFields = [ "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/create-discount.js b/packages/medusa/src/api/routes/admin/discounts/create-discount.js index b053b16728..4e42bdc882 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/create-discount.js @@ -36,6 +36,9 @@ import { MedusaError, Validator } from "medusa-core-utils" * type: array * items: * type: string + * usage_limit: + * type: number + * description: Maximum times the discount can be used * metadata: * description: An optional set of key-value pairs to hold additional information. * type: object @@ -64,14 +67,14 @@ export default async (req, res) => { .required(), allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), - usage_limit: Validator.number() - .positive() - .optional(), }) .required(), is_disabled: Validator.boolean().default(false), starts_at: Validator.date().optional(), ends_at: Validator.date().optional(), + usage_limit: Validator.number() + .positive() + .optional(), regions: Validator.array() .items(Validator.string()) .optional(), diff --git a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js index b393c19d8a..30e7288f62 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js +++ b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.js @@ -26,6 +26,7 @@ export default async (req, res) => { const schema = Validator.object().keys({ code: Validator.string().required(), + usage_limit: Validator.number().default(1), metadata: Validator.object().optional(), }) diff --git a/packages/medusa/src/api/routes/admin/discounts/index.js b/packages/medusa/src/api/routes/admin/discounts/index.js index dbfc487424..6a240a78ed 100644 --- a/packages/medusa/src/api/routes/admin/discounts/index.js +++ b/packages/medusa/src/api/routes/admin/discounts/index.js @@ -62,6 +62,8 @@ export const defaultFields = [ "is_disabled", "rule_id", "parent_discount_id", + "usage_limit", + "usage_count", "starts_at", "ends_at", "created_at", diff --git a/packages/medusa/src/api/routes/admin/discounts/update-discount.js b/packages/medusa/src/api/routes/admin/discounts/update-discount.js index 0679dc7e1b..f3cfecc1d3 100644 --- a/packages/medusa/src/api/routes/admin/discounts/update-discount.js +++ b/packages/medusa/src/api/routes/admin/discounts/update-discount.js @@ -64,14 +64,14 @@ export default async (req, res) => { value: Validator.number().required(), allocation: Validator.string().required(), valid_for: Validator.array().items(Validator.string()), - usage_limit: Validator.number() - .positive() - .optional(), }) .optional(), is_disabled: Validator.boolean().optional(), starts_at: Validator.date().optional(), ends_at: Validator.date().optional(), + usage_limit: Validator.number() + .positive() + .optional(), regions: Validator.array() .items(Validator.string()) .optional(), diff --git a/packages/medusa/src/migrations/1617002207608-discount_usage.ts b/packages/medusa/src/migrations/1617002207608-discount_usage.ts new file mode 100644 index 0000000000..99d3697963 --- /dev/null +++ b/packages/medusa/src/migrations/1617002207608-discount_usage.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class discountUsage1617002207608 implements MigrationInterface { + name = "discountUsage1617002207608" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "discount_rule" DROP COLUMN "usage_limit"` + ) + await queryRunner.query( + `ALTER TABLE "discount_rule" DROP COLUMN "usage_count"` + ) + await queryRunner.query(`ALTER TABLE "discount" ADD "usage_limit" integer`) + await queryRunner.query( + `ALTER TABLE "discount" ADD "usage_count" integer NOT NULL DEFAULT '0'` + ) + await queryRunner.query( + `ALTER TABLE "discount_rule" ALTER COLUMN "description" DROP NOT NULL` + ) + await queryRunner.query( + `COMMENT ON COLUMN "discount_rule"."description" IS NULL` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "discount" DROP COLUMN "usage_count"`) + await queryRunner.query(`ALTER TABLE "discount" DROP COLUMN "usage_limit"`) + await queryRunner.query( + `ALTER TABLE "discount_rule" ADD "usage_count" integer NOT NULL DEFAULT '0'` + ) + await queryRunner.query( + `ALTER TABLE "discount_rule" ADD "usage_limit" integer` + ) + await queryRunner.query( + `COMMENT ON COLUMN "discount_rule"."description" IS NULL` + ) + await queryRunner.query( + `ALTER TABLE "discount_rule" ALTER COLUMN "description" SET NOT NULL` + ) + } +} diff --git a/packages/medusa/src/models/discount-rule.ts b/packages/medusa/src/models/discount-rule.ts index 76ba081ecf..1e86542892 100644 --- a/packages/medusa/src/models/discount-rule.ts +++ b/packages/medusa/src/models/discount-rule.ts @@ -30,7 +30,7 @@ export class DiscountRule { @PrimaryColumn() id: string - @Column() + @Column({ nullable: true }) description: string @Column({ @@ -63,12 +63,6 @@ export class DiscountRule { }) valid_for: Product[] - @Column({ nullable: true }) - usage_limit: number - - @Column({ default: 0 }) - usage_count: number - @CreateDateColumn({ type: "timestamptz" }) created_at: Date @@ -121,12 +115,6 @@ export class DiscountRule { * type: array * items: * $ref: "#/components/schemas/product" - * usage_limit: - * description: "The maximum number of times that a discount can be used." - * type: integer - * usage_count: - * description: "The number of times a discount has been used." - * type: integer * created_at: * description: "The date with timezone at which the resource was created." * type: string diff --git a/packages/medusa/src/models/discount.ts b/packages/medusa/src/models/discount.ts index 2095808bcd..b76294bece 100644 --- a/packages/medusa/src/models/discount.ts +++ b/packages/medusa/src/models/discount.ts @@ -68,6 +68,12 @@ export class Discount { }) regions: Region[] + @Column({ nullable: true }) + usage_limit: number + + @Column({ default: 0 }) + usage_count: number + @CreateDateColumn({ type: "timestamptz" }) created_at: Date @@ -127,6 +133,12 @@ export class Discount { * type: array * items: * $ref: "#/components/schemas/region" + * usage_limit: + * description: "The maximum number of times that a discount can be used." + * type: integer + * usage_count: + * description: "The number of times a discount has been used." + * type: integer * created_at: * description: "The date with timezone at which the resource was created." * type: string diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 6f3a9d8fec..11a9c72a69 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1467,10 +1467,9 @@ describe("CartService", () => { id: IdMap.getId("limit-reached"), code: "limit-reached", regions: [{ id: IdMap.getId("good") }], - rule: { - usage_count: 2, - usage_limit: 2, - }, + rule: {}, + usage_count: 2, + usage_limit: 2, }) } if (code === "null-count") { @@ -1478,10 +1477,9 @@ describe("CartService", () => { id: IdMap.getId("null-count"), code: "null-count", regions: [{ id: IdMap.getId("good") }], - rule: { - usage_count: null, - usage_limit: 2, - }, + rule: {}, + usage_count: null, + usage_limit: 2, }) } if (code === "FREESHIPPING") { @@ -1630,10 +1628,9 @@ describe("CartService", () => { id: IdMap.getId("null-count"), code: "null-count", regions: [{ id: IdMap.getId("good") }], - rule: { - usage_count: 0, - usage_limit: 2, - }, + usage_count: 0, + usage_limit: 2, + rule: {}, }, ], discount_total: 0, diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index 48a5831180..72995a5931 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -814,10 +814,10 @@ class CartService extends BaseService { const rule = discount.rule // if limit is set and reached, we make an early exit - if (rule?.usage_limit) { - rule.usage_count = rule.usage_count || 0 + if (discount.usage_limit) { + discount.usage_count = discount.usage_count || 0 - if (rule.usage_limit === rule.usage_count) + if (discount.usage_limit === discount.usage_count) throw new MedusaError( MedusaError.Types.NOT_ALLOWED, "Discount has been used maximum allowed times" diff --git a/packages/medusa/src/services/discount.js b/packages/medusa/src/services/discount.js index 760d2ca0dd..6451eb9c72 100644 --- a/packages/medusa/src/services/discount.js +++ b/packages/medusa/src/services/discount.js @@ -83,13 +83,6 @@ class DiscountService extends BaseService { .required(), allocation: Validator.string().required(), valid_for: Validator.array().optional(), - usage_limit: Validator.number() - .positive() - .allow(null) - .optional(), - usage_count: Validator.number() - .positive() - .optional(), created_at: Validator.date().optional(), updated_at: Validator.date() .allow(null) @@ -337,6 +330,7 @@ class DiscountService extends BaseService { is_disabled: false, code: data.code.toUpperCase(), parent_discount_id: discount.id, + usage_limit: discount.usage_limit, } const created = await discountRepo.create(toCreate) diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index c889cd99a9..0a4cea6a46 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -48,12 +48,9 @@ class OrderSubscriber { await Promise.all( order.discounts.map(async d => { - const usageCount = d.rule?.usage_count || 0 + const usageCount = d?.usage_count || 0 return this.discountService_.update(d.id, { - rule: { - ...d.rule, - usage_count: usageCount + 1, - }, + usage_count: usageCount + 1, }) }) )