From 9ca45ea492e755a88737322f900d60abdfa64024 Mon Sep 17 00:00:00 2001 From: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Fri, 13 May 2022 12:42:23 +0200 Subject: [PATCH] feat(medusa): Add endpoints specific to DiscountConditions (#1355) --- .../admin/__snapshots__/discount.js.snap | 125 ++++ .../api/__tests__/admin/discount.js | 544 ++++++++++++++++-- .../simple-discount-condition-factory.ts | 11 +- .../src/resources/admin/discounts.ts | 56 +- .../medusa-react/mocks/data/fixtures.json | 9 +- packages/medusa-react/mocks/handlers/admin.ts | 37 ++ .../src/hooks/admin/discounts/mutations.ts | 52 +- .../hooks/admin/discounts/mutations.test.ts | 69 +++ .../admin/discounts/create-condition.ts | 107 ++++ .../routes/admin/discounts/create-discount.ts | 2 - .../admin/discounts/delete-condition.ts | 109 ++++ .../src/api/routes/admin/discounts/index.ts | 17 + .../admin/discounts/update-condition.ts | 109 ++++ .../medusa/src/models/discount-condition.ts | 4 + .../src/repositories/discount-condition.ts | 16 + packages/medusa/src/repositories/order.ts | 2 +- .../medusa/src/services/discount-condition.ts | 179 ++++++ packages/medusa/src/services/discount.ts | 128 +---- packages/medusa/src/services/product.js | 15 +- packages/medusa/src/types/discount.ts | 1 + 20 files changed, 1433 insertions(+), 159 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/discounts/create-condition.ts create mode 100644 packages/medusa/src/api/routes/admin/discounts/delete-condition.ts create mode 100644 packages/medusa/src/api/routes/admin/discounts/update-condition.ts create mode 100644 packages/medusa/src/services/discount-condition.ts diff --git a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap index a3a4b59fcd..3299580946 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/discount.js.snap @@ -14,3 +14,128 @@ Object { "type": "duplicate_error", } `; + +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", + "created_at": Any, + "deleted_at": null, + "ends_at": null, + "id": "test-discount", + "is_disabled": false, + "is_dynamic": false, + "metadata": null, + "parent_discount": null, + "parent_discount_id": null, + "regions": Array [], + "rule": Object { + "allocation": "total", + "conditions": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "discount_rule_id": Any, + "id": Any, + "metadata": null, + "operator": "in", + "type": "products", + "updated_at": Any, + }, + ], + "created_at": Any, + "deleted_at": null, + "description": null, + "id": Any, + "metadata": null, + "type": "percentage", + "updated_at": Any, + "value": 10, + }, + "rule_id": Any, + "starts_at": Any, + "updated_at": Any, + "usage_count": 0, + "usage_limit": null, + "valid_duration": null, +} +`; + +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", + "created_at": Any, + "deleted_at": null, + "ends_at": null, + "id": "test-discount", + "is_disabled": false, + "is_dynamic": false, + "metadata": null, + "parent_discount_id": null, + "rule": Object { + "allocation": "total", + "conditions": Array [ + Object { + "created_at": Any, + "deleted_at": null, + "discount_rule_id": Any, + "id": Any, + "metadata": null, + "operator": "in", + "products": Array [ + Object { + "collection_id": null, + "created_at": Any, + "deleted_at": null, + "description": null, + "discountable": true, + "external_id": null, + "handle": null, + "height": null, + "hs_code": null, + "id": "test-product", + "is_giftcard": false, + "length": null, + "material": null, + "metadata": null, + "mid_code": null, + "origin_country": null, + "profile_id": Any, + "status": "draft", + "subtitle": null, + "thumbnail": null, + "title": "Practical Frozen Fish", + "type_id": Any, + "updated_at": Any, + "weight": null, + "width": null, + }, + ], + "type": "products", + "updated_at": Any, + }, + ], + "created_at": Any, + "deleted_at": null, + "description": null, + "id": Any, + "metadata": null, + "type": "percentage", + "updated_at": Any, + "value": 10, + }, + "rule_id": Any, + "starts_at": Any, + "updated_at": Any, + "usage_count": 0, + "usage_limit": null, + "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 a9475647a0..3957b122db 100644 --- a/integration-tests/api/__tests__/admin/discount.js +++ b/integration-tests/api/__tests__/admin/discount.js @@ -1,6 +1,5 @@ const path = require("path") const { - Region, DiscountRule, Discount, Customer, @@ -12,7 +11,6 @@ const { useApi } = require("../../../helpers/use-api") const { initDb, useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") const discountSeeder = require("../../helpers/discount-seeder") -const { exportAllDeclaration } = require("@babel/types") const { simpleProductFactory } = require("../../factories") const { simpleDiscountFactory, @@ -433,7 +431,6 @@ describe("/admin/discounts", () => { await adminSeeder(dbConnection) await discountSeeder(dbConnection) } catch (err) { - console.log(err) throw err } }) @@ -1468,41 +1465,37 @@ describe("/admin/discounts", () => { describe("POST /admin/discounts/:discount_id/dynamic-codes", () => { beforeEach(async () => { const manager = dbConnection.manager - try { - await adminSeeder(dbConnection) - await manager.insert(DiscountRule, { - id: "test-discount-rule", - description: "Dynamic rule", - type: "percentage", - value: 10, - allocation: "total", - }) - await manager.insert(Discount, { - id: "test-discount", - code: "DYNAMIC", - is_dynamic: true, - is_disabled: false, - rule_id: "test-discount-rule", - valid_duration: "P2Y", - }) - await manager.insert(DiscountRule, { - id: "test-discount-rule1", - description: "Dynamic rule", - type: "percentage", - value: 10, - allocation: "total", - }) - await manager.insert(Discount, { - id: "test-discount1", - code: "DYNAMICCode", - is_dynamic: true, - is_disabled: false, - rule_id: "test-discount-rule1", - }) - } catch (err) { - console.log(err) - throw err - } + + await adminSeeder(dbConnection) + await manager.insert(DiscountRule, { + id: "test-discount-rule", + description: "Dynamic rule", + type: "percentage", + value: 10, + allocation: "total", + }) + await manager.insert(Discount, { + id: "test-discount", + code: "DYNAMIC", + is_dynamic: true, + is_disabled: false, + rule_id: "test-discount-rule", + valid_duration: "P2Y", + }) + await manager.insert(DiscountRule, { + id: "test-discount-rule1", + description: "Dynamic rule", + type: "percentage", + value: 10, + allocation: "total", + }) + await manager.insert(Discount, { + id: "test-discount1", + code: "DYNAMICCode", + is_dynamic: true, + is_disabled: false, + rule_id: "test-discount-rule1", + }) }) afterEach(async () => { @@ -1554,7 +1547,7 @@ describe("/admin/discounts", () => { } ) .catch((err) => { - // console.log(err) + console.log(err) }) expect(response.status).toEqual(200) @@ -1566,4 +1559,477 @@ describe("/admin/discounts", () => { ) }) }) + + describe("DELETE /admin/discounts/:id/conditions/:condition_id", () => { + beforeEach(async () => { + const manager = dbConnection.manager + await adminSeeder(dbConnection) + + await manager.insert(DiscountRule, { + id: "test-discount-rule-fixed", + description: "Test discount rule", + type: "fixed", + value: 10, + allocation: "total", + }) + + const prod = await simpleProductFactory(dbConnection, { type: "pants" }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount", + code: "TEST", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + type: "products", + operator: "in", + products: [prod.id], + }, + ], + }, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete condition", async () => { + const api = useApi() + + const group = await dbConnection.manager.insert(CustomerGroup, { + id: "customer-group-1", + name: "vip-customers", + }) + + await dbConnection.manager.insert(Customer, { + id: "cus_1234", + email: "oli@email.com", + groups: [group], + }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount", + code: "TEST", + rule: { + type: "percentage", + value: "10", + allocation: "total", + conditions: [ + { + id: "test-condition", + type: "customer_groups", + operator: "in", + customer_groups: ["customer-group-1"], + }, + ], + }, + }) + + const response = await api + .delete("/admin/discounts/test-discount/conditions/test-condition", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + const disc = response.data + + expect(response.status).toEqual(200) + expect(disc).toEqual( + expect.objectContaining({ + id: "test-condition", + deleted: true, + object: "discount-condition", + }) + ) + }) + + 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 disc = response.data + + expect(response.status).toEqual(200) + expect(disc).toEqual( + expect.objectContaining({ + id: "test-condition", + deleted: true, + object: "discount-condition", + }) + ) + }) + + it("should fail if discount does not exist", async () => { + const api = useApi() + + try { + await api.delete( + "/admin/discounts/not-exist/conditions/test-condition", + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "Discount with id not-exist was not found" + ) + } + }) + }) + + describe("POST /admin/discounts/:id/conditions", () => { + beforeEach(async () => { + const manager = dbConnection.manager + await adminSeeder(dbConnection) + + await manager.insert(DiscountRule, { + id: "test-discount-rule-fixed", + description: "Test discount rule", + type: "fixed", + value: 10, + allocation: "total", + }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount", + code: "TEST", + rule: { + type: "percentage", + value: "10", + allocation: "total", + }, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a condition", async () => { + const api = useApi() + + 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 disc = response.data.discount + + expect(response.status).toEqual(200) + expect(disc).toMatchSnapshot({ + id: "test-discount", + code: "TEST", + created_at: expect.any(String), + updated_at: expect.any(String), + rule_id: expect.any(String), + starts_at: expect.any(String), + rule: { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + conditions: [ + { + id: expect.any(String), + discount_rule_id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ], + }, + }) + }) + + it("fails if more than one condition type is provided", async () => { + const api = useApi() + + const prod = await simpleProductFactory(dbConnection, { type: "pants" }) + + const group = await dbConnection.manager.insert(CustomerGroup, { + id: "customer-group-1", + name: "vip-customers", + }) + + await dbConnection.manager.insert(Customer, { + id: "cus_1234", + email: "oli@email.com", + groups: [group], + }) + + try { + await api.post( + "/admin/discounts/test-discount/conditions", + { + operator: "in", + products: [prod.id], + customer_groups: ["customer-group-1"], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "Only one of products, customer_groups is allowed, Only one of customer_groups, products is allowed" + ) + } + }) + + it("throws if discount does not exist", async () => { + expect.assertions(1) + + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) + + try { + await api.post( + "/admin/discounts/does-not-exist/conditions/test-condition", + { + products: [prod2.id], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "Discount with id does-not-exist was not found" + ) + } + }) + }) + + describe("POST /admin/discounts/:id/conditions/:condition_id", () => { + beforeEach(async () => { + const manager = dbConnection.manager + await adminSeeder(dbConnection) + + await manager.insert(DiscountRule, { + id: "test-discount-rule-fixed", + description: "Test discount rule", + type: "fixed", + value: 10, + allocation: "total", + }) + + const prod = 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: [prod.id], + }, + ], + }, + }) + + await simpleDiscountFactory(dbConnection, { + id: "test-discount-2", + code: "TEST2", + rule: { + type: "percentage", + value: "10", + allocation: "total", + }, + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update a condition", async () => { + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { + id: "test-product", + type: "pants", + }) + + const discount = await api + .get("/admin/discounts/test-discount", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + 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 disc = response.data.discount + + expect(response.status).toEqual(200) + expect(disc).toMatchSnapshot({ + id: "test-discount", + code: "TEST", + created_at: expect.any(String), + updated_at: expect.any(String), + rule_id: expect.any(String), + starts_at: expect.any(String), + rule: { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + conditions: [ + { + id: expect.any(String), + discount_rule_id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + products: [ + { + created_at: expect.any(String), + updated_at: expect.any(String), + profile_id: expect.any(String), + type_id: expect.any(String), + id: "test-product", + }, + ], + }, + ], + }, + }) + }) + + it("throws if condition does not exist", async () => { + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) + + try { + await api.post( + "/admin/discounts/test-discount/conditions/does-not-exist", + { + products: [prod2.id], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "DiscountCondition with id does-not-exist was not found for Discount test-discount" + ) + } + }) + + it("throws if discount does not exist", async () => { + expect.assertions(1) + + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) + + try { + await api.post( + "/admin/discounts/does-not-exist/conditions/test-condition", + { + products: [prod2.id], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "Discount with id does-not-exist was not found" + ) + } + }) + + it("throws if condition does not belong to discount", async () => { + const api = useApi() + + const prod2 = await simpleProductFactory(dbConnection, { type: "pants" }) + + try { + await api.post( + "/admin/discounts/test-discount-2/conditions/test-condition", + { + products: [prod2.id], + }, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + } catch (error) { + expect(error.message).toMatchSnapshot( + "DiscountCondition with id test-condition was not found for Discount test-discount-2" + ) + } + }) + }) }) diff --git a/integration-tests/api/factories/simple-discount-condition-factory.ts b/integration-tests/api/factories/simple-discount-condition-factory.ts index ad3582cc66..9735f8a2b3 100644 --- a/integration-tests/api/factories/simple-discount-condition-factory.ts +++ b/integration-tests/api/factories/simple-discount-condition-factory.ts @@ -13,6 +13,7 @@ import faker from "faker" import { Connection } from "typeorm" export type DiscuntConditionFactoryData = { + id?: string rule_id: string type: DiscountConditionType operator: DiscountConditionOperator @@ -93,11 +94,17 @@ export const simpleDiscountConditionFactory = async ( resources = data.customer_groups } - const condToSave = manager.create(DiscountCondition, { + const toCreate = { type: data.type, operator: data.operator, discount_rule_id: data.rule_id, - }) + } + + if (data.id) { + toCreate["id"] = data.id + } + + const condToSave = manager.create(DiscountCondition, toCreate) const { conditionTable, resourceKey } = getJoinTableResourceIdentifiers( data.type diff --git a/packages/medusa-js/src/resources/admin/discounts.ts b/packages/medusa-js/src/resources/admin/discounts.ts index b0048bc92e..95cba9fa89 100644 --- a/packages/medusa-js/src/resources/admin/discounts.ts +++ b/packages/medusa-js/src/resources/admin/discounts.ts @@ -2,8 +2,11 @@ import { AdminDiscountsDeleteRes, AdminDiscountsListRes, AdminDiscountsRes, - AdminGetDiscountParams, AdminGetDiscountsParams, + AdminPostDiscountsDiscountConditions, + AdminPostDiscountsDiscountConditionsCondition, + AdminPostDiscountsDiscountConditionsConditionParams, + AdminPostDiscountsDiscountConditionsParams, AdminPostDiscountsDiscountDynamicCodesReq, AdminPostDiscountsDiscountReq, AdminPostDiscountsReq, @@ -133,6 +136,57 @@ class AdminDiscountsResource extends BaseResource { const path = `/admin/discounts/${id}/regions/${regionId}` return this.client.request("DELETE", path, {}, {}, customHeaders) } + + /** + * @description creates a discount condition + */ + createCondition( + discountId: string, + payload: AdminPostDiscountsDiscountConditions, + query: AdminPostDiscountsDiscountConditionsParams = {}, + customHeaders: Record = {} + ): ResponsePromise { + let path = `/admin/discounts/${discountId}/conditions` + + if (query) { + const queryString = qs.stringify(query) + path = `/admin/discounts/${discountId}/conditions?${queryString}` + } + + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** + * @description Updates a discount condition + */ + updateCondition( + discountId: string, + conditionId: string, + payload: AdminPostDiscountsDiscountConditionsCondition, + query: AdminPostDiscountsDiscountConditionsConditionParams = {}, + customHeaders: Record = {} + ): ResponsePromise { + let path = `/admin/discounts/${discountId}/conditions/${conditionId}` + + if (query) { + const queryString = qs.stringify(query) + path = `/admin/discounts/${discountId}/conditions/${conditionId}?${queryString}` + } + + return this.client.request("POST", path, payload, {}, customHeaders) + } + + /** + * @description Removes a condition from a discount + */ + deleteCondition( + discountId: string, + conditionId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/discounts/${discountId}/conditions/${conditionId}` + return this.client.request("DELETE", path, {}, {}, customHeaders) + } } export default AdminDiscountsResource diff --git a/packages/medusa-react/mocks/data/fixtures.json b/packages/medusa-react/mocks/data/fixtures.json index 5223dacbca..a1afaea812 100644 --- a/packages/medusa-react/mocks/data/fixtures.json +++ b/packages/medusa-react/mocks/data/fixtures.json @@ -478,7 +478,14 @@ "created_at": "2021-03-16T21:24:16.872Z", "updated_at": "2021-03-16T21:24:16.872Z", "deleted_at": null, - "metadata": null + "metadata": null, + "conditions": [ + { + "id": "cnd_01F0YESMVJXKX8XZ6Q9Z6Q6Z6", + "type": "products", + "operator": "in" + } + ] }, "is_disabled": false, "parent_discount_id": null, diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 271012c0f7..3a3238eeff 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -762,6 +762,43 @@ export const adminHandlers = [ ) }), + rest.post("/admin/discounts/:id/conditions", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + discount: { + ...fixtures.get("discount"), + }, + }) + ) + }), + + rest.post("/admin/discounts/:id/conditions/:conditionId", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + discount: { + ...fixtures.get("discount"), + }, + }) + ) + }), + + rest.delete( + "/admin/discounts/:id/conditions/:conditionId", + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + id: req.params.conditionId, + object: "discount-condition", + deleted: true, + discount: fixtures.get("discount"), + }) + ) + } + ), + 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 05a313eb2d..834f289a39 100644 --- a/packages/medusa-react/src/hooks/admin/discounts/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/discounts/mutations.ts @@ -1,9 +1,11 @@ import { AdminDiscountsDeleteRes, AdminDiscountsRes, + AdminPostDiscountsDiscountConditions, + AdminPostDiscountsDiscountConditionsCondition, AdminPostDiscountsDiscountDynamicCodesReq, AdminPostDiscountsDiscountReq, - AdminPostDiscountsReq, + AdminPostDiscountsReq } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useMutation, UseMutationOptions, useQueryClient } from "react-query" @@ -115,3 +117,51 @@ export const useAdminDeleteDynamicDiscountCode = ( ) ) } + +export const useAdminDiscountCreateCondition = ( + discountId: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostDiscountsDiscountConditions + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostDiscountsDiscountConditions) => + client.admin.discounts.createCondition(discountId, payload), + buildOptions(queryClient, adminDiscountKeys.detail(discountId), options) + ) +} + +export const useAdminDiscountUpdateCondition = ( + discountId: string, + conditionId: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostDiscountsDiscountConditionsCondition + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostDiscountsDiscountConditionsCondition) => + client.admin.discounts.updateCondition(discountId, conditionId, payload), + buildOptions(queryClient, adminDiscountKeys.detail(discountId), options) + ) +} + +export const useAdminDiscountRemoveCondition = ( + discountId: string, + options?: UseMutationOptions, Error, string> +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (conditionId: string) => + client.admin.discounts.deleteCondition(discountId, conditionId), + buildOptions(queryClient, adminDiscountKeys.detail(discountId), options) + ) +} 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 3f6531f256..93fef23f70 100644 --- a/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/discounts/mutations.test.ts @@ -1,3 +1,4 @@ +import { DiscountConditionOperator } from "@medusajs/medusa" import { renderHook } from "@testing-library/react-hooks" import { fixtures } from "../../../../mocks/data" import { @@ -6,7 +7,10 @@ import { useAdminDeleteDiscount, useAdminDeleteDynamicDiscountCode, useAdminDiscountAddRegion, + useAdminDiscountCreateCondition, + useAdminDiscountRemoveCondition, useAdminDiscountRemoveRegion, + useAdminDiscountUpdateCondition, useAdminUpdateDiscount, } from "../../../../src/" import { createWrapper } from "../../../utils" @@ -184,3 +188,68 @@ describe("useAdminDeleteDynamicDiscountCode hook", () => { ) }) }) + +describe("useAdminDiscountCreateCondition hook", () => { + test("Creates a condition from a discount", async () => { + const id = fixtures.get("discount").id + const conditionId = fixtures.get("discount").rule.conditions[0].id + + const { result, waitFor } = renderHook( + () => useAdminDiscountCreateCondition(id), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + operator: DiscountConditionOperator.IN, + products: ["test-product"], + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.discount).toEqual(fixtures.get("discount")) + }) +}) +describe("useAdminDiscountUpdateCondition hook", () => { + test("Updates condition for discount", async () => { + const id = fixtures.get("discount").id + const conditionId = fixtures.get("discount").rule.conditions[0].id + + const { result, waitFor } = renderHook( + () => useAdminDiscountUpdateCondition(id, conditionId), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ products: ["test-product"] }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.discount).toEqual(fixtures.get("discount")) + }) +}) + +describe("useAdminDiscountRemoveCondition hook", () => { + test("removes a condition from a discount", async () => { + const id = fixtures.get("discount").id + const conditionId = fixtures.get("discount").rule.conditions[0].id + + const { result, waitFor } = renderHook( + () => useAdminDiscountRemoveCondition(id), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(conditionId) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.id).toEqual(conditionId) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/discounts/create-condition.ts b/packages/medusa/src/api/routes/admin/discounts/create-condition.ts new file mode 100644 index 0000000000..50ffb52e57 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/create-condition.ts @@ -0,0 +1,107 @@ +import { IsOptional, IsString } from "class-validator" +import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." +import { Discount } from "../../../../models/discount" +import { DiscountConditionOperator } from "../../../../models/discount-condition" +import { DiscountService } from "../../../../services" +import DiscountConditionService from "../../../../services/discount-condition" +import { AdminUpsertConditionsReq } from "../../../../types/discount" +import { getRetrieveConfig } from "../../../../utils/get-query-config" +import { validator } from "../../../../utils/validator" +/** + * @oas [post] /discounts/{discount_id}/conditions + * operationId: "PostDiscountsDiscountConditions" + * summary: "Creates a DiscountCondition" + * x-authenticated: true + * parameters: + * - (path) discount_id=* {string} The id of the Product. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result. + * - (query) fields {string} (Comma separated) Which fields should be included in each product of the result. + * description: "Creates a DiscountCondition" + * requestBody: + * content: + * application/json: + * schema: + * properties: + * operator: + * description: Operator of the condition + * type: string + * items: + * properties: + * products: + * type: array + * description: list of products + * product_types: + * type: array + * description: list of product types + * product_collections: + * type: array + * description: list of product collections + * product_tags: + * type: array + * description: list of product tags + * customer_groups: + * type: array + * description: list of customer_groups + * tags: + * - Discount + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * discount: + * $ref: "#/components/schemas/discount" + */ +export default async (req, res) => { + 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" + ) + const discountService: DiscountService = req.scope.resolve("discountService") + + let discount = await discountService.retrieve(discount_id) + + await conditionService.upsertCondition({ + ...validatedCondition, + 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) + + res.status(200).json({ discount }) +} + +export class AdminPostDiscountsDiscountConditions extends AdminUpsertConditionsReq { + @IsString() + operator: DiscountConditionOperator +} + +export class AdminPostDiscountsDiscountConditionsParams { + @IsString() + @IsOptional() + expand?: string + + @IsString() + @IsOptional() + fields?: string +} 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 d3019a54b9..9adaf530c0 100644 --- a/packages/medusa/src/api/routes/admin/discounts/create-discount.ts +++ b/packages/medusa/src/api/routes/admin/discounts/create-discount.ts @@ -83,8 +83,6 @@ import { AdminPostDiscountsDiscountParams } from "./update-discount" export default async (req, res) => { const validated = await validator(AdminPostDiscountsReq, req.body) - console.log(validated.rule.conditions) - const validatedParams = await validator( AdminPostDiscountsDiscountParams, req.query diff --git a/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts b/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts new file mode 100644 index 0000000000..7cfd46a7b1 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/delete-condition.ts @@ -0,0 +1,109 @@ +import { IsOptional, IsString } from "class-validator" +import { MedusaError } from "medusa-core-utils" +import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." +import { Discount } from "../../../../models" +import { DiscountService } from "../../../../services" +import DiscountConditionService from "../../../../services/discount-condition" +import { getRetrieveConfig } from "../../../../utils/get-query-config" +import { validator } from "../../../../utils/validator" + +/** + * @oas [delete] /discounts/{discount_id}/conditions/{condition_id} + * operationId: "DeleteDiscountsDiscountConditionsCondition" + * summary: "Delete a DiscountCondition" + * description: "Deletes a DiscountCondition" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The id of the Discount + * - (path) condition_id=* {string} The id of the DiscountCondition + * tags: + * - Discount + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * id: + * type: string + * description: The id of the deleted DiscountCondition + * object: + * type: string + * description: The type of the object that was deleted. + * deleted: + * type: boolean + * discount: + * type: object + * description: The Discount to which the condition used to belong + */ +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 condition = await conditionService + .retrieve(condition_id) + .catch(() => void 0) + + if (!condition) { + // resolves idempotently in case of non-existing condition + return res.json({ + id: condition_id, + object: "discount-condition", + deleted: true, + }) + } + + const discountService: DiscountService = req.scope.resolve("discountService") + + let 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}` + ) + } + + await conditionService.delete(condition_id) + + const config = getRetrieveConfig( + defaultAdminDiscountsFields, + defaultAdminDiscountsRelations, + validatedParams?.fields?.split(",") as (keyof Discount)[], + validatedParams?.expand?.split(",") + ) + + discount = await discountService.retrieve(discount_id, config) + + res.json({ + id: condition_id, + object: "discount-condition", + deleted: true, + discount, + }) +} + +export class AdminDeleteDiscountsDiscountConditionsConditionParams { + @IsString() + @IsOptional() + expand?: string + + @IsString() + @IsOptional() + fields?: string +} diff --git a/packages/medusa/src/api/routes/admin/discounts/index.ts b/packages/medusa/src/api/routes/admin/discounts/index.ts index 0784a1e4c0..a6f3ea5a7e 100644 --- a/packages/medusa/src/api/routes/admin/discounts/index.ts +++ b/packages/medusa/src/api/routes/admin/discounts/index.ts @@ -49,6 +49,20 @@ export default (app) => { middlewares.wrap(require("./remove-region").default) ) + // Discount condition management + route.post( + "/:discount_id/conditions/:condition_id", + middlewares.wrap(require("./update-condition").default) + ) + route.post( + "/:discount_id/conditions", + middlewares.wrap(require("./create-condition").default) + ) + route.delete( + "/:discount_id/conditions/:condition_id", + middlewares.wrap(require("./delete-condition").default) + ) + return app } @@ -88,12 +102,15 @@ export type AdminDiscountsListRes = PaginatedResponse & { } export * from "./add-region" +export * from "./create-condition" export * from "./create-discount" export * from "./create-dynamic-code" +export * from "./delete-condition" export * from "./delete-discount" export * from "./delete-dynamic-code" export * from "./get-discount" export * from "./get-discount-by-code" export * from "./list-discounts" export * from "./remove-region" +export * from "./update-condition" export * from "./update-discount" diff --git a/packages/medusa/src/api/routes/admin/discounts/update-condition.ts b/packages/medusa/src/api/routes/admin/discounts/update-condition.ts new file mode 100644 index 0000000000..22daf2d1ed --- /dev/null +++ b/packages/medusa/src/api/routes/admin/discounts/update-condition.ts @@ -0,0 +1,109 @@ +import { IsOptional, IsString } from "class-validator" +import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "." +import { Discount } from "../../../../models/discount" +import { DiscountService } from "../../../../services" +import DiscountConditionService from "../../../../services/discount-condition" +import { AdminUpsertConditionsReq } from "../../../../types/discount" +import { getRetrieveConfig } from "../../../../utils/get-query-config" +import { validator } from "../../../../utils/validator" +/** + * @oas [post] /discounts/{discount_id}/conditions/{condition_id} + * operationId: "PostDiscountsDiscountConditionsCondition" + * summary: "Updates a DiscountCondition" + * x-authenticated: true + * parameters: + * - (path) discount_id=* {string} The id of the Product. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each product of the result. + * - (query) fields {string} (Comma separated) Which fields should be included in each product of the result. + * description: "Updates a DiscountCondition" + * requestBody: + * content: + * application/json: + * required: + * - id + * schema: + * properties: + * items: + * properties: + * products: + * type: array + * description: list of products + * product_types: + * type: array + * description: list of product types + * product_collections: + * type: array + * description: list of product collections + * product_tags: + * type: array + * description: list of product tags + * customer_groups: + * type: array + * description: list of customer_groups + * tags: + * - Discount + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * discount: + * $ref: "#/components/schemas/discount" + */ + +export default async (req, res) => { + 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" + ) + + const condition = await conditionService.retrieve(condition_id) + + const discountService: DiscountService = req.scope.resolve("discountService") + + let discount = await discountService.retrieve(discount_id) + + const updateObj = { + ...validatedCondition, + rule_id: discount.rule_id, + id: condition.id, + } + + await conditionService.upsertCondition(updateObj) + + const config = getRetrieveConfig( + defaultAdminDiscountsFields, + defaultAdminDiscountsRelations, + validatedParams?.fields?.split(",") as (keyof Discount)[], + validatedParams?.expand?.split(",") + ) + + discount = await discountService.retrieve(discount.id, config) + + res.status(200).json({ discount }) +} + +export class AdminPostDiscountsDiscountConditionsCondition extends AdminUpsertConditionsReq {} + +export class AdminPostDiscountsDiscountConditionsConditionParams { + @IsString() + @IsOptional() + expand?: string + + @IsString() + @IsOptional() + fields?: string +} diff --git a/packages/medusa/src/models/discount-condition.ts b/packages/medusa/src/models/discount-condition.ts index 4dd96be349..4b1b70ec8f 100644 --- a/packages/medusa/src/models/discount-condition.ts +++ b/packages/medusa/src/models/discount-condition.ts @@ -145,6 +145,10 @@ export class DiscountCondition { @BeforeInsert() private beforeInsert() { + if (this.id) { + return + } + const id = ulid() this.id = `discon_${id}` } diff --git a/packages/medusa/src/repositories/discount-condition.ts b/packages/medusa/src/repositories/discount-condition.ts index c969865ea6..c5618ca0b7 100644 --- a/packages/medusa/src/repositories/discount-condition.ts +++ b/packages/medusa/src/repositories/discount-condition.ts @@ -6,6 +6,7 @@ import { Not, Repository, } from "typeorm" +import { Discount } from "../models" import { DiscountCondition, DiscountConditionOperator, @@ -35,6 +36,21 @@ type DiscountConditionResourceType = EntityTarget< @EntityRepository(DiscountCondition) export class DiscountConditionRepository extends Repository { + async findOneWithDiscount( + conditionId: string, + discountId: string + ): Promise<(DiscountCondition & { discount: Discount }) | undefined> { + return (await this.createQueryBuilder("condition") + .leftJoinAndMapOne( + "condition.discount", + Discount, + "discount", + `condition.discount_rule_id = discount.rule_id and discount.id = :discId and condition.id = :dcId`, + { discId: discountId, dcId: conditionId } + ) + .getOne()) as (DiscountCondition & { discount: Discount }) | undefined + } + getJoinTableResourceIdentifiers(type: string): { joinTable: string resourceKey: string diff --git a/packages/medusa/src/repositories/order.ts b/packages/medusa/src/repositories/order.ts index 9ef6c1b9f8..ca0458602a 100644 --- a/packages/medusa/src/repositories/order.ts +++ b/packages/medusa/src/repositories/order.ts @@ -1,5 +1,5 @@ import { flatten, groupBy, map, merge } from "lodash" -import { EntityRepository, Repository, FindManyOptions } from "typeorm" +import { EntityRepository, FindManyOptions, Repository } from "typeorm" import { Order } from "../models/order" @EntityRepository(Order) diff --git a/packages/medusa/src/services/discount-condition.ts b/packages/medusa/src/services/discount-condition.ts new file mode 100644 index 0000000000..e91707b994 --- /dev/null +++ b/packages/medusa/src/services/discount-condition.ts @@ -0,0 +1,179 @@ +import { MedusaError } from "medusa-core-utils" +import { BaseService } from "medusa-interfaces" +import { EntityManager } from "typeorm" +import { EventBusService } from "." +import { DiscountCondition, DiscountConditionType } from "../models" +import { DiscountConditionRepository } from "../repositories/discount-condition" +import { FindConfig } from "../types/common" +import { UpsertDiscountConditionInput } from "../types/discount" +import { PostgresError } from "../utils/exception-formatter" + +/** + * Provides layer to manipulate discount conditions. + * @implements {BaseService} + */ +class DiscountConditionService extends BaseService { + protected readonly manager_: EntityManager + protected readonly discountConditionRepository_: typeof DiscountConditionRepository + protected readonly eventBus_: EventBusService + protected transactionManager_?: EntityManager + + constructor({ manager, discountConditionRepository, eventBusService }) { + super() + + this.manager_ = manager + this.discountConditionRepository_ = discountConditionRepository + this.eventBus_ = eventBusService + } + + withTransaction(transactionManager: EntityManager): DiscountConditionService { + if (!transactionManager) { + return this + } + + const cloned = new DiscountConditionService({ + manager: transactionManager, + discountConditionRepository: this.discountConditionRepository_, + eventBusService: this.eventBus_, + }) + + cloned.transactionManager_ = transactionManager + + return cloned + } + + async retrieve( + conditionId: string, + config?: FindConfig + ): Promise { + return await this.atomicPhase_(async (manager: EntityManager) => { + const conditionRepo = manager.getCustomRepository( + this.discountConditionRepository_ + ) + + const query = this.buildQuery_({ id: conditionId }, config) + + const condition = await conditionRepo.findOne(query) + + if (!condition) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `DiscountCondition with id ${conditionId} was not found` + ) + } + + return condition + }) + } + + protected static resolveConditionType_(data: UpsertDiscountConditionInput): + | { + type: DiscountConditionType + resource_ids: string[] + } + | undefined { + switch (true) { + case !!data.products?.length: + return { + type: DiscountConditionType.PRODUCTS, + resource_ids: data.products!, + } + case !!data.product_collections?.length: + return { + type: DiscountConditionType.PRODUCT_COLLECTIONS, + resource_ids: data.product_collections!, + } + case !!data.product_types?.length: + return { + type: DiscountConditionType.PRODUCT_TYPES, + resource_ids: data.product_types!, + } + case !!data.product_tags?.length: + return { + type: DiscountConditionType.PRODUCT_TAGS, + resource_ids: data.product_tags!, + } + case !!data.customer_groups?.length: + return { + type: DiscountConditionType.CUSTOMER_GROUPS, + resource_ids: data.customer_groups!, + } + default: + return undefined + } + } + + async upsertCondition(data: UpsertDiscountConditionInput): Promise { + let resolvedConditionType + + return await this.atomicPhase_( + async (manager: EntityManager) => { + resolvedConditionType = + DiscountConditionService.resolveConditionType_(data) + + if (!resolvedConditionType) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Missing one of products, collections, tags, types or customer groups in data` + ) + } + + const discountConditionRepo: DiscountConditionRepository = + manager.getCustomRepository(this.discountConditionRepository_) + + if (data.id) { + return await discountConditionRepo.addConditionResources( + data.id, + resolvedConditionType.resource_ids, + resolvedConditionType.type, + true + ) + } + + const created = discountConditionRepo.create({ + discount_rule_id: data.rule_id, + operator: data.operator, + type: resolvedConditionType.type, + }) + + const discountCondition = await discountConditionRepo.save(created) + + return await discountConditionRepo.addConditionResources( + discountCondition.id, + resolvedConditionType.resource_ids, + resolvedConditionType.type + ) + }, + async (err: { code: string }) => { + if (err.code === PostgresError.DUPLICATE_ERROR) { + // A unique key constraint failed meaning the combination of + // discount rule id, type, and operator already exists in the db. + throw new MedusaError( + MedusaError.Types.DUPLICATE_ERROR, + `Discount Condition with operator '${data.operator}' and type '${resolvedConditionType?.type}' already exist on a Discount Rule` + ) + } + } + ) + } + + async delete(discountConditionId: string): Promise { + return await this.atomicPhase_(async (manager: EntityManager) => { + const conditionRepo = manager.getCustomRepository( + this.discountConditionRepository_ + ) + + const condition = await conditionRepo.findOne({ + where: { id: discountConditionId }, + }) + + if (!condition) { + return Promise.resolve() + } + + return await conditionRepo.remove(condition) + }) + } +} + +export default DiscountConditionService diff --git a/packages/medusa/src/services/discount.ts b/packages/medusa/src/services/discount.ts index f2cf604c97..1dadeaaed1 100644 --- a/packages/medusa/src/services/discount.ts +++ b/packages/medusa/src/services/discount.ts @@ -11,7 +11,6 @@ import { } from "." import { Cart } from "../models/cart" import { Discount } from "../models/discount" -import { DiscountConditionType } from "../models/discount-condition" import { AllocationType as DiscountAllocation, DiscountRule, @@ -28,10 +27,10 @@ import { CreateDynamicDiscountInput, FilterableDiscountProps, UpdateDiscountInput, - UpsertDiscountConditionInput, } from "../types/discount" import { isFuture, isPast } from "../utils/date-helpers" -import { formatException, PostgresError } from "../utils/exception-formatter" +import { formatException } from "../utils/exception-formatter" +import DiscountConditionService from "./discount-condition" /** * Provides layer to manipulate discounts. @@ -43,6 +42,7 @@ class DiscountService extends BaseService { private discountRuleRepository_: typeof DiscountRuleRepository private giftCardRepository_: typeof GiftCardRepository private discountConditionRepository_: typeof DiscountConditionRepository + private discountConditionService_: DiscountConditionService private totalsService_: TotalsService private productService_: ProductService private regionService_: RegionService @@ -54,6 +54,7 @@ class DiscountService extends BaseService { discountRuleRepository, giftCardRepository, discountConditionRepository, + discountConditionService, totalsService, productService, regionService, @@ -77,6 +78,9 @@ class DiscountService extends BaseService { /** @private @const {DiscountConditionRepository} */ this.discountConditionRepository_ = discountConditionRepository + /** @private @const {DiscountConditionRepository} */ + this.discountConditionService_ = discountConditionService + /** @private @const {TotalsService} */ this.totalsService_ = totalsService @@ -104,6 +108,7 @@ class DiscountService extends BaseService { discountRuleRepository: this.discountRuleRepository_, giftCardRepository: this.giftCardRepository_, discountConditionRepository: this.discountConditionRepository_, + discountConditionService: this.discountConditionService_, totalsService: this.totalsService_, productService: this.productService_, regionService: this.regionService_, @@ -263,9 +268,13 @@ class DiscountService extends BaseService { const result = await discountRepo.save(created) if (conditions?.length) { - for (const cond of conditions) { - await this.upsertDiscountCondition_(result.id, cond) - } + await Promise.all( + conditions.map(async (cond) => { + await this.discountConditionService_ + .withTransaction(manager) + .upsertCondition({ rule_id: result.rule_id, ...cond }) + }) + ) } return result @@ -382,9 +391,13 @@ class DiscountService extends BaseService { } if (conditions?.length) { - for (const cond of conditions) { - await this.upsertDiscountCondition_(discount.id, cond) - } + await Promise.all( + conditions.map(async (cond) => { + await this.discountConditionService_ + .withTransaction(manager) + .upsertCondition({ rule_id: discount.rule_id, ...cond }) + }) + ) } if (regions) { @@ -573,103 +586,6 @@ class DiscountService extends BaseService { }) } - resolveConditionType_(data: UpsertDiscountConditionInput): - | { - type: DiscountConditionType - resource_ids: string[] - } - | undefined { - switch (true) { - case !!data.products?.length: - return { - type: DiscountConditionType.PRODUCTS, - resource_ids: data.products!, - } - case !!data.product_collections?.length: - return { - type: DiscountConditionType.PRODUCT_COLLECTIONS, - resource_ids: data.product_collections!, - } - case !!data.product_types?.length: - return { - type: DiscountConditionType.PRODUCT_TYPES, - resource_ids: data.product_types!, - } - case !!data.product_tags?.length: - return { - type: DiscountConditionType.PRODUCT_TAGS, - resource_ids: data.product_tags!, - } - case !!data.customer_groups?.length: - return { - type: DiscountConditionType.CUSTOMER_GROUPS, - resource_ids: data.customer_groups!, - } - default: - return undefined - } - } - - async upsertDiscountCondition_( - discountId: string, - data: UpsertDiscountConditionInput - ): Promise { - const resolvedConditionType = this.resolveConditionType_(data) - - const res = this.atomicPhase_( - async (manager) => { - const discountConditionRepo: DiscountConditionRepository = - manager.getCustomRepository(this.discountConditionRepository_) - - if (!resolvedConditionType) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Missing one of products, collections, tags, types or customer groups in data` - ) - } - - if (data.id) { - return await discountConditionRepo.addConditionResources( - data.id, - resolvedConditionType.resource_ids, - resolvedConditionType.type, - true - ) - } - - const discount = await this.retrieve(discountId, { - relations: ["rule", "rule.conditions"], - }) - - const created = discountConditionRepo.create({ - discount_rule_id: discount.rule_id, - operator: data.operator, - type: resolvedConditionType.type, - }) - - const discountCondition = await discountConditionRepo.save(created) - - return await discountConditionRepo.addConditionResources( - discountCondition.id, - resolvedConditionType.resource_ids, - resolvedConditionType.type - ) - }, - async (err: { code: string }) => { - if (err.code === PostgresError.DUPLICATE_ERROR) { - // A unique key constraint failed meaning the combination of - // discount rule id, type, and operator already exists in the db. - throw new MedusaError( - MedusaError.Types.DUPLICATE_ERROR, - `Discount Condition with operator '${data.operator}' and type '${resolvedConditionType?.type}' already exist on a Discount Rule` - ) - } - } - ) - - return res - } - async validateDiscountForProduct( discountRuleId: string, productId: string | undefined diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index b746e2e049..6c5a48d862 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -672,7 +672,7 @@ class ProductService extends BaseService { ) if (!product) { - return Promise.resolve() + return } await productRepo.softRemove(product) @@ -1069,22 +1069,25 @@ class ProductService extends BaseService { const productArray = Array.isArray(products) ? products : [products] - const priceSelectionStrategy = - this.priceSelectionStrategy_.withTransaction(manager) + const priceSelectionStrategy = this.priceSelectionStrategy_.withTransaction( + manager + ) const productsWithPrices = await Promise.all( productArray.map(async (p) => { if (p.variants?.length) { p.variants = await Promise.all( p.variants.map(async (v) => { - const prices = - await priceSelectionStrategy.calculateVariantPrice(v.id, { + const prices = await priceSelectionStrategy.calculateVariantPrice( + v.id, + { region_id: regionId, currency_code: currencyCode, cart_id: cart_id, customer_id: customer_id, include_discount_prices, - }) + } + ) return { ...v, diff --git a/packages/medusa/src/types/discount.ts b/packages/medusa/src/types/discount.ts index 0e3a2199ba..e35b045cb2 100644 --- a/packages/medusa/src/types/discount.ts +++ b/packages/medusa/src/types/discount.ts @@ -105,6 +105,7 @@ export class AdminUpsertConditionsReq { } export type UpsertDiscountConditionInput = { + rule_id?: string id?: string operator?: DiscountConditionOperator products?: string[]