From 765a2cccda2c4c552ede9ec23e0c1e3dd4ea44fc Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 13 Oct 2022 16:34:06 +0200 Subject: [PATCH] Feat/add discount condition batch (#2430) * feat(medusa): Allow to add items to a discount condition by batch + cleanup of discounts and discount conditions end points * style(medusa): cleanup catch and log * feat(medusa-react, medusa-js): Add support to add item batch to discount condition * cleanup * cleanup * rename items to resources * fix(medusa-js): url * Create fast-suns-repair.md * update naming * tests(integration): Update tests to reflect API changes * feat(medusa): Delete a condition should be idempotent on discount and condition * revert --- .changeset/fast-suns-repair.md | 7 + .../admin/__snapshots__/discount.js.snap | 12 - .../api/__tests__/admin/discount.js | 1538 +++++++---------- .../src/resources/admin/discounts.ts | 30 +- packages/medusa-react/mocks/handlers/admin.ts | 27 + .../src/hooks/admin/discounts/mutations.ts | 30 +- .../src/hooks/admin/discounts/queries.ts | 2 +- .../hooks/admin/discounts/mutations.test.ts | 44 + .../does-condition-belong-to-discount.ts | 33 + packages/medusa/src/api/middlewares/index.ts | 1 + .../add-resources-to-condition-batch.ts | 131 ++ .../admin/discounts/create-condition.ts | 43 +- .../routes/admin/discounts/create-discount.ts | 39 +- .../admin/discounts/create-dynamic-code.ts | 14 +- .../admin/discounts/delete-condition.ts | 47 +- .../routes/admin/discounts/get-condition.ts | 58 +- .../admin/discounts/get-discount-by-code.ts | 38 +- .../routes/admin/discounts/get-discount.ts | 32 +- .../src/api/routes/admin/discounts/index.ts | 128 +- .../routes/admin/discounts/list-discounts.ts | 54 +- .../admin/discounts/update-condition.ts | 41 +- .../routes/admin/discounts/update-discount.ts | 39 +- .../medusa/src/repositories/customer-group.ts | 4 +- .../src/repositories/discount-condition.ts | 10 +- .../src/repositories/product-collection.ts | 4 +- .../medusa/src/repositories/product-tag.ts | 4 +- .../medusa/src/repositories/product-type.ts | 4 +- .../medusa/src/services/__mocks__/discount.js | 13 +- .../medusa/src/services/customer-group.ts | 6 +- .../medusa/src/services/discount-condition.ts | 7 +- packages/medusa/src/services/discount.ts | 2 +- packages/medusa/src/services/index.ts | 1 + .../medusa/src/services/product-collection.ts | 6 +- packages/medusa/src/services/product-tag.ts | 6 +- packages/medusa/src/services/product-type.ts | 6 +- packages/medusa/src/types/discount.ts | 28 +- 36 files changed, 1217 insertions(+), 1272 deletions(-) create mode 100644 .changeset/fast-suns-repair.md create mode 100644 packages/medusa/src/api/middlewares/discount/does-condition-belong-to-discount.ts create mode 100644 packages/medusa/src/api/routes/admin/discounts/add-resources-to-condition-batch.ts diff --git a/.changeset/fast-suns-repair.md b/.changeset/fast-suns-repair.md new file mode 100644 index 0000000000..a58c47932c --- /dev/null +++ b/.changeset/fast-suns-repair.md @@ -0,0 +1,7 @@ +--- +"@medusajs/medusa-js": patch +"medusa-react": patch +"@medusajs/medusa": patch +--- + +Feat(medusa, medusa-js, medusa-react): add resources to discount condition by batch diff --git a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap index 68602778e1..2395a494df 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap @@ -58,10 +58,6 @@ Object { } `; -exports[`/admin/discounts GET /admin/discounts/:id/conditions/:condition_id throws if condition does not belong to discount: DiscountCondition with id test-condition was not found for Discount test-discount-2 1`] = `"Request failed with status code 404"`; - -exports[`/admin/discounts GET /admin/discounts/:id/conditions/:condition_id throws if condition does not exist: DiscountCondition with id test-condition was not found 1`] = `"Request failed with status code 404"`; - exports[`/admin/discounts POST /admin/discounts fails if multiple types of resources are provided on update 1`] = ` Object { "message": "Only one of products, product_types is allowed, Only one of product_types, products is allowed", @@ -77,8 +73,6 @@ Object { } `; -exports[`/admin/discounts POST /admin/discounts/:id/conditions fails if more than one condition type is provided: Only one of products, customer_groups is allowed, Only one of customer_groups, products is allowed 1`] = `"Request failed with status code 400"`; - exports[`/admin/discounts POST /admin/discounts/:id/conditions should create a condition 1`] = ` Object { "code": "TEST", @@ -124,8 +118,6 @@ Object { } `; -exports[`/admin/discounts POST /admin/discounts/:id/conditions throws if discount does not exist: Discount with id does-not-exist was not found 1`] = `"Request failed with status code 404"`; - exports[`/admin/discounts POST /admin/discounts/:id/conditions/:condition_id should update a condition 1`] = ` Object { "code": "TEST", @@ -197,7 +189,3 @@ Object { "valid_duration": null, } `; - -exports[`/admin/discounts POST /admin/discounts/:id/conditions/:condition_id throws if condition does not exist: DiscountCondition with id does-not-exist was not found for Discount test-discount 1`] = `"Request failed with status code 404"`; - -exports[`/admin/discounts POST /admin/discounts/:id/conditions/:condition_id throws if discount does not exist: Discount with id does-not-exist was not found 1`] = `"Request failed with status code 404"`; diff --git a/integration-tests/api/__tests__/admin/discount.js b/integration-tests/api/__tests__/admin/discount.js index d4821d081d..6b79a4629c 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -18,6 +18,12 @@ const { jest.setTimeout(30000) +const adminReqConfig = { + headers: { + Authorization: "Bearer test_token", + }, +} + describe("/admin/discounts", () => { let medusaProcess let dbConnection @@ -108,18 +114,10 @@ describe("/admin/discounts", () => { }, }) - const response = await api - .get( - "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.customer_groups", - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.customer_groups", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -171,18 +169,10 @@ describe("/admin/discounts", () => { }, }) - const response = await api - .get( - "/admin/discounts/test-discount?fields=id&expand=parent_discount", - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/test-discount?fields=id&expand=parent_discount", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -195,18 +185,10 @@ describe("/admin/discounts", () => { it("should retrieve discount with product conditions created with factory", async () => { const api = useApi() - const response = await api - .get( - "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.products,rule.conditions.product_types", - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/test-discount?expand=rule,rule.conditions,rule.conditions.products,rule.conditions.product_types", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -296,15 +278,7 @@ describe("/admin/discounts", () => { it("should list discounts that match a specific query in a case insensitive manner", async () => { const api = useApi() - const response = await api - .get("/admin/discounts?q=barca", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get("/admin/discounts?q=barca", adminReqConfig) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toEqual( @@ -320,15 +294,10 @@ describe("/admin/discounts", () => { it("lists fixed discounts", async () => { const api = useApi() - const response = await api - .get("/admin/discounts?rule[type]=fixed", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts?rule[type]=fixed", + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) @@ -347,11 +316,7 @@ describe("/admin/discounts", () => { const api = useApi() await api - .get("/admin/discounts?rule[type]=blah", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/discounts?rule[type]=blah", adminReqConfig) .catch((err) => { expect(err.response.status).toEqual(400) expect(err.response.data.type).toEqual("invalid_data") @@ -368,15 +333,10 @@ describe("/admin/discounts", () => { rule: expect.objectContaining({ type: "fixed" }), }) - const response = await api - .get("/admin/discounts?rule[type]=percentage", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts?rule[type]=percentage", + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discounts).toEqual( expect.not.arrayContaining([notExpected]) @@ -386,15 +346,10 @@ describe("/admin/discounts", () => { it("lists dynamic discounts ", async () => { const api = useApi() - const response = await api - .get("/admin/discounts?is_dynamic=true", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts?is_dynamic=true", + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) @@ -411,15 +366,10 @@ describe("/admin/discounts", () => { it("lists disabled discounts ", async () => { const api = useApi() - const response = await api - .get("/admin/discounts?is_disabled=true", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts?is_disabled=true", + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) expect(response.data.discounts).toHaveLength(1) @@ -448,28 +398,20 @@ describe("/admin/discounts", () => { it("creates a discount with a rule", 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, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -493,54 +435,46 @@ describe("/admin/discounts", () => { tags: ["ss23"], }) - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - conditions: [ - { - products: [product.id], - operator: "in", - }, - { - products: [anotherProduct.id], - operator: "not_in", - }, - { - product_types: [product.type_id], - operator: "not_in", - }, - { - product_types: [anotherProduct.type_id], - operator: "in", - }, - { - product_tags: [product.tags[0].id], - operator: "not_in", - }, - { - product_tags: [anotherProduct.tags[0].id], - operator: "in", - }, - ], - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", + conditions: [ + { + products: [product.id], + operator: "in", + }, + { + products: [anotherProduct.id], + operator: "not_in", + }, + { + product_types: [product.type_id], + operator: "not_in", + }, + { + product_types: [anotherProduct.type_id], + operator: "in", + }, + { + product_tags: [product.tags[0].id], + operator: "not_in", + }, + { + product_tags: [anotherProduct.tags[0].id], + operator: "in", + }, + ], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount.rule.conditions).toEqual( @@ -584,38 +518,30 @@ describe("/admin/discounts", () => { type: "pants", }) - const response = await api - .post( - "/admin/discounts?expand=rule,rule.conditions", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - conditions: [ - { - products: [product.id], - operator: "in", - }, - { - product_types: [product.type_id], - operator: "not_in", - }, - ], - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts?expand=rule,rule.conditions", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", + conditions: [ + { + products: [product.id], + operator: "in", + }, + { + product_types: [product.type_id], + operator: "not_in", + }, + ], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount.rule.conditions).toHaveLength(2) @@ -635,32 +561,24 @@ describe("/admin/discounts", () => { const createdRule = response.data.discount.rule const condsToUpdate = createdRule.conditions[0] - const updated = await api - .post( - `/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`, - { - rule: { - id: createdRule.id, - value: createdRule.value, - allocation: createdRule.allocation, - conditions: [ - { - id: condsToUpdate.id, - operator: "not_in", - products: [product.id, anotherProduct.id], - }, - ], - }, + const updated = await api.post( + `/admin/discounts/${response.data.discount.id}?expand=rule,rule.conditions,rule.conditions.products`, + { + rule: { + id: createdRule.id, + value: createdRule.value, + allocation: createdRule.allocation, + conditions: [ + { + id: condsToUpdate.id, + operator: "not_in", + products: [product.id, anotherProduct.id], + }, + ], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + }, + adminReqConfig + ) expect(updated.status).toEqual(200) expect(updated.data.discount.rule.conditions).toEqual( @@ -696,34 +614,26 @@ describe("/admin/discounts", () => { type: "pants", }) - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - conditions: [ - { - products: [product.id], - operator: "in", - }, - ], - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", + conditions: [ + { + products: [product.id], + operator: "in", + }, + ], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) @@ -782,11 +692,7 @@ describe("/admin/discounts", () => { }, usage_limit: 10, }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) } catch (error) { expect(error.response.data.type).toEqual("invalid_data") @@ -807,34 +713,26 @@ describe("/admin/discounts", () => { type: "pants", }) - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - conditions: [ - { - products: [product.id], - operator: "in", - }, - ], - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", + conditions: [ + { + products: [product.id], + operator: "in", + }, + ], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) @@ -871,28 +769,20 @@ describe("/admin/discounts", () => { 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, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -902,21 +792,13 @@ describe("/admin/discounts", () => { }) ) - const updated = await api - .post( - `/admin/discounts/${response.data.discount.id}`, - { - usage_limit: 20, - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const updated = await api.post( + `/admin/discounts/${response.data.discount.id}`, + { + usage_limit: 20, + }, + adminReqConfig + ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( @@ -930,28 +812,20 @@ describe("/admin/discounts", () => { it("creates a discount and fails to update it because attempting type update", 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, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -971,11 +845,7 @@ describe("/admin/discounts", () => { type: "free_shipping", }, }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) @@ -988,29 +858,21 @@ describe("/admin/discounts", () => { it("creates a discount and fails to update it because attempting is_dynamic update", async () => { const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - is_dynamic: true, - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + is_dynamic: true, + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1028,11 +890,7 @@ describe("/admin/discounts", () => { usage_limit: 20, is_dynamic: false, }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) @@ -1045,28 +903,20 @@ describe("/admin/discounts", () => { it("automatically sets the code to an uppercase string on update", 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, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOworld", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1076,22 +926,14 @@ describe("/admin/discounts", () => { }) ) - const updated = await api - .post( - `/admin/discounts/${response.data.discount.id}`, - { - code: "HELLOWORLD_test", - usage_limit: 20, - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const updated = await api.post( + `/admin/discounts/${response.data.discount.id}`, + { + code: "HELLOWORLD_test", + usage_limit: 20, + }, + adminReqConfig + ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( @@ -1105,29 +947,21 @@ describe("/admin/discounts", () => { it("creates a dynamic discount and updates it", async () => { const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD_DYNAMIC", - is_dynamic: true, - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD_DYNAMIC", + is_dynamic: true, + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1138,21 +972,13 @@ describe("/admin/discounts", () => { }) ) - const updated = await api - .post( - `/admin/discounts/${response.data.discount.id}`, - { - usage_limit: 20, - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const updated = await api.post( + `/admin/discounts/${response.data.discount.id}`, + { + usage_limit: 20, + }, + adminReqConfig + ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( @@ -1183,11 +1009,7 @@ describe("/admin/discounts", () => { usage_limit: 10, regions: ["test-region", "test-region-2"], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) @@ -1201,28 +1023,20 @@ describe("/admin/discounts", () => { expect.assertions(2) const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "fixed", - value: 10, - allocation: "total", - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "fixed", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) await api .post( @@ -1230,11 +1044,7 @@ describe("/admin/discounts", () => { { regions: ["test-region", "test-region-2"], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { @@ -1249,39 +1059,27 @@ describe("/admin/discounts", () => { expect.assertions(2) const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "fixed", - value: 10, - allocation: "total", - }, - usage_limit: 10, - regions: ["test-region"], + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "fixed", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + regions: ["test-region"], + }, + adminReqConfig + ) await api .post( `/admin/discounts/${response.data.discount.id}/regions/test-region-2`, {}, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { @@ -1295,30 +1093,22 @@ describe("/admin/discounts", () => { it("creates a discount with start and end dates", 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, - starts_at: new Date("09/15/2021 11:50"), - ends_at: new Date("09/15/2021 17:50"), + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + starts_at: new Date("09/15/2021 11:50"), + ends_at: new Date("09/15/2021 17:50"), + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1338,23 +1128,15 @@ describe("/admin/discounts", () => { new Date("09/15/2021 17:50") ) - const updated = await api - .post( - `/admin/discounts/${response.data.discount.id}`, - { - usage_limit: 20, - starts_at: new Date("09/14/2021 11:50"), - ends_at: new Date("09/17/2021 17:50"), - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const updated = await api.post( + `/admin/discounts/${response.data.discount.id}`, + { + usage_limit: 20, + starts_at: new Date("09/14/2021 11:50"), + ends_at: new Date("09/17/2021 17:50"), + }, + adminReqConfig + ) expect(updated.status).toEqual(200) expect(updated.data.discount).toEqual( @@ -1380,30 +1162,22 @@ describe("/admin/discounts", () => { const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: "HELLOWORLD", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - }, - usage_limit: 10, - starts_at: new Date("09/15/2021 11:50"), - ends_at: new Date("09/15/2021 17:50"), + const response = await api.post( + "/admin/discounts", + { + code: "HELLOWORLD", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + starts_at: new Date("09/15/2021 11:50"), + ends_at: new Date("09/15/2021 17:50"), + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1430,11 +1204,7 @@ describe("/admin/discounts", () => { usage_limit: 20, ends_at: new Date("09/11/2021 17:50"), }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) @@ -1463,11 +1233,7 @@ describe("/admin/discounts", () => { starts_at: new Date("09/15/2021 11:50"), ends_at: new Date("09/14/2021 17:50"), }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.status).toEqual(400) @@ -1510,23 +1276,15 @@ describe("/admin/discounts", () => { it("Removes ends_at, valid_duration and usage_limit when fields are updated with null", async () => { const api = useApi() - await api - .post( - "/admin/discounts/test-discount", - { - ends_at: null, - valid_duration: null, - usage_limit: null, - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + await api.post( + "/admin/discounts/test-discount", + { + ends_at: null, + valid_duration: null, + usage_limit: null, + }, + adminReqConfig + ) const resultingDiscount = await api.get( "/admin/discounts/test-discount", @@ -1574,39 +1332,23 @@ describe("/admin/discounts", () => { const api = useApi() // First we soft-delete the discount - await api - .delete("/admin/discounts/test-discount", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + await api.delete("/admin/discounts/test-discount", adminReqConfig) // Lets try to create a discount with same code as deleted one - const response = await api - .post( - "/admin/discounts", - { - code: "TESTING", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: "TESTING", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1634,11 +1376,7 @@ describe("/admin/discounts", () => { }, usage_limit: 10, }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) } catch (error) { expect(error.response.data.message).toEqual( @@ -1692,21 +1430,13 @@ describe("/admin/discounts", () => { it("creates a dynamic discount with ends_at", 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) - }) + const response = await api.post( + "/admin/discounts/test-discount/dynamic-codes", + { + code: "HELLOWORLD", + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1720,21 +1450,13 @@ describe("/admin/discounts", () => { it("creates a dynamic discount without ends_at", async () => { const api = useApi() - const response = await api - .post( - "/admin/discounts/test-discount1/dynamic-codes", - { - code: "HELLOWORLD", - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.post( + "/admin/discounts/test-discount1/dynamic-codes", + { + code: "HELLOWORLD", + }, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.discount).toEqual( @@ -1816,15 +1538,10 @@ describe("/admin/discounts", () => { }, }) - const response = await api - .delete("/admin/discounts/test-discount/conditions/test-condition", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.delete( + "/admin/discounts/test-discount/conditions/test-condition", + adminReqConfig + ) const disc = response.data @@ -1841,15 +1558,10 @@ describe("/admin/discounts", () => { it("should not fail if condition does not exist", async () => { const api = useApi() - const response = await api - .delete("/admin/discounts/test-discount/conditions/test-condition", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.delete( + "/admin/discounts/test-discount/conditions/test-condition", + adminReqConfig + ) const disc = response.data @@ -1866,20 +1578,16 @@ describe("/admin/discounts", () => { it("should fail if discount does not exist", async () => { const api = useApi() - try { - await api.delete( + const err = await api + .delete( "/admin/discounts/not-exist/conditions/test-condition", - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "Discount with id not-exist was not found" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Discount with id not-exist was not found" + ) }) }) @@ -1917,22 +1625,14 @@ describe("/admin/discounts", () => { const prod = await simpleProductFactory(dbConnection, { type: "pants" }) - const response = await api - .post( - "/admin/discounts/test-discount/conditions", - { - operator: "in", - products: [prod.id], - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.post( + "/admin/discounts/test-discount/conditions", + { + operator: "in", + products: [prod.id], + }, + adminReqConfig + ) const disc = response.data.discount @@ -1976,25 +1676,21 @@ describe("/admin/discounts", () => { groups: [group], }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/test-discount/conditions", { operator: "in", products: [prod.id], customer_groups: ["customer-group-1"], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "Only one of products, customer_groups is allowed, Only one of customer_groups, products is allowed" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Only one of products, customer_groups is allowed, Only one of customer_groups, products is allowed" + ) }) it("throws if discount does not exist", async () => { @@ -2004,23 +1700,19 @@ describe("/admin/discounts", () => { const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/does-not-exist/conditions/test-condition", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "Discount with id does-not-exist was not found" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Discount with id does-not-exist was not found" + ) }) }) @@ -2081,33 +1773,20 @@ describe("/admin/discounts", () => { type: "pants", }) - const discount = await api - .get("/admin/discounts/test-discount", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const discount = await api.get( + "/admin/discounts/test-discount", + adminReqConfig + ) const cond = discount.data.discount.rule.conditions[0] - const response = await api - .post( - `/admin/discounts/test-discount/conditions/${cond.id}?expand=rule,rule.conditions,rule.conditions.products`, - { - products: [prod2.id], - }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const response = await api.post( + `/admin/discounts/test-discount/conditions/${cond.id}?expand=rule,rule.conditions,rule.conditions.products`, + { + products: [prod2.id], + }, + adminReqConfig + ) const disc = response.data.discount @@ -2149,23 +1828,19 @@ describe("/admin/discounts", () => { const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/test-discount/conditions/does-not-exist", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "DiscountCondition with id does-not-exist was not found for Discount test-discount" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "DiscountCondition with id does-not-exist was not found" + ) }) it("throws if discount does not exist", async () => { @@ -2175,23 +1850,19 @@ describe("/admin/discounts", () => { const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/does-not-exist/conditions/test-condition", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "Discount with id does-not-exist was not found" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Discount with id does-not-exist was not found" + ) }) it("throws if condition does not belong to discount", async () => { @@ -2199,23 +1870,19 @@ describe("/admin/discounts", () => { const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/test-discount-2/conditions/test-condition", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "DiscountCondition with id test-condition was not found for Discount test-discount-2" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Condition with id test-condition does not belong to Discount with id test-discount-2" + ) }) }) @@ -2255,15 +1922,10 @@ describe("/admin/discounts", () => { it("should get condition", async () => { const api = useApi() - const discountCondition = await api - .get("/admin/discounts/test-discount/conditions/test-condition", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const discountCondition = await api.get( + "/admin/discounts/test-discount/conditions/test-condition", + adminReqConfig + ) const cond = discountCondition.data.discount_condition @@ -2286,18 +1948,10 @@ describe("/admin/discounts", () => { it("should get condition with expand + fields", async () => { const api = useApi() - const discountCondition = await api - .get( - "/admin/discounts/test-discount/conditions/test-condition?expand=products&fields=id,type", - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + const discountCondition = await api.get( + "/admin/discounts/test-discount/conditions/test-condition?expand=products&fields=id,type", + adminReqConfig + ) const cond = discountCondition.data.discount_condition @@ -2322,47 +1976,57 @@ describe("/admin/discounts", () => { const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( + const err = await api + .post( "/admin/discounts/test-discount/conditions/does-not-exist", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "DiscountCondition with id test-condition was not found" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "DiscountCondition with id does-not-exist was not found" + ) }) it("throws if condition does not belong to discount", async () => { const api = useApi() + await simpleDiscountFactory(dbConnection, { + id: "test-discount-2", + code: "TEST2", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + id: "test-condition-2", + type: "products", + operator: "in", + products: [], + }, + ], + }, + }) + const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) - try { - await api.post( - "/admin/discounts/test-discount-2/conditions/test-condition", + const err = await api + .post( + "/admin/discounts/test-discount/conditions/test-condition-2", { products: [prod2.id], }, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) - } catch (error) { - expect(error.message).toMatchSnapshot( - "DiscountCondition with id test-condition was not found for Discount test-discount-2" - ) - } + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Condition with id test-condition-2 does not belong to Discount with id test-discount" + ) }) }) @@ -2398,15 +2062,10 @@ describe("/admin/discounts", () => { it("should retrieve discount using uppercase code", async () => { const api = useApi() - const response = await api - .get("/admin/discounts/code/TEST", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/code/TEST", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -2421,15 +2080,10 @@ describe("/admin/discounts", () => { it("should retrieve discount using lowercase code", async () => { const api = useApi() - const response = await api - .get("/admin/discounts/code/test", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/code/test", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -2444,15 +2098,10 @@ describe("/admin/discounts", () => { it("should retrieve discount using mixed casing code", async () => { const api = useApi() - const response = await api - .get("/admin/discounts/code/TesT", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/code/TesT", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -2468,11 +2117,7 @@ describe("/admin/discounts", () => { const api = useApi() try { - await api.get("/admin/discounts/code/non-existing", { - headers: { - Authorization: "Bearer test_token", - }, - }) + await api.get("/admin/discounts/code/non-existing", adminReqConfig) } catch (error) { expect(error.response.status).toEqual(404) expect(error.response.data.message).toBe( @@ -2484,28 +2129,20 @@ describe("/admin/discounts", () => { it("should trim and uppercase code on insert", async () => { const api = useApi() - const response = await api - .post( - "/admin/discounts", - { - code: " Testing ", - rule: { - description: "test", - type: "percentage", - value: 10, - allocation: "total", - }, - usage_limit: 10, + const response = await api.post( + "/admin/discounts", + { + code: " Testing ", + rule: { + description: "test", + type: "percentage", + value: 10, + allocation: "total", }, - { - headers: { - Authorization: "Bearer test_token", - }, - } - ) - .catch((err) => { - console.log(err) - }) + usage_limit: 10, + }, + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -2528,15 +2165,10 @@ describe("/admin/discounts", () => { }, }) - const response = await api - .get("/admin/discounts/code/ testing", { - headers: { - Authorization: "Bearer test_token", - }, - }) - .catch((err) => { - console.log(err) - }) + const response = await api.get( + "/admin/discounts/code/ testing", + adminReqConfig + ) const disc = response.data.discount expect(response.status).toEqual(200) @@ -2547,4 +2179,144 @@ describe("/admin/discounts", () => { ) }) }) + + describe("POST /admin/discounts/:id/conditions/:condition_id/batch", () => { + let prod1 + + beforeEach(async () => { + await adminSeeder(dbConnection) + + prod1 = await simpleProductFactory(dbConnection, { type: "pants" }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount", + code: "TEST", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + id: "test-condition", + type: "products", + operator: "in", + products: [prod1.id], + }, + ], + }, + }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount-2", + code: "TEST2", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + id: "test-condition-2", + type: "products", + operator: "in", + products: [], + }, + ], + }, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update a condition with batch items", async () => { + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { + id: "test-product-2", + type: "pants 2", + }) + const prod3 = await simpleProductFactory(dbConnection, { + id: "test-product-3", + type: "pants 3", + }) + + const discount = await api.get( + "/admin/discounts/test-discount", + adminReqConfig + ) + + const cond = discount.data.discount.rule.conditions[0] + + const response = await api.post( + `/admin/discounts/test-discount/conditions/${cond.id}/batch?expand=rule,rule.conditions,rule.conditions.products`, + { + resources: [{ id: prod2.id }, { id: prod3.id }], + }, + adminReqConfig + ) + + const disc = response.data.discount + + expect(response.status).toEqual(200) + expect(disc.rule.conditions).toHaveLength(1) + expect(disc.rule.conditions[0].products).toHaveLength(3) + expect(disc).toEqual( + expect.objectContaining({ + id: "test-discount", + code: "TEST", + rule: expect.objectContaining({ + conditions: expect.arrayContaining([ + expect.objectContaining({ + products: expect.arrayContaining([ + expect.objectContaining({ + id: prod1.id, + }), + expect.objectContaining({ + id: prod2.id, + }), + expect.objectContaining({ + id: prod3.id, + }), + ]), + }), + ]), + }), + }) + ) + }) + + it("should fail if condition does not belong to discount", async () => { + const api = useApi() + + const err = await api + .post( + "/admin/discounts/test-discount/conditions/test-condition-2/batch", + {}, + adminReqConfig + ) + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Condition with id test-condition-2 does not belong to Discount with id test-discount" + ) + }) + + it("should fail if discount does not exist", async () => { + const api = useApi() + + const err = await api + .post( + "/admin/discounts/not-exist/conditions/test-condition/batch", + {}, + adminReqConfig + ) + .catch((e) => e) + + expect(err.response.data.message).toBe( + "Discount with id not-exist was not found" + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/discounts.ts b/packages/medusa-js/src/resources/admin/discounts.ts index 7bbda8c425..6e37247bcb 100644 --- a/packages/medusa-js/src/resources/admin/discounts.ts +++ b/packages/medusa-js/src/resources/admin/discounts.ts @@ -7,6 +7,8 @@ import { AdminGetDiscountsParams, AdminPostDiscountsDiscountConditions, AdminPostDiscountsDiscountConditionsCondition, + AdminPostDiscountsDiscountConditionsConditionBatchParams, + AdminPostDiscountsDiscountConditionsConditionBatchReq, AdminPostDiscountsDiscountConditionsConditionParams, AdminPostDiscountsDiscountConditionsParams, AdminPostDiscountsDiscountDynamicCodesReq, @@ -121,7 +123,7 @@ class AdminDiscountsResource extends BaseResource { if (query) { const queryString = qs.stringify(query) - path = `/admin/discounts?${queryString}` + path += `?${queryString}` } return this.client.request("GET", path, undefined, {}, customHeaders) @@ -152,7 +154,7 @@ class AdminDiscountsResource extends BaseResource { if (query) { const queryString = qs.stringify(query) - path = `/admin/discounts/${discountId}/conditions?${queryString}` + path += `?${queryString}` } return this.client.request("POST", path, payload, {}, customHeaders) @@ -172,7 +174,7 @@ class AdminDiscountsResource extends BaseResource { if (query) { const queryString = qs.stringify(query) - path = `/admin/discounts/${discountId}/conditions/${conditionId}?${queryString}` + path += `?${queryString}` } return this.client.request("POST", path, payload, {}, customHeaders) @@ -203,11 +205,31 @@ class AdminDiscountsResource extends BaseResource { if (query) { const queryString = qs.stringify(query) - path = `/admin/discounts/${discountId}/conditions/${conditionId}?${queryString}` + path += `?${queryString}` } return this.client.request("GET", path, undefined, {}, customHeaders) } + + /** + * @description Add a batch of items to a discount condition + */ + addConditionResourceBatch( + discountId: string, + conditionId: string, + payload: AdminPostDiscountsDiscountConditionsConditionBatchReq, + query?: AdminPostDiscountsDiscountConditionsConditionBatchParams, + customHeaders: Record = {} + ): ResponsePromise { + let path = `/admin/discounts/${discountId}/conditions/${conditionId}/batch` + + if (query) { + const queryString = qs.stringify(query) + path += `?${queryString}` + } + + return this.client.request("POST", path, payload, {}, customHeaders) + } } export default AdminDiscountsResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index d373f4911c..e9c9a0c448 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -912,6 +912,33 @@ export const adminHandlers = [ } ), + rest.post( + "/admin/discounts/:id/conditions/:conditionId/batch", + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + discount: { + ...fixtures.get("discount"), + rule: { + ...fixtures.get("discount").rule, + conditions: [ + { + ...fixtures.get("discount").rule.conditions[0], + products: [ + ...(fixtures.get("discount").rule.conditions[0]?.products ?? + []), + ...(req.body as any).resources, + ], + }, + ], + }, + }, + }) + ) + } + ), + rest.get("/admin/draft-orders/", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/admin/discounts/mutations.ts b/packages/medusa-react/src/hooks/admin/discounts/mutations.ts index 834f289a39..c79e13bd46 100644 --- a/packages/medusa-react/src/hooks/admin/discounts/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/discounts/mutations.ts @@ -3,16 +3,42 @@ import { AdminDiscountsRes, AdminPostDiscountsDiscountConditions, AdminPostDiscountsDiscountConditionsCondition, + AdminPostDiscountsDiscountConditionsConditionBatchParams, + AdminPostDiscountsDiscountConditionsConditionBatchReq, AdminPostDiscountsDiscountDynamicCodesReq, AdminPostDiscountsDiscountReq, - AdminPostDiscountsReq + AdminPostDiscountsReq, } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useMutation, UseMutationOptions, useQueryClient } from "react-query" -import { useMedusa } from "../../../contexts/medusa" +import { useMedusa } from "../../../contexts" import { buildOptions } from "../../utils/buildOptions" import { adminDiscountKeys } from "./queries" +export const useAdminAddDiscountConditionResourceBatch = ( + discountId: string, + conditionId: string, + query?: AdminPostDiscountsDiscountConditionsConditionBatchParams, + options?: UseMutationOptions< + Response, + Error, + AdminPostDiscountsDiscountConditionsConditionBatchReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostDiscountsDiscountConditionsConditionBatchReq) => + client.admin.discounts.addConditionResourceBatch( + discountId, + conditionId, + payload, + query + ), + buildOptions(queryClient, adminDiscountKeys.detail(discountId), options) + ) +} + export const useAdminCreateDiscount = ( options?: UseMutationOptions< Response, diff --git a/packages/medusa-react/src/hooks/admin/discounts/queries.ts b/packages/medusa-react/src/hooks/admin/discounts/queries.ts index 9b989493a5..1cbf47cdcd 100644 --- a/packages/medusa-react/src/hooks/admin/discounts/queries.ts +++ b/packages/medusa-react/src/hooks/admin/discounts/queries.ts @@ -10,7 +10,7 @@ import { Response } from "@medusajs/medusa-js" import { useQuery } from "react-query" import { useMedusa } from "../../../contexts" import { UseQueryOptionsWrapper } from "../../../types" -import { queryKeysFactory } from "../../utils/index" +import { queryKeysFactory } from "../../utils" const ADMIN_DISCOUNTS_QUERY_KEY = `admin_discounts` as const diff --git a/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts b/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts index 93fef23f70..9b18f99f4a 100644 --- a/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts @@ -2,6 +2,7 @@ import { DiscountConditionOperator } from "@medusajs/medusa" import { renderHook } from "@testing-library/react-hooks" import { fixtures } from "../../../../mocks/data" import { + useAdminAddDiscountConditionResourceBatch, useAdminCreateDiscount, useAdminCreateDynamicDiscountCode, useAdminDeleteDiscount, @@ -15,6 +16,49 @@ import { } from "../../../../src/" import { createWrapper } from "../../../utils" +describe("useAdminAddDiscountConditionResourceBatch hook", () => { + test("add items to a discount condition and return the discount", async () => { + const resources = [ + { + id: fixtures.get("product").id, + }, + ] + const discountId = fixtures.get("discount").id + const conditionId = fixtures.get("discount").rule.conditions[0].id + + const { result, waitFor } = renderHook( + () => useAdminAddDiscountConditionResourceBatch(discountId, conditionId), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ resources }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.discount).toEqual( + expect.objectContaining({ + ...fixtures.get("discount"), + rule: { + ...fixtures.get("discount").rule, + conditions: [ + { + ...fixtures.get("discount").rule.conditions[0], + products: [ + ...(fixtures.get("discount").rule.conditions[0]?.products ?? + []), + { id: fixtures.get("product").id }, + ], + }, + ], + }, + }) + ) + }) +}) + describe("useAdminCreateDiscount hook", () => { test("creates a discount and returns it", async () => { const discount = { diff --git a/packages/medusa/src/api/middlewares/discount/does-condition-belong-to-discount.ts b/packages/medusa/src/api/middlewares/discount/does-condition-belong-to-discount.ts new file mode 100644 index 0000000000..fa478a8554 --- /dev/null +++ b/packages/medusa/src/api/middlewares/discount/does-condition-belong-to-discount.ts @@ -0,0 +1,33 @@ +import { MedusaError } from "medusa-core-utils" +import { DiscountConditionService, DiscountService } from "../../../services" + +export async function doesConditionBelongToDiscount(req, res, next) { + try { + const { discount_id, condition_id } = req.params + const conditionService: DiscountConditionService = req.scope.resolve( + "discountConditionService" + ) + const discountService: DiscountService = + req.scope.resolve("discountService") + + const discount = await discountService.retrieve(discount_id, { + select: ["id", "rule_id"], + }) + const condition = await conditionService.retrieve(condition_id, { + select: ["id", "discount_rule_id"], + }) + + if (condition.discount_rule_id !== discount.rule_id) { + return next( + new MedusaError( + MedusaError.Types.INVALID_DATA, + `Condition with id ${condition_id} does not belong to Discount with id ${discount_id}` + ) + ) + } + + next() + } catch (e) { + next(e) + } +} diff --git a/packages/medusa/src/api/middlewares/index.ts b/packages/medusa/src/api/middlewares/index.ts index 08d79c2701..2557b5d956 100644 --- a/packages/medusa/src/api/middlewares/index.ts +++ b/packages/medusa/src/api/middlewares/index.ts @@ -5,6 +5,7 @@ import { default as wrap } from "./await-middleware" export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job" export { canAccessBatchJob } from "./batch-job/can-access-batch-job" +export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount" export { transformQuery } from "./transform-query" export { transformBody } from "./transform-body" diff --git a/packages/medusa/src/api/routes/admin/discounts/add-resources-to-condition-batch.ts b/packages/medusa/src/api/routes/admin/discounts/add-resources-to-condition-batch.ts new file mode 100644 index 0000000000..9c13d5b179 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/add-resources-to-condition-batch.ts @@ -0,0 +1,131 @@ +import DiscountConditionService from "../../../../services/discount-condition" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" +import { DiscountService } from "../../../../services" +import { + DiscountConditionMapTypeToProperty, + UpsertDiscountConditionInput, +} from "../../../../types/discount" +import { IsArray } from "class-validator" +import { FindParams } from "../../../../types/common" + +/** + * @oas [post] /discounts/{discount_id}/conditions/{condition_id}/batch + * operationId: "PostDiscountsDiscountConditionsConditionBatch" + * summary: "Add a batch of resources to a discount condition" + * description: "Add a batch of resources to a discount condition." + * x-authenticated: true + * parameters: + * - (path) discount_id=* {string} The ID of the Product. + * - (path) condition_id=* {string} The ID of the condition on which to add the item. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each discount of the result. + * - (query) fields {string} (Comma separated) Which fields should be included in each discount of the result. + * requestBody: + * content: + * application/json: + * schema: + * required: + * - resources + * properties: + * resources: + * description: The resources to be added to the discount condition + * type: array + * items: + * required: + * - id + * properties: + * id: + * description: The id of the item + * type: string + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * import { DiscountConditionOperator } from "@medusajs/medusa" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.discounts.addConditionResourceBatch(discount_id, condition_id, { + * resources: [{ id: item_id }] + * }) + * .then(({ discount }) => { + * console.log(discount.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/discounts/{id}/conditions/{condition_id}/batch' \ + * --header 'Authorization: Bearer {api_token}' \ + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "resources": [{ "id": "item_id" }] + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Discount Condition + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * discount: + * $ref: "#/components/schemas/discount" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { discount_id, condition_id } = req.params + const validatedBody = + req.validatedBody as AdminPostDiscountsDiscountConditionsConditionBatchReq + + const conditionService: DiscountConditionService = req.scope.resolve( + "discountConditionService" + ) + const manager: EntityManager = req.scope.resolve("manager") + + const condition = await conditionService.retrieve(condition_id, { + select: ["id", "type", "discount_rule_id"], + }) + + const updateObj: UpsertDiscountConditionInput = { + id: condition_id, + rule_id: condition.discount_rule_id, + [DiscountConditionMapTypeToProperty[condition.type]]: + validatedBody.resources, + } + + await manager.transaction(async (transactionManager) => { + await conditionService + .withTransaction(transactionManager) + .upsertCondition(updateObj, false) + }) + + const discountService: DiscountService = req.scope.resolve("discountService") + const discount = await discountService.retrieve( + discount_id, + req.retrieveConfig + ) + + res.status(200).json({ discount }) +} + +export class AdminPostDiscountsDiscountConditionsConditionBatchReq { + @IsArray() + resources: { id: string }[] +} + +export class AdminPostDiscountsDiscountConditionsConditionBatchParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/create-condition.ts b/packages/medusa/src/api/routes/admin/discounts/create-condition.ts index d5292d7504..eab26c3591 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-condition.ts +++ b/packages/medusa/src/api/routes/admin/discounts/create-condition.ts @@ -1,13 +1,12 @@ -import { Discount, DiscountConditionOperator } from "../../../../models" -import { IsOptional, IsString } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." +import { Request, Response } from "express" +import { DiscountConditionOperator } from "../../../../models" +import { IsString } from "class-validator" import { AdminUpsertConditionsReq } from "../../../../types/discount" import DiscountConditionService from "../../../../services/discount-condition" import { DiscountService } from "../../../../services" import { EntityManager } from "typeorm" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" /** * @oas [post] /discounts/{discount_id}/conditions @@ -105,19 +104,8 @@ import { validator } from "../../../../utils/validator" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { discount_id } = req.params - - const validatedCondition = await validator( - AdminPostDiscountsDiscountConditions, - req.body - ) - - const validatedParams = await validator( - AdminPostDiscountsDiscountConditionsParams, - req.query - ) - const conditionService: DiscountConditionService = req.scope.resolve( "discountConditionService" ) @@ -130,19 +118,12 @@ export default async (req, res) => { return await conditionService .withTransaction(transactionManager) .upsertCondition({ - ...validatedCondition, + ...(req.validatedBody as AdminPostDiscountsDiscountConditions), rule_id: discount.rule_id, }) }) - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validatedParams?.fields?.split(",") as (keyof Discount)[], - validatedParams?.expand?.split(",") - ) - - discount = await discountService.retrieve(discount.id, config) + discount = await discountService.retrieve(discount.id, req.retrieveConfig) res.status(200).json({ discount }) } @@ -153,12 +134,4 @@ export class AdminPostDiscountsDiscountConditions extends AdminUpsertConditionsR operator: DiscountConditionOperator } -export class AdminPostDiscountsDiscountConditionsParams { - @IsString() - @IsOptional() - expand?: string - - @IsString() - @IsOptional() - fields?: string -} +export class AdminPostDiscountsDiscountConditionsParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/create-discount.ts b/packages/medusa/src/api/routes/admin/discounts/create-discount.ts index 1980d7e715..c13c48459a 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-discount.ts +++ b/packages/medusa/src/api/routes/admin/discounts/create-discount.ts @@ -1,6 +1,5 @@ import { AllocationType, - Discount, DiscountConditionOperator, DiscountRuleType, } from "../../../../models" @@ -17,17 +16,14 @@ import { IsString, ValidateNested, } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." - -import { AdminPostDiscountsDiscountParams } from "./update-discount" import { AdminUpsertConditionsReq } from "../../../../types/discount" import DiscountService from "../../../../services/discount" import { EntityManager } from "typeorm" import { IsGreaterThan } from "../../../../utils/validators/greater-than" import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration" import { Type } from "class-transformer" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { Request, Response } from "express" +import { FindParams } from "../../../../types/common" /** * @oas [post] /discounts @@ -202,32 +198,21 @@ import { validator } from "../../../../utils/validator" * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { - const validated = await validator(AdminPostDiscountsReq, req.body) - - const validatedParams = await validator( - AdminPostDiscountsDiscountParams, - req.query - ) - +export default async (req: Request, res: Response) => { const discountService: DiscountService = req.scope.resolve("discountService") const manager: EntityManager = req.scope.resolve("manager") const created = await manager.transaction(async (transactionManager) => { return await discountService .withTransaction(transactionManager) - .create(validated) + .create(req.validatedBody as AdminPostDiscountsReq) }) - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validatedParams?.fields?.split(",") as (keyof Discount)[], - validatedParams?.expand?.split(",") + const discount = await discountService.retrieve( + created.id, + req.retrieveConfig ) - const discount = await discountService.retrieve(created.id, config) - res.status(200).json({ discount }) } @@ -309,12 +294,4 @@ export class AdminCreateCondition extends AdminUpsertConditionsReq { operator: DiscountConditionOperator } -export class AdminPostDiscountsParams { - @IsArray() - @IsOptional() - expand?: string[] - - @IsArray() - @IsOptional() - fields?: string[] -} +export class AdminPostDiscountsParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.ts b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.ts index ef7a8dd1fd..fce2a62dea 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.ts +++ b/packages/medusa/src/api/routes/admin/discounts/create-dynamic-code.ts @@ -1,3 +1,4 @@ +import { Request, Response } from "express" import { IsNotEmpty, IsNumber, @@ -9,7 +10,6 @@ import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." import DiscountService from "../../../../services/discount" import { EntityManager } from "typeorm" -import { validator } from "../../../../utils/validator" /** * @oas [post] /discounts/{id}/dynamic-codes @@ -72,20 +72,18 @@ import { validator } from "../../../../utils/validator" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { discount_id } = req.params - const validated = await validator( - AdminPostDiscountsDiscountDynamicCodesReq, - req.body - ) - const discountService: DiscountService = req.scope.resolve("discountService") const manager: EntityManager = req.scope.resolve("manager") const created = await manager.transaction(async (transactionManager) => { return await discountService .withTransaction(transactionManager) - .createDynamicCode(discount_id, validated) + .createDynamicCode( + discount_id, + req.validatedBody as AdminPostDiscountsDiscountDynamicCodesReq + ) }) const discount = await discountService.retrieve(created.id, { diff --git a/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts b/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts index 81802a779a..b0e6b9c690 100644 --- a/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts +++ b/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts @@ -1,13 +1,8 @@ -import { IsOptional, IsString } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." - -import { Discount } from "../../../../models" import DiscountConditionService from "../../../../services/discount-condition" import { DiscountService } from "../../../../services" import { EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" /** * @oas [delete] /discounts/{discount_id}/conditions/{condition_id} @@ -78,39 +73,34 @@ import { validator } from "../../../../utils/validator" export default async (req, res) => { const { discount_id, condition_id } = req.params - const validatedParams = await validator( - AdminDeleteDiscountsDiscountConditionsConditionParams, - req.query - ) - const conditionService: DiscountConditionService = req.scope.resolve( "discountConditionService" ) + const discountService: DiscountService = req.scope.resolve("discountService") const condition = await conditionService .retrieve(condition_id) .catch(() => void 0) if (!condition) { + const discount = await discountService.retrieve( + discount_id, + req.retrieveConfig + ) // resolves idempotently in case of non-existing condition return res.json({ id: condition_id, object: "discount-condition", deleted: true, + discount, }) } - const discountService: DiscountService = req.scope.resolve("discountService") - let discount = await discountService.retrieve(discount_id, { - relations: ["rule", "rule.conditions"], + select: ["id", "rule_id"], }) - const existsOnDiscount = discount.rule.conditions.some( - (c) => c.id === condition_id - ) - - if (!existsOnDiscount) { + if (condition.discount_rule_id !== discount.rule_id) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Condition with id ${condition_id} does not belong to Discount with id ${discount_id}` @@ -124,14 +114,7 @@ export default async (req, res) => { .delete(condition_id) }) - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validatedParams?.fields?.split(",") as (keyof Discount)[], - validatedParams?.expand?.split(",") - ) - - discount = await discountService.retrieve(discount_id, config) + discount = await discountService.retrieve(discount_id, req.retrieveConfig) res.json({ id: condition_id, @@ -141,12 +124,4 @@ export default async (req, res) => { }) } -export class AdminDeleteDiscountsDiscountConditionsConditionParams { - @IsString() - @IsOptional() - expand?: string - - @IsString() - @IsOptional() - fields?: string -} +export class AdminDeleteDiscountsDiscountConditionsConditionParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/get-condition.ts b/packages/medusa/src/api/routes/admin/discounts/get-condition.ts index 2d5414c7f1..7b3ac24629 100644 --- a/packages/medusa/src/api/routes/admin/discounts/get-condition.ts +++ b/packages/medusa/src/api/routes/admin/discounts/get-condition.ts @@ -1,15 +1,6 @@ -import { IsOptional, IsString } from "class-validator" -import { - defaultAdminDiscountConditionFields, - defaultAdminDiscountConditionRelations, -} from "." - -import { DiscountCondition } from "../../../../models" +import { Request, Response } from "express" import DiscountConditionService from "../../../../services/discount-condition" -import { DiscountService } from "../../../../services" -import { MedusaError } from "medusa-core-utils" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" /** * @oas [get] /discounts/{discount_id}/conditions/{condition_id} @@ -66,37 +57,8 @@ import { validator } from "../../../../utils/validator" * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { - const { discount_id, condition_id } = req.params - - const validatedParams = await validator( - AdminGetDiscountsDiscountConditionsConditionParams, - req.query - ) - - const discountService: DiscountService = req.scope.resolve("discountService") - - const discount = await discountService.retrieve(discount_id, { - relations: ["rule", "rule.conditions"], - }) - - const existsOnDiscount = discount.rule.conditions.some( - (c) => c.id === condition_id - ) - - if (!existsOnDiscount) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Condition with id ${condition_id} does not belong to Discount with id ${discount_id}` - ) - } - - const config = getRetrieveConfig( - defaultAdminDiscountConditionFields, - defaultAdminDiscountConditionRelations, - validatedParams?.fields?.split(",") as (keyof DiscountCondition)[], - validatedParams?.expand?.split(",") - ) +export default async (req: Request, res: Response) => { + const { condition_id } = req.params const conditionService: DiscountConditionService = req.scope.resolve( "discountConditionService" @@ -104,18 +66,10 @@ export default async (req, res) => { const discountCondition = await conditionService.retrieve( condition_id, - config + req.retrieveConfig ) res.status(200).json({ discount_condition: discountCondition }) } -export class AdminGetDiscountsDiscountConditionsConditionParams { - @IsString() - @IsOptional() - expand?: string - - @IsString() - @IsOptional() - fields?: string -} +export class AdminGetDiscountsDiscountConditionsConditionParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/get-discount-by-code.ts b/packages/medusa/src/api/routes/admin/discounts/get-discount-by-code.ts index 389e7e23e0..2a9eb497b1 100644 --- a/packages/medusa/src/api/routes/admin/discounts/get-discount-by-code.ts +++ b/packages/medusa/src/api/routes/admin/discounts/get-discount-by-code.ts @@ -1,10 +1,7 @@ -import { IsOptional, IsString } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." - -import { Discount } from "../../../../models" +import { Request, Response } from "express" import DiscountService from "../../../../services/discount" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" + /** * @oas [get] /discounts/code/{code} * operationId: "GetDiscountsDiscountCode" @@ -58,33 +55,16 @@ import { validator } from "../../../../utils/validator" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { code } = req.params - const validated = await validator( - AdminGetDiscountsDiscountCodeParams, - req.query - ) - - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validated?.fields?.split(",") as (keyof Discount)[], - validated?.expand?.split(",") - ) - const discountService: DiscountService = req.scope.resolve("discountService") - const discount = await discountService.retrieveByCode(code, config) + const discount = await discountService.retrieveByCode( + code, + req.retrieveConfig + ) res.status(200).json({ discount }) } -export class AdminGetDiscountsDiscountCodeParams { - @IsOptional() - @IsString() - expand?: string - - @IsOptional() - @IsString() - fields?: string -} +export class AdminGetDiscountsDiscountCodeParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/get-discount.ts b/packages/medusa/src/api/routes/admin/discounts/get-discount.ts index f7b45f83ff..c826f22373 100644 --- a/packages/medusa/src/api/routes/admin/discounts/get-discount.ts +++ b/packages/medusa/src/api/routes/admin/discounts/get-discount.ts @@ -1,10 +1,7 @@ -import { IsOptional, IsString } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." - -import { Discount } from "../../../.." import DiscountService from "../../../../services/discount" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { Request, Response } from "express" +import { FindParams } from "../../../../types/common" + /** * @oas [get] /discounts/{id} * operationId: "GetDiscountsDiscount" @@ -58,30 +55,13 @@ import { validator } from "../../../../utils/validator" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { discount_id } = req.params - const validated = await validator(AdminGetDiscountParams, req.query) - - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validated?.fields?.split(",") as (keyof Discount)[], - validated?.expand?.split(",") - ) - const discountService: DiscountService = req.scope.resolve("discountService") - const data = await discountService.retrieve(discount_id, config) + const data = await discountService.retrieve(discount_id, req.retrieveConfig) res.status(200).json({ discount: data }) } -export class AdminGetDiscountParams { - @IsOptional() - @IsString() - expand?: string - - @IsOptional() - @IsString() - fields?: string -} +export class AdminGetDiscountParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/index.ts b/packages/medusa/src/api/routes/admin/discounts/index.ts index 8f93dd555d..49e7779f7f 100644 --- a/packages/medusa/src/api/routes/admin/discounts/index.ts +++ b/packages/medusa/src/api/routes/admin/discounts/index.ts @@ -3,26 +3,89 @@ import "reflect-metadata" import { Discount } from "../../../.." import { DiscountCondition } from "../../../../models" import { DeleteResponse, PaginatedResponse } from "../../../../types/common" -import middlewares from "../../../middlewares" +import middlewares, { + doesConditionBelongToDiscount, + transformBody, + transformQuery, +} from "../../../middlewares" +import { + AdminPostDiscountsDiscountConditionsConditionBatchParams, + AdminPostDiscountsDiscountConditionsConditionBatchReq, +} from "./add-resources-to-condition-batch" +import { + AdminPostDiscountsDiscountConditionsCondition, + AdminPostDiscountsDiscountConditionsConditionParams, +} from "./update-condition" +import { + AdminPostDiscountsDiscountConditions, + AdminPostDiscountsDiscountConditionsParams, +} from "./create-condition" +import { AdminPostDiscountsDiscountDynamicCodesReq } from "./create-dynamic-code" +import { + AdminPostDiscountsDiscountParams, + AdminPostDiscountsDiscountReq, +} from "./update-discount" +import { + AdminPostDiscountsParams, + AdminPostDiscountsReq, +} from "./create-discount" +import { AdminGetDiscountsParams } from "./list-discounts" +import { AdminGetDiscountsDiscountConditionsConditionParams } from "./get-condition" +import { AdminDeleteDiscountsDiscountConditionsConditionParams } from "./delete-condition" +import { AdminGetDiscountsDiscountCodeParams } from "./get-discount-by-code" +import { AdminGetDiscountParams } from "./get-discount" const route = Router() export default (app) => { app.use("/discounts", route) - route.get("/", middlewares.wrap(require("./list-discounts").default)) - route.post("/", middlewares.wrap(require("./create-discount").default)) + route.get( + "/", + transformQuery(AdminGetDiscountsParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: true, + }), + middlewares.wrap(require("./list-discounts").default) + ) + route.post( + "/", + transformQuery(AdminPostDiscountsParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), + transformBody(AdminPostDiscountsReq), + middlewares.wrap(require("./create-discount").default) + ) route.get( "/:discount_id", + transformQuery(AdminGetDiscountParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), middlewares.wrap(require("./get-discount").default) ) route.get( "/code/:code", + transformQuery(AdminGetDiscountsDiscountCodeParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), middlewares.wrap(require("./get-discount-by-code").default) ) route.post( "/:discount_id", + transformQuery(AdminPostDiscountsDiscountParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), + transformBody(AdminPostDiscountsDiscountReq), middlewares.wrap(require("./update-discount").default) ) route.delete( @@ -33,6 +96,7 @@ export default (app) => { // Dynamic codes route.post( "/:discount_id/dynamic-codes", + transformBody(AdminPostDiscountsDiscountDynamicCodesReq), middlewares.wrap(require("./create-dynamic-code").default) ) route.delete( @@ -51,23 +115,64 @@ export default (app) => { ) // Discount condition management - route.get( - "/:discount_id/conditions/:condition_id", - middlewares.wrap(require("./get-condition").default) - ) - route.post( - "/:discount_id/conditions/:condition_id", - middlewares.wrap(require("./update-condition").default) - ) route.post( "/:discount_id/conditions", + transformQuery(AdminPostDiscountsDiscountConditionsParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), + transformBody(AdminPostDiscountsDiscountConditions), middlewares.wrap(require("./create-condition").default) ) + route.delete( "/:discount_id/conditions/:condition_id", + transformQuery(AdminDeleteDiscountsDiscountConditionsConditionParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), middlewares.wrap(require("./delete-condition").default) ) + const conditionRouter = Router({ mergeParams: true }) + route.use( + "/:discount_id/conditions/:condition_id", + doesConditionBelongToDiscount, + conditionRouter + ) + + conditionRouter.get( + "/", + transformQuery(AdminGetDiscountsDiscountConditionsConditionParams, { + defaultFields: defaultAdminDiscountConditionFields, + defaultRelations: defaultAdminDiscountConditionRelations, + isList: false, + }), + middlewares.wrap(require("./get-condition").default) + ) + conditionRouter.post( + "/", + transformQuery(AdminPostDiscountsDiscountConditionsConditionParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), + transformBody(AdminPostDiscountsDiscountConditionsCondition), + middlewares.wrap(require("./update-condition").default) + ) + conditionRouter.post( + "/batch", + transformQuery(AdminPostDiscountsDiscountConditionsConditionBatchParams, { + defaultFields: defaultAdminDiscountsFields, + defaultRelations: defaultAdminDiscountsRelations, + isList: false, + }), + transformBody(AdminPostDiscountsDiscountConditionsConditionBatchReq), + middlewares.wrap(require("./add-resources-to-condition-batch").default) + ) + return app } @@ -129,3 +234,4 @@ export * from "./list-discounts" export * from "./remove-region" export * from "./update-condition" export * from "./update-discount" +export * from "./add-resources-to-condition-batch" diff --git a/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts b/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts index 5aa78af038..8cd27195f3 100644 --- a/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts +++ b/packages/medusa/src/api/routes/admin/discounts/list-discounts.ts @@ -1,20 +1,15 @@ import { IsBoolean, - IsInt, IsOptional, IsString, ValidateNested, } from "class-validator" import { Transform, Type } from "class-transformer" -import _, { pickBy } from "lodash" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." import { AdminGetDiscountsDiscountRuleParams } from "../../../../types/discount" -import { Discount } from "../../../.." -import DiscountService from "../../../../services/discount" -import { FindConfig } from "../../../../types/common" -import { validator } from "../../../../utils/validator" -import { isDefined } from "../../../../utils" +import { extendedFindParamsMixin } from "../../../../types/common" +import { Request, Response } from "express" +import { DiscountService } from "../../../../services" /** * @oas [get] /discounts @@ -97,38 +92,29 @@ import { isDefined } from "../../../../utils" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { - const validated = await validator(AdminGetDiscountsParams, req.query) - +export default async (req: Request, res: Response) => { const discountService: DiscountService = req.scope.resolve("discountService") - const relations = - validated.expand?.split(",") ?? defaultAdminDiscountsRelations - - const listConfig: FindConfig = { - select: defaultAdminDiscountsFields, - relations, - skip: validated.offset, - take: validated.limit, - order: { created_at: "DESC" }, - } - - const filterableFields = _.omit(validated, ["limit", "offset", "expand"]) + const { filterableFields, listConfig } = req + const { skip, take } = listConfig const [discounts, count] = await discountService.listAndCount( - pickBy(filterableFields, (val) => isDefined(val)), + filterableFields, listConfig ) res.status(200).json({ discounts, count, - offset: validated.offset, - limit: validated.limit, + offset: skip, + limit: take, }) } -export class AdminGetDiscountsParams { +export class AdminGetDiscountsParams extends extendedFindParamsMixin({ + limit: 20, + offset: 0, +}) { @ValidateNested() @IsOptional() @Type(() => AdminGetDiscountsDiscountRuleParams) @@ -147,18 +133,4 @@ export class AdminGetDiscountsParams { @IsOptional() @Transform(({ value }) => value === "true") is_disabled?: boolean - - @IsInt() - @IsOptional() - @Type(() => Number) - limit = 20 - - @IsInt() - @IsOptional() - @Type(() => Number) - offset = 0 - - @IsString() - @IsOptional() - expand?: string } diff --git a/packages/medusa/src/api/routes/admin/discounts/update-condition.ts b/packages/medusa/src/api/routes/admin/discounts/update-condition.ts index 292b5e7b40..5cec5138a0 100644 --- a/packages/medusa/src/api/routes/admin/discounts/update-condition.ts +++ b/packages/medusa/src/api/routes/admin/discounts/update-condition.ts @@ -1,13 +1,9 @@ -import { IsOptional, IsString } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." - +import { Request, Response } from "express" import { AdminUpsertConditionsReq } from "../../../../types/discount" -import { Discount } from "../../../../models" import DiscountConditionService from "../../../../services/discount-condition" import { DiscountService } from "../../../../services" import { EntityManager } from "typeorm" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" /** * @oas [post] /discounts/{discount_id}/conditions/{condition_id} @@ -104,19 +100,9 @@ import { validator } from "../../../../utils/validator" * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { discount_id, condition_id } = req.params - const validatedCondition = await validator( - AdminPostDiscountsDiscountConditionsCondition, - req.body - ) - - const validatedParams = await validator( - AdminPostDiscountsDiscountConditionsConditionParams, - req.query - ) - const conditionService: DiscountConditionService = req.scope.resolve( "discountConditionService" ) @@ -128,7 +114,7 @@ export default async (req, res) => { let discount = await discountService.retrieve(discount_id) const updateObj = { - ...validatedCondition, + ...(req.validatedBody as AdminPostDiscountsDiscountConditionsCondition), rule_id: discount.rule_id, id: condition.id, } @@ -140,14 +126,7 @@ export default async (req, res) => { .upsertCondition(updateObj) }) - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validatedParams?.fields?.split(",") as (keyof Discount)[], - validatedParams?.expand?.split(",") - ) - - discount = await discountService.retrieve(discount.id, config) + discount = await discountService.retrieve(discount.id, req.retrieveConfig) res.status(200).json({ discount }) } @@ -155,12 +134,4 @@ export default async (req, res) => { // eslint-disable-next-line max-len export class AdminPostDiscountsDiscountConditionsCondition extends AdminUpsertConditionsReq {} -export class AdminPostDiscountsDiscountConditionsConditionParams { - @IsString() - @IsOptional() - expand?: string - - @IsString() - @IsOptional() - fields?: string -} +export class AdminPostDiscountsDiscountConditionsConditionParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/discounts/update-discount.ts b/packages/medusa/src/api/routes/admin/discounts/update-discount.ts index 0078ab7f44..b0fcde127b 100644 --- a/packages/medusa/src/api/routes/admin/discounts/update-discount.ts +++ b/packages/medusa/src/api/routes/admin/discounts/update-discount.ts @@ -1,4 +1,5 @@ -import { Discount, DiscountConditionOperator } from "../../../../models" +import { Request, Response } from "express" +import { AllocationType, DiscountConditionOperator } from "../../../../models" import { IsArray, IsBoolean, @@ -12,17 +13,14 @@ import { IsString, ValidateNested, } from "class-validator" -import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." import { AdminUpsertConditionsReq } from "../../../../types/discount" -import { AllocationType } from "../../../../models" import DiscountService from "../../../../services/discount" import { EntityManager } from "typeorm" import { IsGreaterThan } from "../../../../utils/validators/greater-than" import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration" import { Type } from "class-transformer" -import { getRetrieveConfig } from "../../../../utils/get-query-config" -import { validator } from "../../../../utils/validator" +import { FindParams } from "../../../../types/common" /** * @oas [post] /discounts/{id} @@ -176,34 +174,23 @@ import { validator } from "../../../../utils/validator" * "500": * $ref: "#/components/responses/500_error" */ -export default async (req, res) => { +export default async (req: Request, res: Response) => { const { discount_id } = req.params - const validated = await validator(AdminPostDiscountsDiscountReq, req.body) - - const validatedParams = await validator( - AdminPostDiscountsDiscountParams, - req.query - ) - const discountService: DiscountService = req.scope.resolve("discountService") const manager: EntityManager = req.scope.resolve("manager") await manager.transaction(async (transactionManager) => { return await discountService .withTransaction(transactionManager) - .update(discount_id, validated) + .update(discount_id, req.validatedBody as AdminPostDiscountsDiscountReq) }) - const config = getRetrieveConfig( - defaultAdminDiscountsFields, - defaultAdminDiscountsRelations, - validatedParams?.fields?.split(",") as (keyof Discount)[], - validatedParams?.expand?.split(",") + const discount = await discountService.retrieve( + discount_id, + req.retrieveConfig ) - const discount = await discountService.retrieve(discount_id, config) - res.status(200).json({ discount }) } @@ -287,12 +274,4 @@ export class AdminUpsertCondition extends AdminUpsertConditionsReq { operator: DiscountConditionOperator } -export class AdminPostDiscountsDiscountParams { - @IsString() - @IsOptional() - expand?: string - - @IsString() - @IsOptional() - fields?: string -} +export class AdminPostDiscountsDiscountParams extends FindParams {} diff --git a/packages/medusa/src/repositories/customer-group.ts b/packages/medusa/src/repositories/customer-group.ts index 95df1555e9..f9971a8536 100644 --- a/packages/medusa/src/repositories/customer-group.ts +++ b/packages/medusa/src/repositories/customer-group.ts @@ -7,7 +7,7 @@ import { SelectQueryBuilder, } from "typeorm" import { CustomerGroup } from "../models" -import { ExtendedFindConfig, Writable } from "../types/common" +import { ExtendedFindConfig, Selector } from "../types/common" import { getGroupedRelations, mergeEntitiesWithRelations, @@ -16,7 +16,7 @@ import { } from "../utils/repository" export type DefaultWithoutRelations = Omit< - ExtendedFindConfig>>, + ExtendedFindConfig>, "relations" > diff --git a/packages/medusa/src/repositories/discount-condition.ts b/packages/medusa/src/repositories/discount-condition.ts index caf4faa871..fe6a730678 100644 --- a/packages/medusa/src/repositories/discount-condition.ts +++ b/packages/medusa/src/repositories/discount-condition.ts @@ -17,6 +17,7 @@ import { DiscountConditionProduct } from "../models/discount-condition-product" import { DiscountConditionProductCollection } from "../models/discount-condition-product-collection" import { DiscountConditionProductTag } from "../models/discount-condition-product-tag" import { DiscountConditionProductType } from "../models/discount-condition-product-type" +import { isString } from "../utils" export enum DiscountConditionJoinTableForeignKey { PRODUCT_ID = "product_id", @@ -150,7 +151,7 @@ export class DiscountConditionRepository extends Repository { async addConditionResources( conditionId: string, - resourceIds: string[], + resourceIds: (string | { id: string })[], type: DiscountConditionType, overrideExisting = false ): Promise< @@ -171,7 +172,10 @@ export class DiscountConditionRepository extends Repository { return Promise.resolve([]) } - toInsert = resourceIds.map((rId) => ({ + const idsToInsert = resourceIds.map((rId): string => { + return isString(rId) ? rId : rId.id + }) + toInsert = idsToInsert.map((rId) => ({ condition_id: conditionId, [joinTableForeignKey]: rId, })) @@ -189,7 +193,7 @@ export class DiscountConditionRepository extends Repository { .from(conditionTable) .where({ condition_id: conditionId, - [joinTableForeignKey]: Not(In(resourceIds)), + [joinTableForeignKey]: Not(In(idsToInsert)), }) .execute() } diff --git a/packages/medusa/src/repositories/product-collection.ts b/packages/medusa/src/repositories/product-collection.ts index 2493500797..4311690a48 100644 --- a/packages/medusa/src/repositories/product-collection.ts +++ b/packages/medusa/src/repositories/product-collection.ts @@ -1,13 +1,13 @@ import { EntityRepository, Repository } from "typeorm" import { ProductCollection } from "../models" -import { ExtendedFindConfig } from "../types/common" +import { ExtendedFindConfig, Selector } from "../types/common" @EntityRepository(ProductCollection) // eslint-disable-next-line max-len export class ProductCollectionRepository extends Repository { async findAndCountByDiscountConditionId( conditionId: string, - query: ExtendedFindConfig> + query: ExtendedFindConfig> ): Promise<[ProductCollection[], number]> { const qb = this.createQueryBuilder("pc") diff --git a/packages/medusa/src/repositories/product-tag.ts b/packages/medusa/src/repositories/product-tag.ts index fb3211b295..97f616052a 100644 --- a/packages/medusa/src/repositories/product-tag.ts +++ b/packages/medusa/src/repositories/product-tag.ts @@ -1,6 +1,6 @@ import { EntityRepository, In, Repository } from "typeorm" import { ProductTag } from "../models/product-tag" -import { ExtendedFindConfig } from "../types/common" +import { ExtendedFindConfig, Selector } from "../types/common" type UpsertTagsInput = (Partial & { value: string @@ -67,7 +67,7 @@ export class ProductTagRepository extends Repository { async findAndCountByDiscountConditionId( conditionId: string, - query: ExtendedFindConfig> + query: ExtendedFindConfig> ) { const qb = this.createQueryBuilder("pt") diff --git a/packages/medusa/src/repositories/product-type.ts b/packages/medusa/src/repositories/product-type.ts index 14178b4710..4c036c1612 100644 --- a/packages/medusa/src/repositories/product-type.ts +++ b/packages/medusa/src/repositories/product-type.ts @@ -1,6 +1,6 @@ import { EntityRepository, Repository } from "typeorm" import { ProductType } from "../models/product-type" -import { ExtendedFindConfig } from "../types/common" +import { ExtendedFindConfig, Selector } from "../types/common" type UpsertTypeInput = Partial & { value: string @@ -29,7 +29,7 @@ export class ProductTypeRepository extends Repository { async findAndCountByDiscountConditionId( conditionId: string, - query: ExtendedFindConfig> + query: ExtendedFindConfig> ): Promise<[ProductType[], number]> { const qb = this.createQueryBuilder("pt") diff --git a/packages/medusa/src/services/__mocks__/discount.js b/packages/medusa/src/services/__mocks__/discount.js index 97effdd307..ec129b81c1 100644 --- a/packages/medusa/src/services/__mocks__/discount.js +++ b/packages/medusa/src/services/__mocks__/discount.js @@ -6,6 +6,7 @@ export const discounts = { code: "Something", is_dynamic: true, rule: { + id: IdMap.getId("dynamic_rule"), type: "percentage", allocation: "total", value: 10, @@ -16,6 +17,7 @@ export const discounts = { id: IdMap.getId("total10"), code: "10%OFF", rule: { + id: IdMap.getId("total10_rule"), type: "percentage", allocation: "total", value: 10, @@ -26,6 +28,7 @@ export const discounts = { id: IdMap.getId("item10Percent"), code: "MEDUSA", rule: { + id: IdMap.getId("item10Percent_rule"), type: "percentage", allocation: "item", value: 10, @@ -36,6 +39,7 @@ export const discounts = { id: IdMap.getId("total10Fixed"), code: "MEDUSA", rule: { + id: IdMap.getId("total10Fixed_rule"), type: "fixed", allocation: "total", value: 10, @@ -46,6 +50,7 @@ export const discounts = { id: IdMap.getId("item9Fixed"), code: "MEDUSA", rule: { + id: IdMap.getId("item9Fixed_rule"), type: "fixed", allocation: "item", value: 9, @@ -56,6 +61,7 @@ export const discounts = { id: IdMap.getId("item2Fixed"), code: "MEDUSA", rule: { + id: IdMap.getId("item2Fixed_rule"), type: "fixed", allocation: "item", value: 2, @@ -66,6 +72,7 @@ export const discounts = { id: IdMap.getId("item10FixedNoVariants"), code: "MEDUSA", rule: { + id: IdMap.getId("item10FixedNoVariants_rule"), type: "fixed", allocation: "item", value: 10, @@ -77,6 +84,7 @@ export const discounts = { code: "MEDUSA", ends_at: new Date("December 17, 1995 03:24:00"), rule: { + id: IdMap.getId("expired_rule"), type: "fixed", allocation: "item", value: 10, @@ -87,6 +95,7 @@ export const discounts = { id: IdMap.getId("freeshipping"), code: "FREESHIPPING", rule: { + id: IdMap.getId("freeshipping_rule"), type: "free_shipping", allocation: "total", value: 10, @@ -97,6 +106,7 @@ export const discounts = { id: IdMap.getId("us-discount"), code: "US10", rule: { + id: IdMap.getId("us-discount_rule"), type: "free_shipping", allocation: "total", value: 10, @@ -106,6 +116,7 @@ export const discounts = { alreadyExists: { code: "ALREADYEXISTS", rule: { + id: IdMap.getId("ALREADYEXISTS_rule"), type: "percentage", allocation: "total", value: 20, @@ -115,7 +126,7 @@ export const discounts = { } export const DiscountServiceMock = { - withTransaction: function() { + withTransaction: function () { return this }, create: jest.fn().mockImplementation((data) => { diff --git a/packages/medusa/src/services/customer-group.ts b/packages/medusa/src/services/customer-group.ts index c565ba7d94..38c6e621c2 100644 --- a/packages/medusa/src/services/customer-group.ts +++ b/packages/medusa/src/services/customer-group.ts @@ -6,7 +6,7 @@ import { CustomerGroupRepository, FindWithoutRelationsOptions, } from "../repositories/customer-group" -import { FindConfig } from "../types/common" +import { FindConfig, Selector } from "../types/common" import { CustomerGroupUpdate } from "../types/customer-groups" import { buildQuery, @@ -196,7 +196,7 @@ class CustomerGroupService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, @@ -214,7 +214,7 @@ class CustomerGroupService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, diff --git a/packages/medusa/src/services/discount-condition.ts b/packages/medusa/src/services/discount-condition.ts index 0b0405e117..17d9e9b30d 100644 --- a/packages/medusa/src/services/discount-condition.ts +++ b/packages/medusa/src/services/discount-condition.ts @@ -72,7 +72,7 @@ class DiscountConditionService extends TransactionBaseService { protected static resolveConditionType_(data: UpsertDiscountConditionInput): | { type: DiscountConditionType - resource_ids: string[] + resource_ids: (string | { id: string })[] } | undefined { switch (true) { @@ -107,7 +107,8 @@ class DiscountConditionService extends TransactionBaseService { } async upsertCondition( - data: UpsertDiscountConditionInput + data: UpsertDiscountConditionInput, + overrideExisting: boolean = true ): Promise< ( | DiscountConditionProduct @@ -146,7 +147,7 @@ class DiscountConditionService extends TransactionBaseService { data.id, resolvedConditionType.resource_ids, resolvedConditionType.type, - true + overrideExisting ) } diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index 8f713289d5..87d7093580 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -255,7 +255,7 @@ class DiscountService extends TransactionBaseService { if (!discount) { throw new MedusaError( MedusaError.Types.NOT_FOUND, - `Discount with ${discountId} was not found` + `Discount with id ${discountId} was not found` ) } diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index 8278ecb849..6e196954ac 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -8,6 +8,7 @@ export { default as CustomShippingOptionService } from "./custom-shipping-option export { default as CustomerGroupService } from "./customer-group" export { default as CustomerService } from "./customer" export { default as DiscountService } from "./discount" +export { default as DiscountConditionService } from "./discount-condition" export { default as DraftOrderService } from "./draft-order" export { default as EventBusService } from "./event-bus" export { default as FulfillmentProviderService } from "./fulfillment-provider" diff --git a/packages/medusa/src/services/product-collection.ts b/packages/medusa/src/services/product-collection.ts index 081f5d3e72..6935179a6e 100644 --- a/packages/medusa/src/services/product-collection.ts +++ b/packages/medusa/src/services/product-collection.ts @@ -4,7 +4,7 @@ import { TransactionBaseService } from "../interfaces" import { ProductCollection } from "../models" import { ProductRepository } from "../repositories/product" import { ProductCollectionRepository } from "../repositories/product-collection" -import { FindConfig } from "../types/common" +import { FindConfig, Selector } from "../types/common" import { CreateProductCollection, UpdateProductCollection, @@ -219,7 +219,7 @@ class ProductCollectionService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, @@ -236,7 +236,7 @@ class ProductCollectionService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, diff --git a/packages/medusa/src/services/product-tag.ts b/packages/medusa/src/services/product-tag.ts index 859bf39dc4..89b1fcfbb4 100644 --- a/packages/medusa/src/services/product-tag.ts +++ b/packages/medusa/src/services/product-tag.ts @@ -2,7 +2,7 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, ILike } from "typeorm" import { ProductTag } from "../models" import { ProductTagRepository } from "../repositories/product-tag" -import { FindConfig } from "../types/common" +import { FindConfig, Selector } from "../types/common" import { TransactionBaseService } from "../interfaces" import { buildQuery, isString } from "../utils" @@ -69,7 +69,7 @@ class ProductTagService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, @@ -86,7 +86,7 @@ class ProductTagService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, diff --git a/packages/medusa/src/services/product-type.ts b/packages/medusa/src/services/product-type.ts index 7137cdf754..10be231648 100644 --- a/packages/medusa/src/services/product-type.ts +++ b/packages/medusa/src/services/product-type.ts @@ -2,7 +2,7 @@ import { MedusaError } from "medusa-core-utils" import { EntityManager, ILike } from "typeorm" import { ProductType } from "../models" import { ProductTypeRepository } from "../repositories/product-type" -import { FindConfig } from "../types/common" +import { FindConfig, Selector } from "../types/common" import { TransactionBaseService } from "../interfaces" import { buildQuery, isString } from "../utils" @@ -53,7 +53,7 @@ class ProductTypeService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, @@ -70,7 +70,7 @@ class ProductTypeService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: Partial & { + selector: Selector & { q?: string discount_condition_id?: string } = {}, diff --git a/packages/medusa/src/types/discount.ts b/packages/medusa/src/types/discount.ts index 94e28065f4..d2d2648014 100644 --- a/packages/medusa/src/types/discount.ts +++ b/packages/medusa/src/types/discount.ts @@ -8,10 +8,14 @@ import { Validate, ValidateNested, } from "class-validator" -import { DiscountConditionOperator } from "../models/discount-condition" -import { AllocationType, DiscountRuleType } from "../models/discount-rule" +import { + AllocationType, + DiscountConditionOperator, + DiscountConditionType, + DiscountRuleType, + Region, +} from "../models" import { ExactlyOne } from "./validators/exactly-one" -import { Region } from "../models" export type QuerySelector = { q?: string @@ -105,15 +109,23 @@ export class AdminUpsertConditionsReq { customer_groups?: string[] } +export const DiscountConditionMapTypeToProperty = { + [DiscountConditionType.PRODUCTS]: "products", + [DiscountConditionType.PRODUCT_TYPES]: "product_types", + [DiscountConditionType.PRODUCT_COLLECTIONS]: "product_collections", + [DiscountConditionType.PRODUCT_TAGS]: "product_tags", + [DiscountConditionType.CUSTOMER_GROUPS]: "customer_groups", +} + export type UpsertDiscountConditionInput = { rule_id?: string id?: string operator?: DiscountConditionOperator - products?: string[] - product_collections?: string[] - product_types?: string[] - product_tags?: string[] - customer_groups?: string[] + products?: (string | { id: string })[] + product_collections?: (string | { id: string })[] + product_types?: (string | { id: string })[] + product_tags?: (string | { id: string })[] + customer_groups?: (string | { id: string })[] } export type CreateDiscountRuleInput = {