diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js new file mode 100644 index 0000000000..0a10176b1c --- /dev/null +++ b/integration-tests/api/__tests__/admin/discount.js @@ -0,0 +1,83 @@ +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/: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__/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-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/migrations/1617002207608-discount_usage.ts b/packages/medusa/src/migrations/1617002207608-discount_usage.ts new file mode 100644 index 0000000000..4b2176e48d --- /dev/null +++ b/packages/medusa/src/migrations/1617002207608-discount_usage.ts @@ -0,0 +1,29 @@ +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'` + ) + } + + 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` + ) + } +} diff --git a/packages/medusa/src/models/discount-rule.ts b/packages/medusa/src/models/discount-rule.ts index 76ba081ecf..397678dd0d 100644 --- a/packages/medusa/src/models/discount-rule.ts +++ b/packages/medusa/src/models/discount-rule.ts @@ -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..9e8405d9af 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: data.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, }) }) )