From bc42b201ea84884921333c3d0cf30f091e04138e Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 26 Sep 2023 14:59:55 +0200 Subject: [PATCH] feat(pricing): add price rule entity (#5050) **What** - add price-rule entity to pricing module blocked by #4977 Fixes CORE-1497 Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com> Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com> --- .../__fixtures__/price-rule/data.ts | 24 ++ .../__fixtures__/price-rule/index.ts | 85 +++++ .../__fixtures__/rule-type/index.ts | 11 +- .../services/price-rule/index.spec.ts | 325 ++++++++++++++++++ .../pricing-module/price-rule.spec.ts | 322 +++++++++++++++++ .../services/pricing-module/rule-type.spec.ts | 3 + packages/pricing/src/loaders/container.ts | 8 +- .../migrations/.snapshot-medusa-pricing.json | 139 ++++++++ .../src/migrations/Migration20230913055746.ts | 23 ++ packages/pricing/src/models/index.ts | 1 + packages/pricing/src/models/price-rule.ts | 69 ++++ packages/pricing/src/repositories/index.ts | 1 + .../pricing/src/repositories/price-rule.ts | 144 ++++++++ packages/pricing/src/services/index.ts | 3 +- packages/pricing/src/services/price-rule.ts | 97 ++++++ .../pricing/src/services/pricing-module.ts | 110 +++++- packages/types/src/pricing/common/index.ts | 1 + .../types/src/pricing/common/price-rule.ts | 44 +++ packages/types/src/pricing/index.ts | 2 +- packages/types/src/pricing/service.ts | 50 ++- 20 files changed, 1444 insertions(+), 18 deletions(-) create mode 100644 packages/pricing/integration-tests/__fixtures__/price-rule/data.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/price-rule/index.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts create mode 100644 packages/pricing/src/migrations/Migration20230913055746.ts create mode 100644 packages/pricing/src/models/price-rule.ts create mode 100644 packages/pricing/src/repositories/price-rule.ts create mode 100644 packages/pricing/src/services/price-rule.ts create mode 100644 packages/types/src/pricing/common/price-rule.ts diff --git a/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts b/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts new file mode 100644 index 0000000000..ba422fb1cc --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts @@ -0,0 +1,24 @@ +import { CreatePriceRuleDTO } from "@medusajs/types" + +export const defaultPriceRuleData = [ + { + id: "price-rule-1", + price_set_id: "price-set-1", + rule_type_id: "rule-type-1", + value: "region_1", + price_list_id: "test", + price_set_money_amount: { + money_amount: { amount: 100, currency_code: "EUR" }, + }, + }, + { + id: "price-rule-2", + price_set_id: "price-set-2", + rule_type_id: "rule-type-1", + value: "region_2", + price_list_id: "test", + price_set_money_amount: { + money_amount: { amount: 100, currency_code: "EUR" }, + }, + }, +] as unknown as CreatePriceRuleDTO[] diff --git a/packages/pricing/integration-tests/__fixtures__/price-rule/index.ts b/packages/pricing/integration-tests/__fixtures__/price-rule/index.ts new file mode 100644 index 0000000000..3bc2849584 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-rule/index.ts @@ -0,0 +1,85 @@ +import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models" + +import { CreatePriceRuleDTO } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { createMoneyAmounts } from "../money-amount" +import { createRuleTypes } from "../rule-type" +import { defaultPriceRuleData } from "./data" + +export async function createPriceRules( + manager: SqlEntityManager, + pricesRulesData: CreatePriceRuleDTO[] = defaultPriceRuleData +): Promise { + const priceRules: PriceRule[] = [] + + for (let priceRuleData of pricesRulesData) { + const priceRuleDataClone = { ...priceRuleData } + + if (priceRuleDataClone.price_set_id) { + priceRuleDataClone.price_set = manager.getReference( + PriceSet, + priceRuleDataClone.price_set_id + ) + } + + let dbRuleType: RuleType | undefined = await manager.findOne(RuleType, { + id: priceRuleDataClone.rule_type_id, + }) + + if (!dbRuleType) { + const [createdRuleType] = await createRuleTypes(manager, [ + { + id: priceRuleDataClone.rule_type_id, + name: "rule 1", + rule_attribute: "region_id", + }, + ]) + + dbRuleType = createdRuleType + } + + priceRuleDataClone.rule_type = manager.getReference( + RuleType, + dbRuleType!.id + ) + + const priceSetMoneyAmountId = + priceRuleDataClone.price_set_money_amount_id || + priceRuleDataClone.price_set_money_amount?.id + + let psma: PriceSetMoneyAmount = await manager.findOne(PriceSetMoneyAmount, {id: priceSetMoneyAmountId}) + + if (!psma) { + const [ma] = await createMoneyAmounts(manager, [ + priceRuleDataClone.price_set_money_amount.money_amount ?? { + amount: 100, + currency_code: "EUR", + }, + ]) + + psma = manager.create(PriceSetMoneyAmount, { + price_set: manager.getReference( + PriceSet, + priceRuleDataClone.price_set.id + ), + money_amount: ma.id, + title: "test", + }) + + await manager.persist(psma).flush() + } + + priceRuleDataClone.price_set_money_amount = manager.getReference( + PriceSetMoneyAmount, + psma.id + ) + + const priceRule = manager.create(PriceRule, priceRuleDataClone) + + priceRules.push(priceRule) + } + + await manager.persistAndFlush(priceRules) + + return priceRules +} diff --git a/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts index 458c6f5449..7c6b3d7b50 100644 --- a/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts @@ -1,20 +1,19 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" import { RuleType } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" import { defaultRuleTypesData } from "./data" export async function createRuleTypes( manager: SqlEntityManager, ruletypesData: any[] = defaultRuleTypesData ): Promise { - const RuleTypes: RuleType[] = [] + const ruleTypes: RuleType[] = [] for (let ruleTypeData of ruletypesData) { const ruleType = manager.create(RuleType, ruleTypeData) - RuleTypes.push(ruleType) + await manager.persistAndFlush(ruleType) + ruleTypes.push(ruleType) } - await manager.persistAndFlush(RuleTypes) - - return RuleTypes + return ruleTypes } diff --git a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts new file mode 100644 index 0000000000..f31aae1b5d --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts @@ -0,0 +1,325 @@ +import { PriceSet, PriceSetMoneyAmount } from "@models" + +import { CreatePriceRuleDTO } from "@medusajs/types" +import { MikroOrmWrapper } from "../../../utils" +import { PriceRuleRepository } from "@repositories" +import { PriceRuleService } from "@services" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { createCurrencies } from "../../../__fixtures__/currency" +import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { createPriceRules } from "../../../__fixtures__/price-rule" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { createRuleTypes } from "../../../__fixtures__/rule-type" + +jest.setTimeout(30000) + +describe("PriceRule Service", () => { + let service: PriceRuleService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + testManager = await MikroOrmWrapper.forkManager() + + const priceRuleRepository = new PriceRuleRepository({ + manager: repositoryManager, + }) + + service = new PriceRuleService({ + priceRuleRepository: priceRuleRepository, + }) + + await createCurrencies(testManager) + + await createRuleTypes(testManager) + await createPriceSets(testManager) + await createPriceRules(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("list", () => { + it("should list priceRules", async () => { + const priceRuleResult = await service.list() + const serialized = JSON.parse(JSON.stringify(priceRuleResult)) + + expect(serialized).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should list priceRules by id", async () => { + const priceRuleResult = await service.list({ + id: ["price-rule-1"], + }) + + expect(priceRuleResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + ]) + }) + + it("should list priceRules with relations and selects", async () => { + const priceRulesResult = await service.list( + { + id: ["price-rule-1"], + }, + { + select: ["id", "price_set.id"], + relations: ["price_set"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceRulesResult)) + + expect(serialized).toEqual([ + { + id: "price-rule-1", + price_set: { + id: "price-set-1", + }, + }, + ]) + }) + + describe("listAndCount", () => { + it("should return priceRules and count", async () => { + const [priceRulesResult, count] = await service.listAndCount() + + expect(count).toEqual(2) + expect(priceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should return priceRules and count when filtered", async () => { + const [priceRulesResult, count] = await service.listAndCount({ + id: ["price-rule-1"], + }) + + expect(count).toEqual(1) + expect(priceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + ]) + }) + + it("should list priceRules with relations and selects", async () => { + const [priceRulesResult, count] = await service.listAndCount( + { + id: ["price-rule-1"], + }, + { + select: ["id", "price_set.id"], + relations: ["price_set"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceRulesResult)) + + expect(count).toEqual(1) + expect(serialized).toEqual([ + { + id: "price-rule-1", + price_set: { + id: "price-set-1", + }, + }, + ]) + }) + + it("should return priceRules and count when using skip and take", async () => { + const [priceRulesResult, count] = await service.listAndCount( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(2) + expect(priceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [priceRulesResult, count] = await service.listAndCount( + {}, + { + take: 1, + select: ["id"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceRulesResult)) + + expect(count).toEqual(2) + expect(serialized).toEqual([ + { + id: "price-rule-1", + }, + ]) + }) + }) + + describe("retrieve", () => { + const id = "price-rule-1" + + it("should return priceRule for the given id", async () => { + const priceRule = await service.retrieve(id) + + expect(priceRule).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when priceRule with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceRule with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.retrieve(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"priceRuleId" must be defined') + }) + + it("should return priceRule based on config select param", async () => { + const priceRule = await service.retrieve(id, { + select: ["id"], + }) + + const serialized = JSON.parse(JSON.stringify(priceRule)) + + expect(serialized).toEqual({ + id, + }) + }) + }) + + describe("delete", () => { + const id = "price-set-1" + + it("should delete the priceRules given an id successfully", async () => { + await service.delete([id]) + + const priceRules = await service.list({ + id: [id], + }) + + expect(priceRules).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "price-set-1" + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceRule with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + random: "does-not-exist", + } as any, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual('PriceRule with id "undefined" not found') + }) + + it("should create a priceRule successfully", async () => { + const [ma] = await createMoneyAmounts(testManager, [ + { + amount: 100, + currency_code: "EUR", + }, + ]) + + const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, { + price_set: testManager.getReference(PriceSet, "price-set-1"), + money_amount: ma.id, + title: "test", + }) + + await testManager.persist(psma).flush() + + await service.create([ + { + id: "price-rule-new", + price_set_id: "price-set-1", + rule_type_id: "rule-type-1", + value: "region_1", + price_list_id: "test", + price_set_money_amount_id: psma.id, + } as unknown as CreatePriceRuleDTO, + ]) + + const [pricerule] = await service.list({ + id: ["price-rule-new"], + }) + + expect(pricerule).toEqual( + expect.objectContaining({ + id: "price-rule-new", + } as unknown as CreatePriceRuleDTO) + ) + }) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts new file mode 100644 index 0000000000..87228d45b4 --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts @@ -0,0 +1,322 @@ +import { CreatePriceRuleDTO, IPricingModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSet } from "@models" + +import { PriceSetMoneyAmount, initialize } from "../../../../src" +import { createCurrencies } from "../../../__fixtures__/currency" +import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { DB_URL, MikroOrmWrapper } from "../../../utils" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { createPriceRules } from "../../../__fixtures__/price-rule" + +jest.setTimeout(30000) + +describe("PricingModule Service - PriceRule", () => { + let service: IPricingModuleService + let testManager: SqlEntityManager + + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + testManager = MikroOrmWrapper.forkManager() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRICING_DB_SCHEMA, + }, + }) + + await createCurrencies(testManager) + + await createRuleTypes(testManager) + await createPriceSets(testManager) + await createPriceRules(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("list", () => { + it("should list priceRules", async () => { + const PriceRulesResult = await service.listPriceRules() + const serialized = JSON.parse(JSON.stringify(PriceRulesResult)) + + expect(serialized).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should list priceRules by id", async () => { + const priceRuleResult = await service.listPriceRules({ + id: ["price-rule-1"], + }) + + expect(priceRuleResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + ]) + }) + + it("should list priceRules with relations and selects", async () => { + const priceRulesResult = await service.listPriceRules( + { + id: ["price-rule-1"], + }, + { + select: ["id", "price_set.id"], + relations: ["price_set"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceRulesResult)) + + expect(serialized).toEqual([ + { + id: "price-rule-1", + price_set: { + id: "price-set-1", + }, + }, + ]) + }) + + describe("listAndCount", () => { + it("should return priceRules and count", async () => { + const [priceRulesResult, count] = await service.listAndCountPriceRules() + + expect(count).toEqual(2) + expect(priceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should return priceRules and count when filtered", async () => { + const [priceRulesResult, count] = await service.listAndCountPriceRules({ + id: ["price-rule-1"], + }) + + expect(count).toEqual(1) + expect(priceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-1", + }), + ]) + }) + + it("should list PriceRules with relations and selects", async () => { + const [PriceRulesResult, count] = await service.listAndCountPriceRules( + { + id: ["price-rule-1"], + }, + { + select: ["id", "price_set.id"], + relations: ["price_set"], + } + ) + + const serialized = JSON.parse(JSON.stringify(PriceRulesResult)) + + expect(count).toEqual(1) + expect(serialized).toEqual([ + { + id: "price-rule-1", + price_set: { + id: "price-set-1", + }, + }, + ]) + }) + + it("should return PriceRules and count when using skip and take", async () => { + const [PriceRulesResult, count] = await service.listAndCountPriceRules( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(2) + expect(PriceRulesResult).toEqual([ + expect.objectContaining({ + id: "price-rule-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [PriceRulesResult, count] = await service.listAndCountPriceRules( + {}, + { + take: 1, + select: ["id"], + } + ) + + const serialized = JSON.parse(JSON.stringify(PriceRulesResult)) + + expect(count).toEqual(2) + expect(serialized).toEqual([ + { + id: "price-rule-1", + }, + ]) + }) + }) + + describe("retrieve", () => { + const id = "price-rule-1" + + it("should return PriceRule for the given id", async () => { + const PriceRule = await service.retrievePriceRule(id) + + expect(PriceRule).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when PriceRule with id does not exist", async () => { + let error + + try { + await service.retrievePriceRule("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceRule with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.retrievePriceRule(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"priceRuleId" must be defined') + }) + + it("should return PriceRule based on config select param", async () => { + const PriceRule = await service.retrievePriceRule(id, { + select: ["id"], + }) + + const serialized = JSON.parse(JSON.stringify(PriceRule)) + + expect(serialized).toEqual({ + id, + }) + }) + }) + + describe("delete", () => { + const id = "price-set-1" + + it("should delete the PriceRules given an id successfully", async () => { + await service.deletePriceRules([id]) + + const PriceRules = await service.listPriceRules({ + id: [id], + }) + + expect(PriceRules).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "price-set-1" + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.updatePriceRules([ + { + id: "does-not-exist", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceRule with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.updatePriceRules([ + { + random: "does-not-exist", + } as any, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual('PriceRule with id "undefined" not found') + }) + + it("should create a PriceRule successfully", async () => { + const [ma] = await createMoneyAmounts(testManager, [ + { + amount: 100, + currency_code: "EUR", + }, + ]) + + const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, { + price_set: testManager.getReference(PriceSet, "price-set-1"), + money_amount: ma.id, + title: "test", + }) + + await testManager.persist(psma).flush() + + await service.createPriceRules([ + { + id: "price-rule-new", + price_set_id: "price-set-1", + rule_type_id: "rule-type-1", + value: "region_1", + price_list_id: "test", + price_set_money_amount_id: psma.id, + } as unknown as CreatePriceRuleDTO, + ]) + + const [pricerule] = await service.listPriceRules({ + id: ["price-rule-new"], + }) + + expect(pricerule).toEqual( + expect.objectContaining({ + id: "price-rule-new", + } as unknown as CreatePriceRuleDTO) + ) + }) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts index 04b5d763d3..89f977c0bb 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts @@ -1,3 +1,6 @@ +import { DB_URL, MikroOrmWrapper } from "../../../utils" + +import { Currency } from "@models" import { IPricingModuleService } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" diff --git a/packages/pricing/src/loaders/container.ts b/packages/pricing/src/loaders/container.ts index 020322d01b..a94f1f8a98 100644 --- a/packages/pricing/src/loaders/container.ts +++ b/packages/pricing/src/loaders/container.ts @@ -1,10 +1,10 @@ -import { ModulesSdkTypes } from "@medusajs/types" import * as defaultRepositories from "@repositories" import * as defaultServices from "@services" import { LoaderOptions } from "@medusajs/modules-sdk" -import { loadCustomRepositories } from "@medusajs/utils" +import { ModulesSdkTypes } from "@medusajs/types" import { asClass } from "awilix" +import { loadCustomRepositories } from "@medusajs/utils" export default async ({ container, @@ -22,6 +22,7 @@ export default async ({ moneyAmountService: asClass(defaultServices.MoneyAmountService).singleton(), priceSetService: asClass(defaultServices.PriceSetService).singleton(), ruleTypeService: asClass(defaultServices.RuleTypeService).singleton(), + priceRuleService: asClass(defaultServices.PriceRuleService).singleton(), }) if (customRepositories) { @@ -50,5 +51,8 @@ function loadDefaultRepositories({ container }) { ruleTypeRepository: asClass( defaultRepositories.RuleTypeRepository ).singleton(), + priceRuleRepository: asClass( + defaultRepositories.PriceRuleRepository + ).singleton(), }) } diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json index c5b2fd0c72..0ecb8d9534 100644 --- a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json +++ b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json @@ -337,6 +337,145 @@ ], "checks": [], "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "price_set_id": { + "name": "price_set_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "rule_type_id": { + "name": "rule_type_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "is_dynamic": { + "name": "is_dynamic", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "priority": { + "name": "priority", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + }, + "price_set_money_amount_id": { + "name": "price_set_money_amount_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "price_list_id": { + "name": "price_list_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "price_rule", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "rule_type_id" + ], + "composite": false, + "keyName": "price_rule_rule_type_id_unique", + "primary": false, + "unique": true + }, + { + "keyName": "price_rule_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "price_rule_price_set_id_foreign": { + "constraintName": "price_rule_price_set_id_foreign", + "columnNames": [ + "price_set_id" + ], + "localTableName": "public.price_rule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.price_set", + "updateRule": "cascade" + }, + "price_rule_rule_type_id_foreign": { + "constraintName": "price_rule_rule_type_id_foreign", + "columnNames": [ + "rule_type_id" + ], + "localTableName": "public.price_rule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.rule_type", + "updateRule": "cascade" + }, + "price_rule_price_set_money_amount_id_foreign": { + "constraintName": "price_rule_price_set_money_amount_id_foreign", + "columnNames": [ + "price_set_money_amount_id" + ], + "localTableName": "public.price_rule", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.price_set_money_amount", + "updateRule": "cascade" + } + } } ] } diff --git a/packages/pricing/src/migrations/Migration20230913055746.ts b/packages/pricing/src/migrations/Migration20230913055746.ts new file mode 100644 index 0000000000..e42ad50860 --- /dev/null +++ b/packages/pricing/src/migrations/Migration20230913055746.ts @@ -0,0 +1,23 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20230913055746 extends Migration { + + async up(): Promise { + this.addSql('create table "price_rule" ("id" text not null, "price_set_id" text not null, "rule_type_id" text not null, "is_dynamic" boolean not null default false, "value" text not null, "priority" integer not null default 0, "price_set_money_amount_id" text not null, "price_list_id" text not null, constraint "price_rule_pkey" primary key ("id"));'); + + this.addSql('alter table "price_rule" add constraint "price_rule_price_set_id_foreign" foreign key ("price_set_id") references "price_set" ("id") on update cascade;'); + this.addSql('alter table "price_rule" add constraint "price_rule_rule_type_id_foreign" foreign key ("rule_type_id") references "rule_type" ("id") on update cascade;'); + this.addSql('alter table "price_rule" add constraint "price_rule_price_set_money_amount_id_foreign" foreign key ("price_set_money_amount_id") references "price_set_money_amount" ("id") on update cascade;'); + } + + async down(): Promise { + this.addSql('alter table "price_rule" drop constraint "price_rule_price_set_id_foreign";'); + + this.addSql('alter table "price_rule" drop constraint "price_rule_price_set_money_amount_id_foreign";'); + + this.addSql('alter table "price_rule" drop constraint "price_rule_rule_type_id_foreign";'); + + this.addSql('drop table if exists "price_rule" cascade;'); + }g + +} diff --git a/packages/pricing/src/models/index.ts b/packages/pricing/src/models/index.ts index 08b2d18e18..7929d9ce0f 100644 --- a/packages/pricing/src/models/index.ts +++ b/packages/pricing/src/models/index.ts @@ -3,3 +3,4 @@ export { default as MoneyAmount } from "./money-amount" export { default as PriceSet } from "./price-set" export { default as PriceSetMoneyAmount } from "./price-set-money-amount" export { default as RuleType } from "./rule-type" +export { default as PriceRule } from "./price-rule" \ No newline at end of file diff --git a/packages/pricing/src/models/price-rule.ts b/packages/pricing/src/models/price-rule.ts new file mode 100644 index 0000000000..4a179b0af3 --- /dev/null +++ b/packages/pricing/src/models/price-rule.ts @@ -0,0 +1,69 @@ +import { + BeforeCreate, + Collection, + Entity, + ManyToMany, + ManyToOne, + OneToMany, + OneToOne, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import MoneyAmount from "./money-amount" +import PriceSet from "./price-set" +import PriceSetMoneyAmount from "./price-set-money-amount" +import RuleType from "./rule-type" +import { generateEntityId } from "@medusajs/utils" + +type OptionalFields = "id" | "is_dynamic" | "priority" +type OptionalRelations = "price_set" | "rule_type" | "price_set_money_amount" + +@Entity() +export default class PriceRule { + [OptionalProps]: OptionalFields | OptionalRelations + + @PrimaryKey({ columnType: "text" }) + id!: string + + @ManyToOne({ + entity: () => PriceSet, + fieldName: "price_set_id", + name: "price_rule_price_set_id_unique", + }) + price_set: PriceSet + + @ManyToOne({ + entity: () => RuleType, + fieldName: "rule_type_id", + name: "price_rule_rule_type_id_unique", + }) + rule_type: RuleType + + @Property({ columnType: "boolean", default: false }) + is_dynamic: boolean + + @Property({ columnType: "text" }) + value: string + + @Property({ columnType: "integer", default: 0 }) + priority: number + + @ManyToOne({ + entity: () => PriceSetMoneyAmount, + fieldName: "price_set_money_amount_id", + name: "price_set_money_amount_id_unique", + }) + price_set_money_amount: PriceSetMoneyAmount + + @Property({ columnType: "text" }) + price_list_id!: string + + // TODO: Add price list + + @BeforeCreate() + beforeCreate() { + this.id = generateEntityId(this.id, "pset") + } +} diff --git a/packages/pricing/src/repositories/index.ts b/packages/pricing/src/repositories/index.ts index 6e66f5cd35..1d2b4eec1b 100644 --- a/packages/pricing/src/repositories/index.ts +++ b/packages/pricing/src/repositories/index.ts @@ -3,3 +3,4 @@ export { CurrencyRepository } from "./currency" export { MoneyAmountRepository } from "./money-amount" export { PriceSetRepository } from "./price-set" export { RuleTypeRepository } from "./rule-type" +export { PriceRuleRepository } from "./price-rule" diff --git a/packages/pricing/src/repositories/price-rule.ts b/packages/pricing/src/repositories/price-rule.ts new file mode 100644 index 0000000000..0e5c6f3ca5 --- /dev/null +++ b/packages/pricing/src/repositories/price-rule.ts @@ -0,0 +1,144 @@ +import { + Context, + CreatePriceRuleDTO, + DAL, + UpdatePriceRuleDTO, +} from "@medusajs/types" +import { DALUtils, MedusaError } from "@medusajs/utils" +import { + LoadStrategy, + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, +} from "@mikro-orm/core" +import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models" + +import { SqlEntityManager } from "@mikro-orm/postgresql" + +export class PriceRuleRepository extends DALUtils.MikroOrmBaseRepository { + protected readonly manager_: SqlEntityManager + + constructor({ manager }: { manager: SqlEntityManager }) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.manager_ = manager + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await manager.find( + PriceRule, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[PriceRule[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await manager.findAndCount( + PriceRule, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(PriceRule, { id: { $in: ids } }, {}) + } + + async create( + data: CreatePriceRuleDTO[], + context: Context = {} + ): Promise { + const manager: SqlEntityManager = + this.getActiveManager(context) + + const toCreate = await Promise.all(data.map(async (ruleData) => { + const ruleDataClone = { ...ruleData } as CreatePriceRuleDTO & { + rule_type: string + price_set: string + price_set_money_amount: string + } + ruleDataClone.rule_type = ruleData.rule_type_id + + ruleDataClone.price_set = ruleData.price_set_id + + ruleDataClone.price_set_money_amount = ruleData.price_set_money_amount_id + + return ruleDataClone + })) + + const priceRules = toCreate.map((ruleData) => { + return manager.create(PriceRule, ruleData as CreatePriceRuleDTO) + }) + + manager.persist(priceRules) + + return priceRules + } + + async update( + data: UpdatePriceRuleDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const priceRuleIds = data.map((priceRuleData) => priceRuleData.id) + const existingPriceRules = await this.find( + { + where: { + id: { + $in: priceRuleIds, + }, + }, + }, + context + ) + + const existingPriceRulesMap = new Map( + existingPriceRules.map<[string, PriceRule]>((priceRule) => [ + priceRule.id, + priceRule, + ]) + ) + + const priceRules = data.map((priceRuleData) => { + const existingPriceRule = existingPriceRulesMap.get(priceRuleData.id) + + if (!existingPriceRule) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `PriceRule with id "${priceRuleData.id}" not found` + ) + } + + return manager.assign(existingPriceRule, priceRuleData) + }) + + manager.persist(priceRules) + + return priceRules + } +} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index 4b5f3692a1..6799d0d2b8 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -2,4 +2,5 @@ export { default as CurrencyService } from "./currency" export { default as MoneyAmountService } from "./money-amount" export { default as PriceSetService } from "./price-set" export { default as PricingModuleService } from "./pricing-module" -export { default as RuleTypeService } from "./rule-type" \ No newline at end of file +export { default as RuleTypeService } from "./rule-type" +export { default as PriceRuleService } from "./price-rule" \ No newline at end of file diff --git a/packages/pricing/src/services/price-rule.ts b/packages/pricing/src/services/price-rule.ts new file mode 100644 index 0000000000..8b26c5e43f --- /dev/null +++ b/packages/pricing/src/services/price-rule.ts @@ -0,0 +1,97 @@ +import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + doNotForceTransaction, + retrieveEntity, + shouldForceTransaction, +} from "@medusajs/utils" +import { PriceRule } from "@models" +import { PriceRuleRepository } from "@repositories" + +type InjectedDependencies = { + priceRuleRepository: DAL.RepositoryService +} + +export default class PriceRuleService { + protected readonly priceRuleRepository_: DAL.RepositoryService + + constructor({ priceRuleRepository }: InjectedDependencies) { + this.priceRuleRepository_ = priceRuleRepository + } + + @InjectManager("priceRuleRepository_") + async retrieve( + priceRuleId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity({ + id: priceRuleId, + entityName: PriceRule.name, + repository: this.priceRuleRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("priceRuleRepository_") + async list( + filters: PricingTypes.FilterablePriceRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryConfig = ModulesSdkUtils.buildQuery(filters, config) + + return (await this.priceRuleRepository_.find( + queryConfig, + sharedContext + )) as TEntity[] + } + + @InjectManager("priceRuleRepository_") + async listAndCount( + filters: PricingTypes.FilterablePriceRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + const queryConfig = ModulesSdkUtils.buildQuery(filters, config) + + return (await this.priceRuleRepository_.findAndCount( + queryConfig, + sharedContext + )) as [TEntity[], number] + } + + @InjectTransactionManager(shouldForceTransaction, "priceRuleRepository_") + async create( + data: PricingTypes.CreatePriceRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.priceRuleRepository_ as PriceRuleRepository).create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "priceRuleRepository_") + async update( + data: PricingTypes.UpdatePriceRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.priceRuleRepository_ as PriceRuleRepository).update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(doNotForceTransaction, "priceRuleRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceRuleRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index e47943563e..4e0bd5f1dd 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -8,13 +8,14 @@ import { PricingFilters, PricingTypes, } from "@medusajs/types" -import { Currency, MoneyAmount, PriceSet, RuleType } from "@models" import { CurrencyService, MoneyAmountService, PriceSetService, RuleTypeService, + PriceRuleService } from "@services" +import { Currency, MoneyAmount, PriceRule, PriceSet, RuleType } from "@models" import { InjectManager, @@ -29,15 +30,17 @@ type InjectedDependencies = { baseRepository: DAL.RepositoryService currencyService: CurrencyService moneyAmountService: MoneyAmountService - ruleTypeService: RuleTypeService priceSetService: PriceSetService + ruleTypeService: RuleTypeService + priceRuleService: PriceRuleService } export default class PricingModuleService< TPriceSet extends PriceSet = PriceSet, TMoneyAmount extends MoneyAmount = MoneyAmount, TCurrency extends Currency = Currency, - TRuleType extends RuleType = RuleType + TRuleType extends RuleType = RuleType, + TPriceRule extends PriceRule = PriceRule, > implements PricingTypes.IPricingModuleService { protected baseRepository_: DAL.RepositoryService @@ -45,6 +48,7 @@ export default class PricingModuleService< protected readonly moneyAmountService_: MoneyAmountService protected readonly ruleTypeService_: RuleTypeService protected readonly priceSetService_: PriceSetService + protected readonly priceRuleService_: PriceRuleService constructor( { @@ -53,6 +57,7 @@ export default class PricingModuleService< currencyService, ruleTypeService, priceSetService, + priceRuleService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -61,6 +66,7 @@ export default class PricingModuleService< this.moneyAmountService_ = moneyAmountService this.ruleTypeService_ = ruleTypeService this.priceSetService_ = priceSetService + this.priceRuleService_ = priceRuleService } __joinerConfig(): ModuleJoinerConfig { @@ -520,4 +526,102 @@ export default class PricingModuleService< ): Promise { await this.ruleTypeService_.delete(ruleTypes, sharedContext) } + + @InjectManager("baseRepository_") + async retrievePriceRule( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceRule = await this.priceRuleService_.retrieve( + id, + config, + sharedContext + ) + + return this.baseRepository_.serialize(priceRule, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async listPriceRules( + filters: PricingTypes.FilterablePriceRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceRules = await this.priceRuleService_.list( + filters, + config, + sharedContext + ) + + return this.baseRepository_.serialize( + priceRules, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async listAndCountPriceRules( + filters: PricingTypes.FilterablePriceRuleProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PricingTypes.PriceRuleDTO[], number]> { + const [priceRules, count] = await this.priceRuleService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + priceRules, + { + populate: true, + } + ), + count, + ] + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async createPriceRules( + data: PricingTypes.CreatePriceRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceRules = await this.priceRuleService_.create(data, sharedContext) + + return this.baseRepository_.serialize( + priceRules, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async updatePriceRules( + data: PricingTypes.UpdatePriceRuleDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceRules = await this.priceRuleService_.update(data, sharedContext) + + return this.baseRepository_.serialize( + priceRules, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async deletePriceRules( + priceRuleIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceRuleService_.delete(priceRuleIds, sharedContext) + } } diff --git a/packages/types/src/pricing/common/index.ts b/packages/types/src/pricing/common/index.ts index b50ecce4ba..c5aec182e6 100644 --- a/packages/types/src/pricing/common/index.ts +++ b/packages/types/src/pricing/common/index.ts @@ -2,3 +2,4 @@ export * from "./currency" export * from "./money-amount" export * from "./price-set" export * from "./rule-type" +export * from './price-rule' diff --git a/packages/types/src/pricing/common/price-rule.ts b/packages/types/src/pricing/common/price-rule.ts new file mode 100644 index 0000000000..6ed9858568 --- /dev/null +++ b/packages/types/src/pricing/common/price-rule.ts @@ -0,0 +1,44 @@ +import { BaseFilterable } from "../../dal" +import { PriceSetDTO } from "./price-set" +import { RuleTypeDTO } from "./rule-type" + +export interface PriceRuleDTO { + id: string + price_set_id: string + price_set: PriceSetDTO + rule_type_id: string + rule_type: RuleTypeDTO + is_dynamic: boolean + value: string + priority: number + price_set_money_amount_id: string + price_list_id: string +} + +export interface CreatePriceRuleDTO { + id: string + price_set_id: string + rule_type_id: string + is_dynamic?: boolean + value: string + priority?: number + price_set_money_amount_id: string + price_list_id: string +} + +export interface UpdatePriceRuleDTO { + id: string + price_set_id?: string + rule_type_id?: string + is_dynamic?: boolean + value?: string + priority?: number + price_set_money_amount_id?: string + price_list_id?: string +} + +export interface FilterablePriceRuleProps + extends BaseFilterable { + id?: string[] + name?: string[] +} diff --git a/packages/types/src/pricing/index.ts b/packages/types/src/pricing/index.ts index 5d6d17eded..eade309433 100644 --- a/packages/types/src/pricing/index.ts +++ b/packages/types/src/pricing/index.ts @@ -1,2 +1,2 @@ export * from "./common" -export * from "./service" \ No newline at end of file +export * from "./service" diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index 2d164a0500..7b32d3b25c 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -1,28 +1,33 @@ -import { FindConfig } from "../common" -import { ModuleJoinerConfig } from "../modules-sdk" -import { Context } from "../shared-context" import { CalculatedPriceSetDTO, CreateCurrencyDTO, CreateMoneyAmountDTO, + CreatePriceRuleDTO, CreatePriceSetDTO, CreateRuleTypeDTO, CurrencyDTO, FilterableCurrencyProps, FilterableMoneyAmountProps, + FilterablePriceRuleProps, FilterablePriceSetProps, FilterableRuleTypeProps, MoneyAmountDTO, + PriceRuleDTO, PriceSetDTO, PricingContext, PricingFilters, RuleTypeDTO, UpdateCurrencyDTO, UpdateMoneyAmountDTO, + UpdatePriceRuleDTO, UpdatePriceSetDTO, UpdateRuleTypeDTO, } from "./common" +import { Context } from "../shared-context" +import { FindConfig } from "../common" +import { ModuleJoinerConfig } from "../modules-sdk" + export interface IPricingModuleService { __joinerConfig(): ModuleJoinerConfig @@ -124,7 +129,6 @@ export interface IPricingModuleService { currencyCodes: string[], sharedContext?: Context ): Promise - retrieveRuleType( code: string, config?: FindConfig, @@ -153,5 +157,41 @@ export interface IPricingModuleService { sharedContext?: Context ): Promise - deleteRuleTypes(ruleTypes: string[], sharedContext?: Context): Promise + deleteRuleTypes( + ruleTypeIds: string[], + sharedContext?: Context + ): Promise + + retrievePriceRule( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listPriceRules( + filters?: FilterablePriceRuleProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountPriceRules( + filters?: FilterablePriceRuleProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[PriceRuleDTO[], number]> + + createPriceRules( + data: CreatePriceRuleDTO[], + sharedContext?: Context + ): Promise + + updatePriceRules( + data: UpdatePriceRuleDTO[], + sharedContext?: Context + ): Promise + + deletePriceRules( + priceRuleIds: string[], + sharedContext?: Context + ): Promise }