From 1e7db5a5cb7c955e72c52e64df8a16b1607eef70 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 29 Sep 2023 13:23:41 +0200 Subject: [PATCH] feat(pricing, types, utils): Exact match based on context + fallback on rule priority if not (#5214) * initial * initial service * update pricing module service * add integration test for rule-type * update pricing-module integration tests * update pricing service interface * feat(pricing): PriceSets as entry point to pricing module * chore: add price set money amount * chore: add price set money amount * chore: change name of test * chore: added changeset * chore: use filterable props from money amount in price sets * chore: update migrations * test update integration test * fix weird behavior * Update packages/pricing/integration-tests/__fixtures__/rule-type/index.ts Co-authored-by: Riqwan Thamir * Apply suggestions from code review Co-authored-by: Riqwan Thamir * move rule-type to common * chore: reset migration * chore: remove incorrect conflicts * chore: address review * chore: remove ghost price list * Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * update id prefix * use persist not persistAndflush * rename key_value to rule_attribute * more renaming * feat(types,pricing): add price set money amount rules to pricing module * chore: cleanup + add test cases for relationship update * chore: revert package json * chore: cleanup * initial * update pricing module service * update pricing-module integration tests * update pricing service interface * chore: update migrations * fix weird behavior * Apply suggestions from code review Co-authored-by: Riqwan Thamir * move rule-type to common * chore: delete duplicate migration files * fix(link-modules): Fix link module initialization (#4990) **What** Add a new configuration on the relationship to specify if the relation is consumed from an internal service (from medusa core). In that case do not check if the service is part of the loaded modules * initial price rule * rebase develop * save here * final changes to create * update price rule integration test * add module integraiton tests for price rules * fix merge * redo wierd order change * pr cleanup * pr cleanup * pr cleanup * update pr * sort out migrations * [wip] * wip * chore: temporarily emulate mikroorm internals * currency code hard filtering * before creating subqueries * chore: wip * chore: wip * chore: add exact match multiple contexts * chore: add one more test * chore: add query that works with exact match * chore: qb the thingy * chore: add some comments * chore: removed extra filter * chore: added some more comments + prettify * chore: test with carlos * chore: add fallbacks and exact match tests * chore: cleanup * feat(types,pricing): add price set money amount rules to pricing module (#5065) * initial * initial service * update pricing module service * add integration test for rule-type * update pricing-module integration tests * update pricing service interface * feat(pricing): PriceSets as entry point to pricing module * chore: add price set money amount * chore: add price set money amount * chore: change name of test * chore: added changeset * chore: use filterable props from money amount in price sets * chore: update migrations * test update integration test * fix weird behavior * Update packages/pricing/integration-tests/__fixtures__/rule-type/index.ts Co-authored-by: Riqwan Thamir * Apply suggestions from code review Co-authored-by: Riqwan Thamir * move rule-type to common * chore: reset migration * chore: remove incorrect conflicts * chore: address review * chore: remove ghost price list * Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * update id prefix * use persist not persistAndflush * rename key_value to rule_attribute * more renaming * feat(types,pricing): add price set money amount rules to pricing module * chore: cleanup + add test cases for relationship update * chore: revert package json * chore: cleanup --------- Co-authored-by: Philip Korsholm Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * chore: minor cleanup * chore: added money amount scoping * chore: added review comments * chore: update changset and undo test scoping * chore: introduce group by util + no queries on empty context * Feat/pricing module methods (#5218) chore: add removePrices to pricing module * Revert "Feat/pricing module methods (#5218)" (#5236) This reverts commit 95c8aaa66423d290a35b6e736e5b187e12d44a36. * chore: review changes * chore: update schema * chore: reset migration --------- Co-authored-by: Philip Korsholm Co-authored-by: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Adrien de Peretti --- .changeset/wicked-drinks-think.md | 7 + .../__fixtures__/currency/index.ts | 2 + .../__fixtures__/money-amount/index.ts | 2 + .../__fixtures__/price-rule/data.ts | 16 +- .../__fixtures__/price-rule/index.ts | 63 +- .../price-set-money-amount-rules/data.ts | 20 + .../price-set-money-amount-rules/index.ts | 22 + .../price-set-money-amount/data.ts | 23 + .../price-set-money-amount/index.ts | 22 + .../__fixtures__/price-set/index.ts | 2 + .../__fixtures__/rule-type/data.ts | 4 +- .../__fixtures__/rule-type/index.ts | 7 +- .../services/price-rule/index.spec.ts | 23 +- .../price-set-money-amonut-rules/index.ts | 300 ++++++++ .../pricing-module/calculate-price.spec.ts | 693 ++++++++++++++++++ .../pricing-module/price-rule.spec.ts | 25 +- .../price-set-money-amount-rules.spec.ts | 291 ++++++++ .../services/pricing-module/price-set.spec.ts | 142 +--- .../services/pricing-module/rule-type.spec.ts | 3 - .../{rule-type.ts => rule-type}/index.spec.ts | 20 +- packages/pricing/src/loaders/container.ts | 8 +- .../.snapshot-medusa-pricing-1.json | 150 ---- .../migrations/.snapshot-medusa-pricing.json | 131 +++- .../src/migrations/Migration20230913055746.ts | 23 - .../src/migrations/Migration20230913123118.ts | 52 -- .../src/migrations/Migration20230928154931.ts | 88 +++ packages/pricing/src/models/index.ts | 3 +- packages/pricing/src/models/price-rule.ts | 11 +- .../models/price-set-money-amount-rules.ts | 35 + .../src/models/price-set-money-amount.ts | 19 + packages/pricing/src/models/price-set.ts | 16 + packages/pricing/src/repositories/index.ts | 5 +- .../pricing/src/repositories/price-rule.ts | 11 +- .../price-set-money-amount-rules.ts | 127 ++++ packages/pricing/src/services/index.ts | 3 +- packages/pricing/src/services/money-amount.ts | 26 +- packages/pricing/src/services/price-rule.ts | 2 +- .../services/price-set-money-amount-rules.ts | 120 +++ packages/pricing/src/services/price-set.ts | 23 +- .../pricing/src/services/pricing-module.ts | 245 ++++++- packages/pricing/src/services/rule-type.ts | 23 +- packages/types/src/pricing/common/index.ts | 2 + .../common/price-set-money-amount-rules.ts | 31 + .../pricing/common/price-set-money-amount.ts | 9 + .../types/src/pricing/common/price-set.ts | 4 +- packages/types/src/pricing/service.ts | 42 +- .../src/common/__tests__/group-by.spec.ts | 50 ++ packages/utils/src/common/group-by.ts | 20 + packages/utils/src/common/index.ts | 1 + 49 files changed, 2414 insertions(+), 553 deletions(-) create mode 100644 .changeset/wicked-drinks-think.md create mode 100644 packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/data.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/index.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/price-set-money-amount/data.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/price-set-money-amount/index.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts rename packages/pricing/integration-tests/__tests__/services/{rule-type.ts => rule-type}/index.spec.ts (94%) delete mode 100644 packages/pricing/src/migrations/.snapshot-medusa-pricing-1.json delete mode 100644 packages/pricing/src/migrations/Migration20230913055746.ts delete mode 100644 packages/pricing/src/migrations/Migration20230913123118.ts create mode 100644 packages/pricing/src/migrations/Migration20230928154931.ts create mode 100644 packages/pricing/src/models/price-set-money-amount-rules.ts create mode 100644 packages/pricing/src/repositories/price-set-money-amount-rules.ts create mode 100644 packages/pricing/src/services/price-set-money-amount-rules.ts create mode 100644 packages/types/src/pricing/common/price-set-money-amount-rules.ts create mode 100644 packages/types/src/pricing/common/price-set-money-amount.ts create mode 100644 packages/utils/src/common/__tests__/group-by.spec.ts create mode 100644 packages/utils/src/common/group-by.ts diff --git a/.changeset/wicked-drinks-think.md b/.changeset/wicked-drinks-think.md new file mode 100644 index 0000000000..b60ffea385 --- /dev/null +++ b/.changeset/wicked-drinks-think.md @@ -0,0 +1,7 @@ +--- +"@medusajs/pricing": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(types,pricing,utils): Exact match based on context + fallback on rule priority if not diff --git a/packages/pricing/integration-tests/__fixtures__/currency/index.ts b/packages/pricing/integration-tests/__fixtures__/currency/index.ts index 3aa94e5f0e..7c9d6bbd76 100644 --- a/packages/pricing/integration-tests/__fixtures__/currency/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/currency/index.ts @@ -2,6 +2,8 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { Currency } from "@models" import { defaultCurrencyData } from "./data" +export * from "./data" + export async function createCurrencies( manager: SqlEntityManager, currencyData: any[] = defaultCurrencyData diff --git a/packages/pricing/integration-tests/__fixtures__/money-amount/index.ts b/packages/pricing/integration-tests/__fixtures__/money-amount/index.ts index dbf895c7cd..688fc183d6 100644 --- a/packages/pricing/integration-tests/__fixtures__/money-amount/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/money-amount/index.ts @@ -2,6 +2,8 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { MoneyAmount } from "@models" import { defaultMoneyAmountsData } from "./data" +export * from "./data" + export async function createMoneyAmounts( manager: SqlEntityManager, moneyAmountsData: any[] = defaultMoneyAmountsData diff --git a/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts b/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts index ba422fb1cc..d0ead194d2 100644 --- a/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts +++ b/packages/pricing/integration-tests/__fixtures__/price-rule/data.ts @@ -1,24 +1,22 @@ import { CreatePriceRuleDTO } from "@medusajs/types" +export * from "./data" + export const defaultPriceRuleData = [ { id: "price-rule-1", price_set_id: "price-set-1", rule_type_id: "rule-type-1", - value: "region_1", + value: "USD", price_list_id: "test", - price_set_money_amount: { - money_amount: { amount: 100, currency_code: "EUR" }, - }, + price_set_money_amount_id: "price-set-money-amount-USD", }, { id: "price-rule-2", price_set_id: "price-set-2", - rule_type_id: "rule-type-1", - value: "region_2", + rule_type_id: "rule-type-2", + value: "region_1", price_list_id: "test", - price_set_money_amount: { - money_amount: { amount: 100, currency_code: "EUR" }, - }, + price_set_money_amount_id: "price-set-money-amount-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 index 3bc2849584..6642f27acd 100644 --- a/packages/pricing/integration-tests/__fixtures__/price-rule/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/price-rule/index.ts @@ -1,11 +1,11 @@ -import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models" +import { PriceRule } 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 * from "./data" + export async function createPriceRules( manager: SqlEntityManager, pricesRulesData: CreatePriceRuleDTO[] = defaultPriceRuleData @@ -13,66 +13,17 @@ export async function createPriceRules( const priceRules: PriceRule[] = [] for (let priceRuleData of pricesRulesData) { - const priceRuleDataClone = { ...priceRuleData } + const priceRuleDataClone: any = { ...priceRuleData } - if (priceRuleDataClone.price_set_id) { - priceRuleDataClone.price_set = manager.getReference( - PriceSet, - priceRuleDataClone.price_set_id - ) - } + priceRuleDataClone.price_set = 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 - ) + priceRuleDataClone.rule_type = priceRuleDataClone.rule_type_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 - ) + priceRuleDataClone.price_set_money_amount = priceSetMoneyAmountId const priceRule = manager.create(PriceRule, priceRuleDataClone) diff --git a/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/data.ts b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/data.ts new file mode 100644 index 0000000000..daa4212543 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/data.ts @@ -0,0 +1,20 @@ +export const defaultPriceSetMoneyAmountRulesData = [ + { + id: "psmar-1", + value: "EUR", + price_set_money_amount: "price-set-money-amount-USD", + rule_type: "rule-type-1", + }, + { + id: "psmar-2", + value: "EU", + price_set_money_amount: "price-set-money-amount-EUR", + rule_type: "rule-type-2", + }, + { + id: "psmar-3", + value: "CAD", + price_set_money_amount: "price-set-money-amount-CAD", + rule_type: "rule-type-2", + }, +] diff --git a/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/index.ts b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/index.ts new file mode 100644 index 0000000000..9d5ea516df --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount-rules/index.ts @@ -0,0 +1,22 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSetMoneyAmountRules } from "@models" +import { defaultPriceSetMoneyAmountRulesData } from "./data" + +export * from "./data" + +export async function createPriceSetMoneyAmountRules( + manager: SqlEntityManager, + psmarData: any[] = defaultPriceSetMoneyAmountRulesData +): Promise { + const priceSetMoneyAmountRules: PriceSetMoneyAmountRules[] = [] + + for (let data of psmarData) { + const psmar = manager.create(PriceSetMoneyAmountRules, data) + + priceSetMoneyAmountRules.push(psmar) + } + + await manager.persistAndFlush(priceSetMoneyAmountRules) + + return priceSetMoneyAmountRules +} diff --git a/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/data.ts b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/data.ts new file mode 100644 index 0000000000..1fa5d67ece --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/data.ts @@ -0,0 +1,23 @@ +export const defaultPriceSetMoneyAmountsData = [ + { + id: "price-set-money-amount-USD", + title: "price set money amount USD", + price_set: "price-set-1", + money_amount: "money-amount-USD", + number_rules: 1, + }, + { + id: "price-set-money-amount-EUR", + title: "price set money amount EUR", + price_set: "price-set-2", + money_amount: "money-amount-EUR", + number_rules: 1, + }, + { + id: "price-set-money-amount-CAD", + title: "price set money amount CAD", + price_set: "price-set-3", + money_amount: "money-amount-CAD", + number_rules: 1, + }, +] diff --git a/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/index.ts b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/index.ts new file mode 100644 index 0000000000..bafc101a33 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set-money-amount/index.ts @@ -0,0 +1,22 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSetMoneyAmount } from "@models" +import { defaultPriceSetMoneyAmountsData } from "./data" + +export * from "./data" + +export async function createPriceSetMoneyAmounts( + manager: SqlEntityManager, + psmaData: any[] = defaultPriceSetMoneyAmountsData +): Promise { + const priceSetMoneyAmount: PriceSetMoneyAmount[] = [] + + for (let data of psmaData) { + const psmar = manager.create(PriceSetMoneyAmount, data) + + priceSetMoneyAmount.push(psmar) + } + + await manager.persistAndFlush(priceSetMoneyAmount) + + return priceSetMoneyAmount +} diff --git a/packages/pricing/integration-tests/__fixtures__/price-set/index.ts b/packages/pricing/integration-tests/__fixtures__/price-set/index.ts index 656c42d865..5ffdff4a8d 100644 --- a/packages/pricing/integration-tests/__fixtures__/price-set/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/price-set/index.ts @@ -3,6 +3,8 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { PriceSet, PriceSetMoneyAmount } from "@models" import { defaultPriceSetsData } from "./data" +export * from "./data" + export async function createPriceSets( manager: SqlEntityManager, priceSetsData: CreatePriceSetDTO[] = defaultPriceSetsData diff --git a/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts b/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts index 52fb0664e0..fbfe6b62d4 100644 --- a/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts +++ b/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts @@ -2,11 +2,11 @@ export const defaultRuleTypesData = [ { id: "rule-type-1", name: "rule 1", - rule_attribute: "region_id", + rule_attribute: "currency_code", }, { id: "rule-type-2", name: "rule 2", - rule_attribute: "currency_code", + rule_attribute: "region_id", }, ] diff --git a/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts index 7c6b3d7b50..837ae8f3e0 100644 --- a/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts +++ b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts @@ -1,7 +1,9 @@ -import { RuleType } from "@models" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { RuleType } from "@models" import { defaultRuleTypesData } from "./data" +export * from "./data" + export async function createRuleTypes( manager: SqlEntityManager, ruletypesData: any[] = defaultRuleTypesData @@ -11,9 +13,10 @@ export async function createRuleTypes( for (let ruleTypeData of ruletypesData) { const ruleType = manager.create(RuleType, ruleTypeData) - await manager.persistAndFlush(ruleType) ruleTypes.push(ruleType) } + await manager.persistAndFlush(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 index f31aae1b5d..f350895ca1 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts @@ -1,15 +1,16 @@ -import { PriceSet, PriceSetMoneyAmount } from "@models" +import { PriceSetMoneyAmount } from "@models" import { CreatePriceRuleDTO } from "@medusajs/types" -import { MikroOrmWrapper } from "../../../utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" 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 { createPriceSetMoneyAmounts } from "../../../__fixtures__/price-set-money-amount" import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) @@ -32,9 +33,10 @@ describe("PriceRule Service", () => { }) await createCurrencies(testManager) - + await createMoneyAmounts(testManager) await createRuleTypes(testManager) await createPriceSets(testManager) + await createPriceSetMoneyAmounts(testManager) await createPriceRules(testManager) }) @@ -291,11 +293,14 @@ describe("PriceRule Service", () => { }, ]) - const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, { - price_set: testManager.getReference(PriceSet, "price-set-1"), - money_amount: ma.id, - title: "test", - }) + const psma: PriceSetMoneyAmount = testManager.create( + PriceSetMoneyAmount, + { + price_set: "price-set-1", + money_amount: ma.id, + title: "test", + } + ) await testManager.persist(psma).flush() diff --git a/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.ts b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.ts new file mode 100644 index 0000000000..c4ee8c78ae --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.ts @@ -0,0 +1,300 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { PriceSetMoneyAmountRulesRepository } from "@repositories" +import { PriceSetMoneyAmountRulesService } from "@services" + +import { createCurrencies } from "../../../__fixtures__/currency" +import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { createPriceSetMoneyAmounts } from "../../../__fixtures__/price-set-money-amount" +import { createPriceSetMoneyAmountRules } from "../../../__fixtures__/price-set-money-amount-rules" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { MikroOrmWrapper } from "../../../utils" + +jest.setTimeout(30000) + +describe("PriceSetMoneyAmountRules Service", () => { + let service: PriceSetMoneyAmountRulesService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + + const priceSetMoneyAmountRulesRepository = + new PriceSetMoneyAmountRulesRepository({ + manager: repositoryManager, + }) + + service = new PriceSetMoneyAmountRulesService({ + priceSetMoneyAmountRulesRepository, + }) + + testManager = await MikroOrmWrapper.forkManager() + + await createCurrencies(testManager) + await createMoneyAmounts(testManager) + await createPriceSets(testManager) + await createRuleTypes(testManager) + await createPriceSetMoneyAmounts(testManager) + await createPriceSetMoneyAmountRules(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("list", () => { + it("should list psmar records", async () => { + const priceSetMoneyAmountRulesResult = await service.list() + + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + expect.objectContaining({ + id: "psmar-2", + }), + expect.objectContaining({ + id: "psmar-3", + }), + ]) + }) + + it("should list psmar record by id", async () => { + const priceSetMoneyAmountRulesResult = await service.list({ + id: ["psmar-1"], + }) + + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + ]) + }) + }) + + describe("listAndCount", () => { + it("should return psmar records and count", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCount() + + expect(count).toEqual(3) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + expect.objectContaining({ + id: "psmar-2", + }), + expect.objectContaining({ + id: "psmar-3", + }), + ]) + }) + + it("should return psmar records and count when filtered", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCount({ + id: ["psmar-1"], + }) + + expect(count).toEqual(1) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + ]) + }) + + it("should return psmar and count when using skip and take", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCount({}, { skip: 1, take: 1 }) + + expect(count).toEqual(3) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCount( + {}, + { + take: 1, + select: ["value"], + } + ) + + const serialized = JSON.parse( + JSON.stringify(priceSetMoneyAmountRulesResult) + ) + + expect(count).toEqual(3) + expect(serialized).toEqual([ + { + id: "psmar-1", + value: "EUR", + }, + ]) + }) + }) + + describe("retrieve", () => { + it("should return priceSetMoneyAmountRules for the given id", async () => { + const priceSetMoneyAmountRules = await service.retrieve("psmar-1") + + expect(priceSetMoneyAmountRules).toEqual( + expect.objectContaining({ + id: "psmar-1", + }) + ) + }) + + it("should throw an error when priceSetMoneyAmountRules with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceSetMoneyAmountRules with id: does-not-exist was not found" + ) + }) + + it("should throw an error when an id is not provided", async () => { + let error + + try { + await service.retrieve(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + '"priceSetMoneyAmountRulesId" must be defined' + ) + }) + + it("should return priceSetMoneyAmountRules based on config select param", async () => { + const priceSetMoneyAmountRulesResult = await service.retrieve("psmar-1", { + select: ["value"], + }) + + const serialized = JSON.parse( + JSON.stringify(priceSetMoneyAmountRulesResult) + ) + + expect(serialized).toEqual({ + value: "EUR", + id: "psmar-1", + }) + }) + }) + + describe("delete", () => { + const id = "psmar-1" + + it("should delete the priceSetMoneyAmountRules given an id successfully", async () => { + await service.delete([id]) + + const priceSetMoneyAmountRules = await service.list({ + id: [id], + }) + + expect(priceSetMoneyAmountRules).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "psmar-1" + + it("should update the value of the priceSetMoneyAmountRules successfully", async () => { + await service.update([ + { + id, + value: "New value", + price_set_money_amount: "price-set-money-amount-CAD", + rule_type: "rule-type-2", + }, + ]) + + const psmar = await service.retrieve(id, { + relations: ["price_set_money_amount", "rule_type"], + }) + + expect(psmar).toEqual( + expect.objectContaining({ + id, + value: "New value", + price_set_money_amount: expect.objectContaining({ + id: "price-set-money-amount-CAD", + }), + rule_type: expect.objectContaining({ + id: "rule-type-2", + }), + }) + ) + }) + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + value: "random value", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceSetMoneyAmountRules with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should create a priceSetMoneyAmountRules successfully", async () => { + const created = await service.create([ + { + price_set_money_amount: "price-set-money-amount-EUR", + rule_type: "rule-type-2", + value: "New priceSetMoneyAmountRule", + }, + ]) + + const [priceSetMoneyAmountRules] = await service.list( + { + id: [created[0]?.id], + }, + { + relations: ["price_set_money_amount", "rule_type"], + } + ) + + expect(priceSetMoneyAmountRules).toEqual( + expect.objectContaining({ + id: created[0]?.id, + value: "New priceSetMoneyAmountRule", + price_set_money_amount: expect.objectContaining({ + id: "price-set-money-amount-EUR", + }), + rule_type: expect.objectContaining({ + id: "rule-type-2", + }), + }) + ) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts new file mode 100644 index 0000000000..75a3b0aea3 --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -0,0 +1,693 @@ +import { + CreatePriceRuleDTO, + CreatePriceSetDTO, + IPricingModuleService, +} from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSet } from "@models" + +import { initialize } from "../../../../src" +import { + createCurrencies, + defaultCurrencyData, +} from "../../../__fixtures__/currency" +import { + createMoneyAmounts, + defaultMoneyAmountsData, +} from "../../../__fixtures__/money-amount" +import { + createPriceRules, + defaultPriceRuleData, +} from "../../../__fixtures__/price-rule" +import { + createPriceSets, + defaultPriceSetsData, +} from "../../../__fixtures__/price-set" +import { + createPriceSetMoneyAmounts, + defaultPriceSetMoneyAmountsData, +} from "../../../__fixtures__/price-set-money-amount" + +import { + createRuleTypes, + defaultRuleTypesData, +} from "../../../__fixtures__/rule-type" +import { DB_URL, MikroOrmWrapper } from "../../../utils" + +jest.setTimeout(30000) + +async function seedData({ + moneyAmountsData = defaultMoneyAmountsData, + priceSetsData = defaultPriceSetsData, + currencyData = defaultCurrencyData, + priceRuleData = defaultPriceRuleData, + priceSetMoneyAmountsData = defaultPriceSetMoneyAmountsData, + ruleTypesData = defaultRuleTypesData, +} = {}) { + const testManager = MikroOrmWrapper.forkManager() + + await createCurrencies(testManager, currencyData) + await createMoneyAmounts(testManager, moneyAmountsData) + await createPriceSets(testManager, priceSetsData) + await createPriceSetMoneyAmounts(testManager, priceSetMoneyAmountsData) + await createRuleTypes(testManager, ruleTypesData) + await createPriceRules(testManager, priceRuleData) +} + +describe("PricingModule Service - Calculate Price", () => { + let service: IPricingModuleService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let data!: PriceSet[] + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = MikroOrmWrapper.forkManager() + testManager = MikroOrmWrapper.forkManager() + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRICING_DB_SCHEMA, + }, + }) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("calculatePrices", () => { + beforeEach(async () => { + const currencyData = [ + { + symbol: "zł", + name: "Polish Zloty", + symbol_native: "zł", + code: "PLN", + }, + { + symbol: "€", + name: "Euro", + symbol_native: "€", + code: "EUR", + }, + ] + + const moneyAmountsData = [ + { + id: "money-amount-currency_code-EUR", + currency_code: "EUR", + amount: 500, + min_quantity: 1, + max_quantity: 10, + }, + { + id: "money-amount-currency_code-PLN", + currency_code: "PLN", + amount: 400, + min_quantity: 1, + max_quantity: 5, + }, + { + id: "money-amount-region_id-PLN", + currency_code: "PLN", + amount: 300, + min_quantity: 1, + max_quantity: 4, + }, + { + id: "money-amount-region_id-PL-EUR", + currency_code: "EUR", + amount: 200, + min_quantity: 1, + max_quantity: 3, + }, + { + id: "money-amount-region_id-PL-EUR-4-qty", + currency_code: "EUR", + amount: 50, + min_quantity: 4, + max_quantity: 10, + }, + { + id: "money-amount-region_id-PL-EUR-customer-group", + currency_code: "EUR", + amount: 100, + min_quantity: 1, + max_quantity: 3, + }, + ] + + const priceSetsData = [ + { + id: "price-set-EUR", + }, + { + id: "price-set-PLN", + }, + ] as unknown as CreatePriceSetDTO[] + + const priceSetMoneyAmountsData = [ + { + id: "psma-currency_code-EUR", + title: "psma EUR - currency_code", + price_set: "price-set-EUR", + money_amount: "money-amount-currency_code-EUR", + number_rules: 1, + }, + { + id: "psma-currency_code-PLN", + title: "psma PLN - currency_code", + price_set: "price-set-PLN", + money_amount: "money-amount-currency_code-PLN", + number_rules: 1, + }, + { + id: "psma-region_id-PLN", + title: "psma PLN - region_id", + price_set: "price-set-PLN", + money_amount: "money-amount-region_id-PLN", + number_rules: 1, + }, + { + id: "psma-region_id_currency_code-PL-EUR", + title: "psma PLN - region_id PL with EUR currency", + price_set: "price-set-PLN", + money_amount: "money-amount-region_id-PL-EUR", + number_rules: 2, + }, + { + id: "psma-region_id_currency_code-PL-EUR-4-qty", + title: "psma PLN - region_id PL with EUR currency for quantity 4", + price_set: "price-set-PLN", + money_amount: "money-amount-region_id-PL-EUR-4-qty", + number_rules: 2, + }, + { + id: "psma-region_id_currency_code-PL-EUR-customer-group", + title: "psma PLN - region_id PL with EUR currency for customer group", + price_set: "price-set-PLN", + money_amount: "money-amount-region_id-PL-EUR-customer-group", + number_rules: 3, + }, + ] + + const ruleTypesData = [ + { + id: "rule-type-currency_code", + name: "rule type currency code", + rule_attribute: "currency_code", + default_priority: 2, + }, + { + id: "rule-type-region_id", + name: "rule type region id", + rule_attribute: "region_id", + default_priority: 1, + }, + { + id: "rule-type-customer_group_id", + name: "rule type customer group id", + rule_attribute: "customer_group_id", + default_priority: 3, + }, + ] + + const priceRuleData = [ + { + id: "price-rule-currency_code-EUR", + price_set_id: "price-set-EUR", + rule_type_id: "rule-type-currency_code", + value: "EUR", + price_list_id: "test", + price_set_money_amount_id: "psma-currency_code-EUR", + }, + { + id: "price-rule-currency_code-PLN", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-currency_code", + value: "PLN", + price_list_id: "test", + price_set_money_amount_id: "psma-currency_code-PLN", + }, + { + id: "price-rule-region_id-PLN", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-region_id", + value: "PL", + price_list_id: "test", + price_set_money_amount_id: "psma-region_id-PLN", + }, + { + id: "price-rule-region_id-currency_code-PL", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-region_id", + value: "PL", + price_list_id: "test", + price_set_money_amount_id: "psma-region_id_currency_code-PL-EUR", + }, + { + id: "price-rule-region_id-currency_code-PLN", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-currency_code", + value: "EUR", + price_list_id: "test", + price_set_money_amount_id: "psma-region_id_currency_code-PL-EUR", + }, + { + id: "price-rule-region_id-currency_code-PL-4-qty", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-region_id", + value: "PL", + price_list_id: "test", + price_set_money_amount_id: + "psma-region_id_currency_code-PL-EUR-4-qty", + }, + { + id: "price-rule-region_id-currency_code-PLN-4-qty", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-currency_code", + value: "EUR", + price_list_id: "test", + price_set_money_amount_id: + "psma-region_id_currency_code-PL-EUR-4-qty", + }, + { + id: "price-rule-region_id-currency_customer_group_code-PL", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-region_id", + value: "PL", + price_list_id: "test", + price_set_money_amount_id: + "psma-region_id_currency_code-PL-EUR-customer-group", + }, + { + id: "price-rule-region_id-currency_customer_group_code-PLN", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-currency_code", + value: "EUR", + price_list_id: "test", + price_set_money_amount_id: + "psma-region_id_currency_code-PL-EUR-customer-group", + }, + { + id: "price-rule-region_id-currency_customer_group_code-test_customer_group", + price_set_id: "price-set-PLN", + rule_type_id: "rule-type-customer_group_id", + value: "test-customer-group", + price_list_id: "test", + price_set_money_amount_id: + "psma-region_id_currency_code-PL-EUR-customer-group", + }, + ] as unknown as CreatePriceRuleDTO[] + + await seedData({ + currencyData, + moneyAmountsData, + priceSetsData, + priceSetMoneyAmountsData, + priceRuleData, + ruleTypesData, + }) + }) + + it("should return null values when no context is set", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + {} + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + + it("should return filled prices when 1 context is present and price is setup for EUR", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { currency_code: "EUR" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + + it("should return filled prices when 1 context is present and price is setup for PLN region_id", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { region_id: "PL" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + { + id: "price-set-PLN", + amount: "300", + currency_code: "PLN", + min_quantity: "1", + max_quantity: "4", + }, + ]) + }) + + it("should return filled prices when 1 context is present and price is setup for PLN currency_code", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { currency_code: "PLN" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + { + id: "price-set-PLN", + amount: "400", + currency_code: "PLN", + min_quantity: "1", + max_quantity: "5", + }, + ]) + }) + + it("should return null prices when 1 context is present and price is NOT setup", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { does_not_exist: "EUR" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + + it("should return filled prices when 2 contexts are present and price is setup", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { currency_code: "EUR", region_id: "PL" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + { + id: "price-set-PLN", + amount: "200", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "3", + }, + ]) + }) + + it("should return filled prices when 2 contexts are present and price is setup along with declaring quantity", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-PLN"] }, + { + context: { currency_code: "EUR", region_id: "PL", quantity: 5 }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-PLN", + amount: "50", + currency_code: "EUR", + min_quantity: "4", + max_quantity: "10", + }, + ]) + }) + + it("should return filled prices when 3 contexts are present and price is partially setup for 2", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + customer_group_id: "test", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + { + id: "price-set-PLN", + amount: "200", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "3", + }, + ]) + }) + + it("should return filled prices when 3 contexts are present and price is setup for 3", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + customer_group_id: "test-customer-group", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + // Currency Code + Region value + customer group id + { + id: "price-set-PLN", + amount: "100", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "3", + }, + ]) + }) + + it("should return filled prices when 3 contexts are present and price is setup for 3, but value is incorrect for 1. It falls back to 2 rule context", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + customer_group_id: "does-not-exist", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + // Currency Code + Region value + { + id: "price-set-PLN", + amount: "200", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "3", + }, + ]) + }) + + it("should return filled prices when 3 contexts are present and price is setup for 3, but value is incorrect for 2. It falls back to 1 rule context when 1 rule is not setup", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "does-not-exist", + region_id: "PL", + customer_group_id: "does-not-exist", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + // PLN price set is not setup for EUR currency_code so it will default to a null set + { + id: "price-set-PLN", + amount: "300", + currency_code: "PLN", + min_quantity: "1", + max_quantity: "4", + }, + ]) + }) + + it("should return filled prices when 3 contexts are present and price is setup for 3, but value is incorrect for 2. It falls back to 1 rule context when 1 rule is setup", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "EUR", + region_id: "does-not-exist", + customer_group_id: "does-not-exist", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + // PLN price set is not setup for EUR currency_code so it will default to a null set + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + + it("should return null prices when 2 context is present and prices are NOT setup", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { does_not_exist: "EUR", does_not_exist_2: "Berlin" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + + it("should return filled prices when 2 context is present and prices are setup, but only for one of the contexts", async () => { + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { currency_code: "EUR", does_not_exist: "Berlin" }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + amount: "500", + currency_code: "EUR", + min_quantity: "1", + max_quantity: "10", + }, + { + id: "price-set-PLN", + amount: null, + currency_code: null, + min_quantity: null, + max_quantity: null, + }, + ]) + }) + }) +}) 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 index 87228d45b4..9e781e2333 100644 --- 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 @@ -1,21 +1,20 @@ 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" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { createPriceSetMoneyAmounts } from "../../../__fixtures__/price-set-money-amount" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { DB_URL, MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) describe("PricingModule Service - PriceRule", () => { let service: IPricingModuleService let testManager: SqlEntityManager - beforeEach(async () => { await MikroOrmWrapper.setupDatabase() @@ -29,9 +28,10 @@ describe("PricingModule Service - PriceRule", () => { }) await createCurrencies(testManager) - + await createMoneyAmounts(testManager) await createRuleTypes(testManager) await createPriceSets(testManager) + await createPriceSetMoneyAmounts(testManager) await createPriceRules(testManager) }) @@ -288,11 +288,14 @@ describe("PricingModule Service - PriceRule", () => { }, ]) - const psma: PriceSetMoneyAmount = testManager.create(PriceSetMoneyAmount, { - price_set: testManager.getReference(PriceSet, "price-set-1"), - money_amount: ma.id, - title: "test", - }) + const psma: PriceSetMoneyAmount = testManager.create( + PriceSetMoneyAmount, + { + price_set: "price-set-1", + money_amount: ma.id, + title: "test", + } + ) await testManager.persist(psma).flush() diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts new file mode 100644 index 0000000000..f8aeafed18 --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts @@ -0,0 +1,291 @@ +import { IPricingModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { initialize } from "../../../../src" +import { createCurrencies } from "../../../__fixtures__/currency" +import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { createPriceSetMoneyAmounts } from "../../../__fixtures__/price-set-money-amount" +import { createPriceSetMoneyAmountRules } from "../../../__fixtures__/price-set-money-amount-rules" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { DB_URL, MikroOrmWrapper } from "../../../utils" + +jest.setTimeout(30000) + +describe("PricingModule Service - PriceSetMoneyAmountRules", () => { + let service: IPricingModuleService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRICING_DB_SCHEMA, + }, + }) + + testManager = await MikroOrmWrapper.forkManager() + + await createCurrencies(testManager) + await createMoneyAmounts(testManager) + await createPriceSets(testManager) + await createRuleTypes(testManager) + await createPriceSetMoneyAmounts(testManager) + await createPriceSetMoneyAmountRules(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("listPriceSetMoneyAmountRules", () => { + it("should list psmar records", async () => { + const priceSetMoneyAmountRulesResult = + await service.listPriceSetMoneyAmountRules() + + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + expect.objectContaining({ + id: "psmar-2", + }), + expect.objectContaining({ + id: "psmar-3", + }), + ]) + }) + + it("should list psmar record by id", async () => { + const priceSetMoneyAmountRulesResult = + await service.listPriceSetMoneyAmountRules({ + id: ["psmar-1"], + }) + + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + ]) + }) + }) + + describe("listAndCount", () => { + it("should return psmar records and count", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCountPriceSetMoneyAmountRules() + + expect(count).toEqual(3) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + expect.objectContaining({ + id: "psmar-2", + }), + expect.objectContaining({ + id: "psmar-3", + }), + ]) + }) + + it("should return psmar records and count when filtered", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCountPriceSetMoneyAmountRules({ + id: ["psmar-1"], + }) + + expect(count).toEqual(1) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-1", + }), + ]) + }) + + it("should return psmar and count when using skip and take", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCountPriceSetMoneyAmountRules( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(3) + expect(priceSetMoneyAmountRulesResult).toEqual([ + expect.objectContaining({ + id: "psmar-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [priceSetMoneyAmountRulesResult, count] = + await service.listAndCountPriceSetMoneyAmountRules( + {}, + { + take: 1, + select: ["value"], + } + ) + + const serialized = JSON.parse( + JSON.stringify(priceSetMoneyAmountRulesResult) + ) + + expect(count).toEqual(3) + expect(serialized).toEqual([ + { + id: "psmar-1", + value: "EUR", + }, + ]) + }) + }) + + describe("retrievePriceSetMoneyAmountRules", () => { + it("should return priceSetMoneyAmountRules for the given id", async () => { + const priceSetMoneyAmountRules = + await service.retrievePriceSetMoneyAmountRules("psmar-1") + + expect(priceSetMoneyAmountRules).toEqual( + expect.objectContaining({ + id: "psmar-1", + }) + ) + }) + + it("should throw an error when priceSetMoneyAmountRules with id does not exist", async () => { + let error + + try { + await service.retrievePriceSetMoneyAmountRules("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceSetMoneyAmountRules with id: does-not-exist was not found" + ) + }) + + it("should throw an error when an id is not provided", async () => { + let error + + try { + await service.retrievePriceSetMoneyAmountRules( + undefined as unknown as string + ) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + '"priceSetMoneyAmountRulesId" must be defined' + ) + }) + + it("should return priceSetMoneyAmountRules based on config select param", async () => { + const priceSetMoneyAmountRulesResult = + await service.retrievePriceSetMoneyAmountRules("psmar-1", { + select: ["value"], + }) + + const serialized = JSON.parse( + JSON.stringify(priceSetMoneyAmountRulesResult) + ) + + expect(serialized).toEqual({ + value: "EUR", + id: "psmar-1", + }) + }) + }) + + describe("deletePriceSetMoneyAmountRules", () => { + const id = "psmar-1" + + it("should delete the priceSetMoneyAmountRuless given an id successfully", async () => { + await service.deletePriceSetMoneyAmountRules([id]) + + const currencies = await service.listPriceSetMoneyAmountRules({ + id: [id], + }) + + expect(currencies).toHaveLength(0) + }) + }) + + describe("updatePriceSetMoneyAmountRules", () => { + const id = "psmar-1" + + it("should update the value of the priceSetMoneyAmountRules successfully", async () => { + await service.updatePriceSetMoneyAmountRules([ + { + id, + value: "New value", + }, + ]) + + const psmar = await service.retrievePriceSetMoneyAmountRules(id) + + expect(psmar.value).toEqual("New value") + }) + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.updatePriceSetMoneyAmountRules([ + { + id: "does-not-exist", + value: "random value", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceSetMoneyAmountRules with id "does-not-exist" not found' + ) + }) + }) + + describe("createPriceSetMoneyAmountRules", () => { + it("should create a priceSetMoneyAmountRules successfully", async () => { + const created = await service.createPriceSetMoneyAmountRules([ + { + price_set_money_amount: "price-set-money-amount-EUR", + rule_type: "rule-type-2", + value: "New priceSetMoneyAmountRule", + }, + ]) + + const [priceSetMoneyAmountRules] = + await service.listPriceSetMoneyAmountRules( + { + id: [created[0]?.id], + }, + { + relations: ["price_set_money_amount", "rule_type"], + } + ) + + expect(priceSetMoneyAmountRules).toEqual( + expect.objectContaining({ + id: created[0]?.id, + value: "New priceSetMoneyAmountRule", + price_set_money_amount: expect.objectContaining({ + id: "price-set-money-amount-EUR", + }), + rule_type: expect.objectContaining({ + id: "rule-type-2", + }), + }) + ) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts index 0ef870fc35..808e062079 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -4,144 +4,62 @@ import { PriceSet } from "@models" import { initialize } from "../../../../src" import { createCurrencies } from "../../../__fixtures__/currency" -import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { + createMoneyAmounts, + defaultMoneyAmountsData, +} from "../../../__fixtures__/money-amount" +import { + createPriceRules, + defaultPriceRuleData, +} from "../../../__fixtures__/price-rule" import { createPriceSets } from "../../../__fixtures__/price-set" +import { + createPriceSetMoneyAmounts, + defaultPriceSetMoneyAmountsData, +} from "../../../__fixtures__/price-set-money-amount" +import { createRuleTypes } from "../../../__fixtures__/rule-type" import { DB_URL, MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) +async function seedData({ + moneyAmountsData = defaultMoneyAmountsData, + priceRuleData = defaultPriceRuleData, + priceSetMoneyAmountsData = defaultPriceSetMoneyAmountsData, +} = {}) { + const testManager = MikroOrmWrapper.forkManager() + + await createCurrencies(testManager) + await createMoneyAmounts(testManager, moneyAmountsData) + await createPriceSets(testManager) + await createPriceSetMoneyAmounts(testManager, priceSetMoneyAmountsData) + await createRuleTypes(testManager) + await createPriceRules(testManager, priceRuleData) +} + describe("PricingModule Service - PriceSet", () => { let service: IPricingModuleService let testManager: SqlEntityManager let repositoryManager: SqlEntityManager let data!: PriceSet[] - const moneyAmountsInputData = [ - { - id: "money-amount-USD", - currency_code: "USD", - amount: 500, - min_quantity: 1, - max_quantity: 10, - }, - { - id: "money-amount-EUR", - currency_code: "EUR", - amount: 500, - min_quantity: 1, - max_quantity: 10, - }, - ] - - const priceSetInputData = [ - { - id: "price-set-1", - money_amounts: [{ id: "money-amount-USD" }], - }, - { - id: "price-set-2", - money_amounts: [{ id: "money-amount-EUR" }], - }, - { - id: "price-set-3", - money_amounts: [], - }, - ] - beforeEach(async () => { await MikroOrmWrapper.setupDatabase() repositoryManager = MikroOrmWrapper.forkManager() - service = await initialize({ database: { clientUrl: DB_URL, schema: process.env.MEDUSA_PRICING_DB_SCHEMA, }, }) - - testManager = MikroOrmWrapper.forkManager() - await createCurrencies(testManager) - await createMoneyAmounts(testManager, moneyAmountsInputData) - data = await createPriceSets(testManager, priceSetInputData) }) + beforeEach(async () => await seedData()) + afterEach(async () => { await MikroOrmWrapper.clearDatabase() }) - describe("calculatePrices", () => { - it("retrieves the calculated prices when no context is set", async () => { - const priceSetsResult = await service.calculatePrices( - { id: ["price-set-1", "price-set-2"] }, - {} - ) - - expect(priceSetsResult).toEqual([ - { - id: "price-set-1", - amount: null, - currency_code: null, - min_quantity: null, - max_quantity: null, - }, - { - id: "price-set-2", - amount: null, - currency_code: null, - min_quantity: null, - max_quantity: null, - }, - ]) - }) - - it("retrieves the calculated prices when a context is set", async () => { - const priceSetsResult = await service.calculatePrices( - { id: ["price-set-1", "price-set-2"] }, - { - context: { - currency_code: "USD", - }, - } - ) - - expect(priceSetsResult).toEqual([ - { - id: "price-set-1", - amount: "500", - currency_code: "USD", - min_quantity: "1", - max_quantity: "10", - }, - { - id: "price-set-2", - amount: null, - currency_code: null, - min_quantity: null, - max_quantity: null, - }, - ]) - }) - - it("retrieves the calculated prices only when id exists in the database", async () => { - const priceSetsResult = await service.calculatePrices( - { id: ["price-set-doesnotexist", "price-set-1"] }, - { - context: { currency_code: "USD" }, - } - ) - - expect(priceSetsResult).toEqual([ - { - id: "price-set-1", - amount: "500", - currency_code: "USD", - min_quantity: "1", - max_quantity: "10", - }, - ]) - }) - }) - describe("list", () => { it("list priceSets", async () => { const priceSetsResult = await service.list() 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 89f977c0bb..04b5d763d3 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,6 +1,3 @@ -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/integration-tests/__tests__/services/rule-type.ts/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts similarity index 94% rename from packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts rename to packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts index c6f8f21dab..2d1e3be1c2 100644 --- a/packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts @@ -3,8 +3,8 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { RuleTypeRepository } from "@repositories" import { RuleTypeService } from "@services" -import { MikroOrmWrapper } from "../../../utils" import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) @@ -13,7 +13,6 @@ describe("RuleType Service", () => { let testManager: SqlEntityManager let repositoryManager: SqlEntityManager - beforeEach(async () => { await MikroOrmWrapper.setupDatabase() repositoryManager = await MikroOrmWrapper.forkManager() @@ -131,9 +130,8 @@ describe("RuleType Service", () => { }) describe("retrieve", () => { - it("should return ruleType for the given id", async () => { - const ruleType = await service.retrieve('rule-type-1') + const ruleType = await service.retrieve("rule-type-1") expect(ruleType).toEqual( expect.objectContaining({ @@ -170,15 +168,15 @@ describe("RuleType Service", () => { }) it("should return ruleType based on config select param", async () => { - const ruleTypeResult = await service.retrieve('rule-type-1', { + const ruleTypeResult = await service.retrieve("rule-type-1", { select: ["name"], }) const serialized = JSON.parse(JSON.stringify(ruleTypeResult)) expect(serialized).toEqual({ - name: 'rule 1', - id: 'rule-type-1' + name: "rule 1", + id: "rule-type-1", }) }) }) @@ -189,11 +187,11 @@ describe("RuleType Service", () => { it("should delete the ruleTypes given an id successfully", async () => { await service.delete([id]) - const currencies = await service.list({ + const ruleTypes = await service.list({ id: [id], }) - expect(currencies).toHaveLength(0) + expect(ruleTypes).toHaveLength(0) }) }) @@ -238,7 +236,7 @@ describe("RuleType Service", () => { await service.create([ { name: "Test Rule", - rule_attribute: 'region_id', + rule_attribute: "region_id", }, ]) @@ -249,7 +247,7 @@ describe("RuleType Service", () => { expect(ruleType).toEqual( expect.objectContaining({ name: "Test Rule", - rule_attribute: 'region_id', + rule_attribute: "region_id", }) ) }) diff --git a/packages/pricing/src/loaders/container.ts b/packages/pricing/src/loaders/container.ts index a94f1f8a98..631b0e7d2a 100644 --- a/packages/pricing/src/loaders/container.ts +++ b/packages/pricing/src/loaders/container.ts @@ -3,8 +3,8 @@ import * as defaultServices from "@services" import { LoaderOptions } from "@medusajs/modules-sdk" import { ModulesSdkTypes } from "@medusajs/types" -import { asClass } from "awilix" import { loadCustomRepositories } from "@medusajs/utils" +import { asClass } from "awilix" export default async ({ container, @@ -22,6 +22,9 @@ export default async ({ moneyAmountService: asClass(defaultServices.MoneyAmountService).singleton(), priceSetService: asClass(defaultServices.PriceSetService).singleton(), ruleTypeService: asClass(defaultServices.RuleTypeService).singleton(), + priceSetMoneyAmountRulesService: asClass( + defaultServices.PriceSetMoneyAmountRulesService + ).singleton(), priceRuleService: asClass(defaultServices.PriceRuleService).singleton(), }) @@ -51,6 +54,9 @@ function loadDefaultRepositories({ container }) { ruleTypeRepository: asClass( defaultRepositories.RuleTypeRepository ).singleton(), + priceSetMoneyAmountRulesRepository: asClass( + defaultRepositories.PriceSetMoneyAmountRulesRepository + ).singleton(), priceRuleRepository: asClass( defaultRepositories.PriceRuleRepository ).singleton(), diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing-1.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing-1.json deleted file mode 100644 index b1ed4609e9..0000000000 --- a/packages/pricing/src/migrations/.snapshot-medusa-pricing-1.json +++ /dev/null @@ -1,150 +0,0 @@ -{ - "namespaces": [ - "public" - ], - "name": "public", - "tables": [ - { - "columns": { - "code": { - "name": "code", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "symbol": { - "name": "symbol", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "symbol_native": { - "name": "symbol_native", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "name": { - "name": "name", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - } - }, - "name": "currency", - "schema": "public", - "indexes": [ - { - "keyName": "currency_pkey", - "columnNames": [ - "code" - ], - "composite": false, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": {} - }, - { - "columns": { - "id": { - "name": "id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, - "currency_code": { - "name": "currency_code", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, - "amount": { - "name": "amount", - "type": "numeric", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "decimal" - }, - "min_quantity": { - "name": "min_quantity", - "type": "numeric", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "decimal" - }, - "max_quantity": { - "name": "max_quantity", - "type": "numeric", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "decimal" - } - }, - "name": "money_amount", - "schema": "public", - "indexes": [ - { - "columnNames": [ - "currency_code" - ], - "composite": false, - "keyName": "IDX_money_amount_currency_code", - "primary": false, - "unique": false - }, - { - "keyName": "money_amount_pkey", - "columnNames": [ - "id" - ], - "composite": false, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": { - "money_amount_currency_code_foreign": { - "constraintName": "money_amount_currency_code_foreign", - "columnNames": [ - "currency_code" - ], - "localTableName": "public.money_amount", - "referencedColumnNames": [ - "code" - ], - "referencedTableName": "public.currency", - "deleteRule": "set null", - "updateRule": "cascade" - } - } - } - ] -} diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json index 0ecb8d9534..cc9253a86e 100644 --- a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json +++ b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json @@ -211,6 +211,16 @@ "primary": false, "nullable": false, "mappedType": "text" + }, + "number_rules": { + "name": "number_rules", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" } }, "name": "price_set_money_amount", @@ -338,6 +348,104 @@ "checks": [], "foreignKeys": {} }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "price_set_money_amount_id": { + "name": "price_set_money_amount_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" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "price_set_money_amount_rules", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "price_set_money_amount_id" + ], + "composite": false, + "keyName": "IDX_price_set_money_amount_rules_price_set_money_amount_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "rule_type_id" + ], + "composite": false, + "keyName": "IDX_price_set_money_amount_rules_rule_type_id", + "primary": false, + "unique": false + }, + { + "keyName": "price_set_money_amount_rules_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "price_set_money_amount_rules_price_set_money_amount_id_foreign": { + "constraintName": "price_set_money_amount_rules_price_set_money_amount_id_foreign", + "columnNames": [ + "price_set_money_amount_id" + ], + "localTableName": "public.price_set_money_amount_rules", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.price_set_money_amount", + "updateRule": "cascade" + }, + "price_set_money_amount_rules_rule_type_id_foreign": { + "constraintName": "price_set_money_amount_rules_rule_type_id_foreign", + "columnNames": [ + "rule_type_id" + ], + "localTableName": "public.price_set_money_amount_rules", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.rule_type", + "updateRule": "cascade" + } + } + }, { "columns": { "id": { @@ -418,14 +526,32 @@ "name": "price_rule", "schema": "public", "indexes": [ + { + "columnNames": [ + "price_set_id" + ], + "composite": false, + "keyName": "IDX_price_rule_price_set_id", + "primary": false, + "unique": false + }, { "columnNames": [ "rule_type_id" ], "composite": false, - "keyName": "price_rule_rule_type_id_unique", + "keyName": "IDX_price_rule_rule_type_id", "primary": false, - "unique": true + "unique": false + }, + { + "columnNames": [ + "price_set_money_amount_id" + ], + "composite": false, + "keyName": "IDX_price_rule_price_set_money_amount_id", + "primary": false, + "unique": false }, { "keyName": "price_rule_pkey", @@ -449,6 +575,7 @@ "id" ], "referencedTableName": "public.price_set", + "deleteRule": "cascade", "updateRule": "cascade" }, "price_rule_rule_type_id_foreign": { diff --git a/packages/pricing/src/migrations/Migration20230913055746.ts b/packages/pricing/src/migrations/Migration20230913055746.ts deleted file mode 100644 index e42ad50860..0000000000 --- a/packages/pricing/src/migrations/Migration20230913055746.ts +++ /dev/null @@ -1,23 +0,0 @@ -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/migrations/Migration20230913123118.ts b/packages/pricing/src/migrations/Migration20230913123118.ts deleted file mode 100644 index fa5abed0d9..0000000000 --- a/packages/pricing/src/migrations/Migration20230913123118.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class Migration20230913123118 extends Migration { - async up(): Promise { - this.addSql( - 'create table "currency" ("code" text not null, "symbol" text not null, "symbol_native" text not null, "name" text not null, constraint "currency_pkey" primary key ("code"));' - ) - - this.addSql( - 'create table "money_amount" ("id" text not null, "currency_code" text null, "amount" numeric null, "min_quantity" numeric null, "max_quantity" numeric null, constraint "money_amount_pkey" primary key ("id"));' - ) - - this.addSql( - 'create index "IDX_money_amount_currency_code" on "money_amount" ("currency_code");' - ) - - this.addSql( - 'create table "price_set" ("id" text not null, constraint "price_set_pkey" primary key ("id"));' - ) - - this.addSql( - 'create table "price_set_money_amount" ("id" text not null, "title" text not null, "price_set_id" text not null, "money_amount_id" text not null, constraint "price_set_money_amount_pkey" primary key ("id"));' - ) - - this.addSql( - 'create index "IDX_price_set_money_amount_price_set_id" on "price_set_money_amount" ("price_set_id");' - ) - - this.addSql( - 'create index "IDX_price_set_money_amount_money_amount_id" on "price_set_money_amount" ("money_amount_id");' - ) - - this.addSql( - 'create table "rule_type" ("id" text not null, "name" text not null, "rule_attribute" text not null, "default_priority" integer not null default 0, constraint "rule_type_pkey" primary key ("id"));' - ) - - this.addSql( - 'create index "IDX_rule_type_rule_attribute" on "rule_type" ("rule_attribute");' - ) - - this.addSql( - 'alter table "money_amount" add constraint "money_amount_currency_code_foreign" foreign key ("currency_code") references "currency" ("code") on update cascade on delete set null;' - ) - - this.addSql( - 'alter table "price_set_money_amount" add constraint "price_set_money_amount_price_set_id_foreign" foreign key ("price_set_id") references "price_set" ("id") on update cascade on delete cascade;' - ) - this.addSql( - 'alter table "price_set_money_amount" add constraint "price_set_money_amount_money_amount_id_foreign" foreign key ("money_amount_id") references "money_amount" ("id") on update cascade;' - ) - } -} diff --git a/packages/pricing/src/migrations/Migration20230928154931.ts b/packages/pricing/src/migrations/Migration20230928154931.ts new file mode 100644 index 0000000000..42749093a8 --- /dev/null +++ b/packages/pricing/src/migrations/Migration20230928154931.ts @@ -0,0 +1,88 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20230928154931 extends Migration { + async up(): Promise { + this.addSql( + 'create table "currency" ("code" text not null, "symbol" text not null, "symbol_native" text not null, "name" text not null, constraint "currency_pkey" primary key ("code"));' + ) + + this.addSql( + 'create table "money_amount" ("id" text not null, "currency_code" text null, "amount" numeric null, "min_quantity" numeric null, "max_quantity" numeric null, constraint "money_amount_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_money_amount_currency_code" on "money_amount" ("currency_code");' + ) + + this.addSql( + 'create table "price_set" ("id" text not null, constraint "price_set_pkey" primary key ("id"));' + ) + + this.addSql( + 'create table "price_set_money_amount" ("id" text not null, "title" text not null, "price_set_id" text not null, "money_amount_id" text not null, "number_rules" integer not null default 0, constraint "price_set_money_amount_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_price_set_money_amount_price_set_id" on "price_set_money_amount" ("price_set_id");' + ) + this.addSql( + 'create index "IDX_price_set_money_amount_money_amount_id" on "price_set_money_amount" ("money_amount_id");' + ) + + this.addSql( + 'create table "rule_type" ("id" text not null, "name" text not null, "rule_attribute" text not null, "default_priority" integer not null default 0, constraint "rule_type_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_rule_type_rule_attribute" on "rule_type" ("rule_attribute");' + ) + + this.addSql( + 'create table "price_set_money_amount_rules" ("id" text not null, "price_set_money_amount_id" text not null, "rule_type_id" text not null, "value" text not null, constraint "price_set_money_amount_rules_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_price_set_money_amount_rules_price_set_money_amount_id" on "price_set_money_amount_rules" ("price_set_money_amount_id");' + ) + this.addSql( + 'create index "IDX_price_set_money_amount_rules_rule_type_id" on "price_set_money_amount_rules" ("rule_type_id");' + ) + + 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( + 'create index "IDX_price_rule_price_set_id" on "price_rule" ("price_set_id");' + ) + this.addSql( + 'create index "IDX_price_rule_rule_type_id" on "price_rule" ("rule_type_id");' + ) + this.addSql( + 'create index "IDX_price_rule_price_set_money_amount_id" on "price_rule" ("price_set_money_amount_id");' + ) + + this.addSql( + 'alter table "money_amount" add constraint "money_amount_currency_code_foreign" foreign key ("currency_code") references "currency" ("code") on update cascade on delete set null;' + ) + + this.addSql( + 'alter table "price_set_money_amount" add constraint "price_set_money_amount_price_set_id_foreign" foreign key ("price_set_id") references "price_set" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table "price_set_money_amount" add constraint "price_set_money_amount_money_amount_id_foreign" foreign key ("money_amount_id") references "money_amount" ("id") on update cascade;' + ) + + this.addSql( + 'alter table "price_set_money_amount_rules" add constraint "price_set_money_amount_rules_price_set_money_amount_id_foreign" foreign key ("price_set_money_amount_id") references "price_set_money_amount" ("id") on update cascade;' + ) + this.addSql( + 'alter table "price_set_money_amount_rules" add constraint "price_set_money_amount_rules_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_id_foreign" foreign key ("price_set_id") references "price_set" ("id") on update cascade on delete 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;' + ) + } +} diff --git a/packages/pricing/src/models/index.ts b/packages/pricing/src/models/index.ts index 7929d9ce0f..94f963b5a3 100644 --- a/packages/pricing/src/models/index.ts +++ b/packages/pricing/src/models/index.ts @@ -1,6 +1,7 @@ export { default as Currency } from "./currency" export { default as MoneyAmount } from "./money-amount" +export { default as PriceRule } from "./price-rule" export { default as PriceSet } from "./price-set" export { default as PriceSetMoneyAmount } from "./price-set-money-amount" +export { default as PriceSetMoneyAmountRules } from "./price-set-money-amount-rules" 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 index 4a179b0af3..30a5a3c45b 100644 --- a/packages/pricing/src/models/price-rule.ts +++ b/packages/pricing/src/models/price-rule.ts @@ -1,21 +1,16 @@ import { BeforeCreate, - Collection, Entity, - ManyToMany, ManyToOne, - OneToMany, - OneToOne, OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" -import MoneyAmount from "./money-amount" +import { generateEntityId } from "@medusajs/utils" 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" @@ -31,6 +26,8 @@ export default class PriceRule { entity: () => PriceSet, fieldName: "price_set_id", name: "price_rule_price_set_id_unique", + onDelete: "cascade", + index: "IDX_price_rule_price_set_id", }) price_set: PriceSet @@ -38,6 +35,7 @@ export default class PriceRule { entity: () => RuleType, fieldName: "rule_type_id", name: "price_rule_rule_type_id_unique", + index: "IDX_price_rule_rule_type_id", }) rule_type: RuleType @@ -54,6 +52,7 @@ export default class PriceRule { entity: () => PriceSetMoneyAmount, fieldName: "price_set_money_amount_id", name: "price_set_money_amount_id_unique", + index: "IDX_price_rule_price_set_money_amount_id", }) price_set_money_amount: PriceSetMoneyAmount diff --git a/packages/pricing/src/models/price-set-money-amount-rules.ts b/packages/pricing/src/models/price-set-money-amount-rules.ts new file mode 100644 index 0000000000..373a25b08e --- /dev/null +++ b/packages/pricing/src/models/price-set-money-amount-rules.ts @@ -0,0 +1,35 @@ +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + ManyToOne, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import PriceSetMoneyAmount from "./price-set-money-amount" +import RuleType from "./rule-type" + +@Entity() +export default class PriceSetMoneyAmountRules { + @PrimaryKey({ columnType: "text" }) + id!: string + + @ManyToOne(() => PriceSetMoneyAmount, { + index: "IDX_price_set_money_amount_rules_price_set_money_amount_id", + }) + price_set_money_amount?: PriceSetMoneyAmount | string + + @ManyToOne(() => RuleType, { + index: "IDX_price_set_money_amount_rules_rule_type_id", + }) + rule_type?: RuleType | string + + @Property({ columnType: "text" }) + value: string + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "psmar") + } +} diff --git a/packages/pricing/src/models/price-set-money-amount.ts b/packages/pricing/src/models/price-set-money-amount.ts index 601e628d74..dd4354e001 100644 --- a/packages/pricing/src/models/price-set-money-amount.ts +++ b/packages/pricing/src/models/price-set-money-amount.ts @@ -1,15 +1,19 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Collection, Entity, ManyToOne, + OneToMany, PrimaryKey, PrimaryKeyType, Property, } from "@mikro-orm/core" import MoneyAmount from "./money-amount" +import PriceRule from "./price-rule" import PriceSet from "./price-set" +import PriceSetMoneyAmountRules from "./price-set-money-amount-rules" @Entity() export default class PriceSetMoneyAmount { @@ -30,6 +34,21 @@ export default class PriceSetMoneyAmount { }) money_amount?: MoneyAmount + @Property({ columnType: "integer", default: 0 }) + number_rules?: number + + @OneToMany({ + entity: () => PriceRule, + mappedBy: (pr) => pr.price_set_money_amount, + }) + price_rules = new Collection(this) + + @OneToMany({ + entity: () => PriceSetMoneyAmountRules, + mappedBy: (psmar) => psmar.price_set_money_amount, + }) + price_set_money_amount_rules = new Collection(this) + @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "psma") diff --git a/packages/pricing/src/models/price-set.ts b/packages/pricing/src/models/price-set.ts index 7d7374b865..fc95ea9577 100644 --- a/packages/pricing/src/models/price-set.ts +++ b/packages/pricing/src/models/price-set.ts @@ -1,20 +1,36 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Collection, Entity, ManyToMany, + OneToMany, + OptionalProps, PrimaryKey, } from "@mikro-orm/core" import MoneyAmount from "./money-amount" +import PriceRule from "./price-rule" import PriceSetMoneyAmount from "./price-set-money-amount" @Entity() export default class PriceSet { + [OptionalProps]?: "price_set_money_amounts" + @PrimaryKey({ columnType: "text" }) id!: string + @OneToMany(() => PriceSetMoneyAmount, (psma) => psma.price_set, { + cascade: [Cascade.REMOVE], + }) + price_set_money_amounts = new Collection(this) + + @OneToMany(() => PriceRule, (pr) => pr.price_set, { + cascade: [Cascade.REMOVE], + }) + price_rules = new Collection(this) + @ManyToMany({ entity: () => MoneyAmount, pivotEntity: () => PriceSetMoneyAmount, diff --git a/packages/pricing/src/repositories/index.ts b/packages/pricing/src/repositories/index.ts index 1d2b4eec1b..02593f70ef 100644 --- a/packages/pricing/src/repositories/index.ts +++ b/packages/pricing/src/repositories/index.ts @@ -1,6 +1,7 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export { CurrencyRepository } from "./currency" export { MoneyAmountRepository } from "./money-amount" -export { PriceSetRepository } from "./price-set" -export { RuleTypeRepository } from "./rule-type" export { PriceRuleRepository } from "./price-rule" +export { PriceSetRepository } from "./price-set" +export { PriceSetMoneyAmountRulesRepository } from "./price-set-money-amount-rules" +export { RuleTypeRepository } from "./rule-type" diff --git a/packages/pricing/src/repositories/price-rule.ts b/packages/pricing/src/repositories/price-rule.ts index 0e5c6f3ca5..13d2268afa 100644 --- a/packages/pricing/src/repositories/price-rule.ts +++ b/packages/pricing/src/repositories/price-rule.ts @@ -10,7 +10,7 @@ import { FilterQuery as MikroFilterQuery, FindOptions as MikroOptions, } from "@mikro-orm/core" -import { PriceRule, PriceSet, PriceSetMoneyAmount, RuleType } from "@models" +import { PriceRule } from "@models" import { SqlEntityManager } from "@mikro-orm/postgresql" @@ -76,20 +76,19 @@ export class PriceRuleRepository extends DALUtils.MikroOrmBaseRepository { const manager: SqlEntityManager = this.getActiveManager(context) - const toCreate = await Promise.all(data.map(async (ruleData) => { + const toCreate = data.map((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 + 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) diff --git a/packages/pricing/src/repositories/price-set-money-amount-rules.ts b/packages/pricing/src/repositories/price-set-money-amount-rules.ts new file mode 100644 index 0000000000..b57b61c4de --- /dev/null +++ b/packages/pricing/src/repositories/price-set-money-amount-rules.ts @@ -0,0 +1,127 @@ +import { + Context, + CreatePriceSetMoneyAmountRulesDTO, + DAL, + UpdatePriceSetMoneyAmountRulesDTO, +} from "@medusajs/types" +import { DALUtils, MedusaError } from "@medusajs/utils" +import { + LoadStrategy, + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, +} from "@mikro-orm/core" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSetMoneyAmountRules } from "@models" + +export class PriceSetMoneyAmountRulesRepository 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( + PriceSetMoneyAmountRules, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[PriceSetMoneyAmountRules[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await manager.findAndCount( + PriceSetMoneyAmountRules, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(PriceSetMoneyAmountRules, { id: { $in: ids } }) + } + + async create( + data: CreatePriceSetMoneyAmountRulesDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const psmar = data.map((psmarData) => { + return manager.create(PriceSetMoneyAmountRules, psmarData) + }) + + manager.persistAndFlush(psmar) + + return psmar + } + + async update( + data: UpdatePriceSetMoneyAmountRulesDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const psmarIds = data.map((psmar) => psmar.id) + const existingRecords = await this.find( + { + where: { + id: { + $in: psmarIds, + }, + }, + }, + context + ) + + const psmarMap = new Map( + existingRecords.map<[string, PriceSetMoneyAmountRules]>((psmar) => [ + psmar.id, + psmar, + ]) + ) + + const psmar = data.map((psmarData) => { + const existingRecord = psmarMap.get(psmarData.id) + + if (!existingRecord) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `PriceSetMoneyAmountRules with id "${psmarData.id}" not found` + ) + } + + return manager.assign(existingRecord, psmarData) + }) + + manager.persist(psmar) + + return psmar + } +} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index 6799d0d2b8..f97d09665b 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -1,6 +1,7 @@ export { default as CurrencyService } from "./currency" export { default as MoneyAmountService } from "./money-amount" +export { default as PriceRuleService } from "./price-rule" export { default as PriceSetService } from "./price-set" +export { default as PriceSetMoneyAmountRulesService } from "./price-set-money-amount-rules" export { default as PricingModuleService } from "./pricing-module" 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/money-amount.ts b/packages/pricing/src/services/money-amount.ts index 46a6ae7e13..9ef7655561 100644 --- a/packages/pricing/src/services/money-amount.ts +++ b/packages/pricing/src/services/money-amount.ts @@ -45,8 +45,13 @@ export default class MoneyAmountService< config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + return (await this.moneyAmountRepository_.find( - this.buildQueryForList(filters, config), + queryOptions, sharedContext )) as TEntity[] } @@ -57,26 +62,15 @@ export default class MoneyAmountService< config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { - return (await this.moneyAmountRepository_.findAndCount( - this.buildQueryForList(filters, config), - sharedContext - )) as [TEntity[], number] - } - - private buildQueryForList( - filters: PricingTypes.FilterableMoneyAmountProps = {}, - config: FindConfig = {} - ) { const queryOptions = ModulesSdkUtils.buildQuery( filters, config ) - if (filters.id) { - queryOptions.where["id"] = { $in: filters.id } - } - - return queryOptions + return (await this.moneyAmountRepository_.findAndCount( + queryOptions, + sharedContext + )) as [TEntity[], number] } @InjectTransactionManager(shouldForceTransaction, "moneyAmountRepository_") diff --git a/packages/pricing/src/services/price-rule.ts b/packages/pricing/src/services/price-rule.ts index 8b26c5e43f..0fc7c8f2c0 100644 --- a/packages/pricing/src/services/price-rule.ts +++ b/packages/pricing/src/services/price-rule.ts @@ -58,7 +58,7 @@ export default class PriceRuleService { @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { const queryConfig = ModulesSdkUtils.buildQuery(filters, config) - + return (await this.priceRuleRepository_.findAndCount( queryConfig, sharedContext diff --git a/packages/pricing/src/services/price-set-money-amount-rules.ts b/packages/pricing/src/services/price-set-money-amount-rules.ts new file mode 100644 index 0000000000..a8c2eb4151 --- /dev/null +++ b/packages/pricing/src/services/price-set-money-amount-rules.ts @@ -0,0 +1,120 @@ +import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + retrieveEntity, +} from "@medusajs/utils" +import { PriceSetMoneyAmountRules } from "@models" + +import { doNotForceTransaction, shouldForceTransaction } from "@medusajs/utils" + +type InjectedDependencies = { + priceSetMoneyAmountRulesRepository: DAL.RepositoryService +} + +export default class PriceSetMoneyAmountRulesService< + TEntity extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules +> { + protected readonly priceSetMoneyAmountRulesRepository_: DAL.RepositoryService + + constructor({ priceSetMoneyAmountRulesRepository }: InjectedDependencies) { + this.priceSetMoneyAmountRulesRepository_ = + priceSetMoneyAmountRulesRepository + } + + @InjectManager("priceSetMoneyAmountRulesRepository_") + async retrieve( + priceSetMoneyAmountRulesId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity< + PriceSetMoneyAmountRules, + PricingTypes.PriceSetMoneyAmountRulesDTO + >({ + id: priceSetMoneyAmountRulesId, + identifierColumn: "id", + entityName: PriceSetMoneyAmountRules.name, + repository: this.priceSetMoneyAmountRulesRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("priceSetMoneyAmountRulesRepository_") + async list( + filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.priceSetMoneyAmountRulesRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("priceSetMoneyAmountRulesRepository_") + async listAndCount( + filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + return (await this.priceSetMoneyAmountRulesRepository_.findAndCount( + this.buildQueryForList(filters, config), + sharedContext + )) as [TEntity[], number] + } + + private buildQueryForList( + filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, + config: FindConfig = {} + ) { + const queryOptions = ModulesSdkUtils.buildQuery( + filters, + config + ) + + return queryOptions + } + + @InjectTransactionManager( + shouldForceTransaction, + "priceSetMoneyAmountRulesRepository_" + ) + async create( + data: PricingTypes.CreatePriceSetMoneyAmountRulesDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.priceSetMoneyAmountRulesRepository_.create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager( + shouldForceTransaction, + "priceSetMoneyAmountRulesRepository_" + ) + async update( + data: PricingTypes.UpdatePriceSetMoneyAmountRulesDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.priceSetMoneyAmountRulesRepository_.update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager( + doNotForceTransaction, + "priceSetMoneyAmountRulesRepository_" + ) + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceSetMoneyAmountRulesRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/pricing/src/services/price-set.ts b/packages/pricing/src/services/price-set.ts index e03697717d..7edc65df11 100644 --- a/packages/pricing/src/services/price-set.ts +++ b/packages/pricing/src/services/price-set.ts @@ -43,8 +43,10 @@ export default class PriceSetService { config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { + const queryOptions = ModulesSdkUtils.buildQuery(filters, config) + return (await this.priceSetRepository_.find( - this.buildQueryForList(filters, config), + queryOptions, sharedContext )) as TEntity[] } @@ -55,23 +57,12 @@ export default class PriceSetService { config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { - return (await this.priceSetRepository_.findAndCount( - this.buildQueryForList(filters, config), - sharedContext - )) as [TEntity[], number] - } - - private buildQueryForList( - filters: PricingTypes.FilterablePriceSetProps = {}, - config: FindConfig = {} - ) { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) - if (filters.id) { - queryOptions.where.id = { $in: filters.id } - } - - return queryOptions + return (await this.priceSetRepository_.findAndCount( + queryOptions, + sharedContext + )) as [TEntity[], number] } @InjectTransactionManager(shouldForceTransaction, "priceSetRepository_") diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 4e0bd5f1dd..d413207b25 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -8,19 +8,30 @@ import { PricingFilters, PricingTypes, } from "@medusajs/types" +import { + Currency, + MoneyAmount, + PriceRule, + PriceSet, + PriceSetMoneyAmountRules, + RuleType, +} from "@models" import { CurrencyService, MoneyAmountService, + PriceRuleService, + PriceSetMoneyAmountRulesService, PriceSetService, RuleTypeService, - PriceRuleService } from "@services" -import { Currency, MoneyAmount, PriceRule, PriceSet, RuleType } from "@models" + +import { EntityManager } from "@mikro-orm/postgresql" import { InjectManager, InjectTransactionManager, MedusaContext, + groupBy, shouldForceTransaction, } from "@medusajs/utils" @@ -31,6 +42,7 @@ type InjectedDependencies = { currencyService: CurrencyService moneyAmountService: MoneyAmountService priceSetService: PriceSetService + priceSetMoneyAmountRulesService: PriceSetMoneyAmountRulesService ruleTypeService: RuleTypeService priceRuleService: PriceRuleService } @@ -40,7 +52,8 @@ export default class PricingModuleService< TMoneyAmount extends MoneyAmount = MoneyAmount, TCurrency extends Currency = Currency, TRuleType extends RuleType = RuleType, - TPriceRule extends PriceRule = PriceRule, + TPriceSetMoneyAmountRules extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules, + TPriceRule extends PriceRule = PriceRule > implements PricingTypes.IPricingModuleService { protected baseRepository_: DAL.RepositoryService @@ -48,6 +61,7 @@ export default class PricingModuleService< protected readonly moneyAmountService_: MoneyAmountService protected readonly ruleTypeService_: RuleTypeService protected readonly priceSetService_: PriceSetService + protected readonly priceSetMoneyAmountRulesService_: PriceSetMoneyAmountRulesService protected readonly priceRuleService_: PriceRuleService constructor( @@ -57,6 +71,7 @@ export default class PricingModuleService< currencyService, ruleTypeService, priceSetService, + priceSetMoneyAmountRulesService, priceRuleService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration @@ -66,6 +81,8 @@ export default class PricingModuleService< this.moneyAmountService_ = moneyAmountService this.ruleTypeService_ = ruleTypeService this.priceSetService_ = priceSetService + this.priceSetMoneyAmountRulesService_ = priceSetMoneyAmountRulesService + this.ruleTypeService_ = ruleTypeService this.priceRuleService_ = priceRuleService } @@ -79,46 +96,91 @@ export default class PricingModuleService< pricingContext: PricingContext = { context: {} }, @MedusaContext() sharedContext: Context = {} ): Promise { + const manager = sharedContext.manager as EntityManager + const knex = manager.getKnex() + // Keeping this whole logic raw in here for now as they will undergo // some changes, will abstract them out once we have a final version const context = pricingContext.context || {} - const priceSetFilters: PricingTypes.FilterablePriceSetProps = { - id: pricingFilters.id, + + // Quantity is used to scope money amounts based on min_quantity and max_quantity. + // We should potentially think of reserved words in pricingContext that can't be used in rules + // or have a separate pricing options that accept things like quantity, price_list_id and other + // pricing module features + const quantity = context.quantity + delete context.quantity + + // Gets all the price set money amounts where rules match for each of the contexts + // that the price set is configured for + const psmaSubQueryKnex = knex({ + psma: "price_set_money_amount", + }) + .select({ + id: "psma.id", + price_set_id: "psma.price_set_id", + money_amount_id: "psma.money_amount_id", + number_rules: "psma.number_rules", + }) + .join("price_rule as pr", "pr.price_set_money_amount_id", "psma.id") + .join("rule_type as rt", "rt.id", "pr.rule_type_id") + .orderBy("number_rules", "desc") + + for (const [key, value] of Object.entries(context)) { + psmaSubQueryKnex.orWhere({ + "rt.rule_attribute": key, + "pr.value": value, + }) } - const priceSets = await this.list( - priceSetFilters, - { - select: [ - "id", - "money_amounts.id", - "money_amounts.currency_code", - "money_amounts.amount", - "money_amounts.min_quantity", - "money_amounts.max_quantity", - ], - relations: ["money_amounts"], - }, - sharedContext - ) + psmaSubQueryKnex + .groupBy("psma.id") + .having(knex.raw("count(DISTINCT rt.rule_attribute) = psma.number_rules")) - const calculatedPrices = priceSets.map( - (priceSet): PricingTypes.CalculatedPriceSetDTO => { - // TODO: This will change with the rules engine selection, - // making a DB query directly instead - // This should look for a default price when no rules apply - // When no price is set, return null values for all cases - const selectedMoneyAmount = priceSet.money_amounts?.find( - (ma) => - context.currency_code && ma.currency_code === context.currency_code - ) + const priceSetQueryKnex = knex({ + ps: "price_set", + }) + .select({ + id: "ps.id", + amount: "ma.amount", + min_quantity: "ma.min_quantity", + max_quantity: "ma.max_quantity", + currency_code: "ma.currency_code", + default_priority: "rt.default_priority", + number_rules: "psma.number_rules", + }) + .join(psmaSubQueryKnex.as("psma"), "psma.price_set_id", "ps.id") + .join("money_amount as ma", "ma.id", "psma.money_amount_id") + .join("price_rule as pr", "pr.price_set_money_amount_id", "psma.id") + .join("rule_type as rt", "rt.id", "pr.rule_type_id") + .whereIn("ps.id", pricingFilters.id) + .orderBy([ + { column: "number_rules", order: "desc" }, + { column: "default_priority", order: "desc" }, + ]) + + if (quantity) { + priceSetQueryKnex.where("ma.min_quantity", "<=", quantity) + priceSetQueryKnex.andWhere("ma.max_quantity", ">=", quantity) + } + + const isContextPresent = Object.entries(context).length + // Only if the context is present do we need to query the database. + const queryBuilderResults = isContextPresent ? await priceSetQueryKnex : [] + const pricesSetPricesMap = groupBy(queryBuilderResults, "id") + + const calculatedPrices = pricingFilters.id.map( + (priceSetId: string): PricingTypes.CalculatedPriceSetDTO => { + // This is where we select prices, for now we just do a first match based on the database results + // which is prioritized by number_rules first for exact match and then deafult_priority of the rule_type + // inject custom price selection here + const price = pricesSetPricesMap.get(priceSetId)?.[0] return { - id: priceSet.id, - amount: selectedMoneyAmount?.amount || null, - currency_code: selectedMoneyAmount?.currency_code || null, - min_quantity: selectedMoneyAmount?.min_quantity || null, - max_quantity: selectedMoneyAmount?.max_quantity || null, + id: priceSetId, + amount: price?.amount || null, + currency_code: price?.currency_code || null, + min_quantity: price?.min_quantity || null, + max_quantity: price?.max_quantity || null, } } ) @@ -527,6 +589,110 @@ export default class PricingModuleService< await this.ruleTypeService_.delete(ruleTypes, sharedContext) } + @InjectManager("baseRepository_") + async retrievePriceSetMoneyAmountRules( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const record = await this.priceSetMoneyAmountRulesService_.retrieve( + id, + config, + sharedContext + ) + + return this.baseRepository_.serialize( + record, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async listPriceSetMoneyAmountRules( + filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const records = await this.priceSetMoneyAmountRulesService_.list( + filters, + config, + sharedContext + ) + + return this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async listAndCountPriceSetMoneyAmountRules( + filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PricingTypes.PriceSetMoneyAmountRulesDTO[], number]> { + const [records, count] = + await this.priceSetMoneyAmountRulesService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }), + count, + ] + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async createPriceSetMoneyAmountRules( + data: PricingTypes.CreatePriceSetMoneyAmountRulesDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const records = await this.priceSetMoneyAmountRulesService_.create( + data, + sharedContext + ) + + return this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async updatePriceSetMoneyAmountRules( + data: PricingTypes.UpdatePriceSetMoneyAmountRulesDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const records = await this.priceSetMoneyAmountRulesService_.update( + data, + sharedContext + ) + + return this.baseRepository_.serialize< + PricingTypes.PriceSetMoneyAmountRulesDTO[] + >(records, { + populate: true, + }) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async deletePriceSetMoneyAmountRules( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceSetMoneyAmountRulesService_.delete(ids, sharedContext) + } + @InjectManager("baseRepository_") async retrievePriceRule( id: string, @@ -539,9 +705,12 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize(priceRule, { - populate: true, - }) + return this.baseRepository_.serialize( + priceRule, + { + populate: true, + } + ) } @InjectManager("baseRepository_") diff --git a/packages/pricing/src/services/rule-type.ts b/packages/pricing/src/services/rule-type.ts index ffecebce50..7dadbbc070 100644 --- a/packages/pricing/src/services/rule-type.ts +++ b/packages/pricing/src/services/rule-type.ts @@ -43,8 +43,10 @@ export default class RuleTypeService { config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { + const queryOptions = ModulesSdkUtils.buildQuery(filters, config) + return (await this.ruleTypeRepository_.find( - this.buildQueryForList(filters, config), + queryOptions, sharedContext )) as TEntity[] } @@ -55,23 +57,12 @@ export default class RuleTypeService { config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { - return (await this.ruleTypeRepository_.findAndCount( - this.buildQueryForList(filters, config), - sharedContext - )) as [TEntity[], number] - } - - private buildQueryForList( - filters: PricingTypes.FilterableRuleTypeProps = {}, - config: FindConfig = {} - ) { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) - if (filters.id) { - queryOptions.where["id"] = { $in: filters.id } - } - - return queryOptions + return (await this.ruleTypeRepository_.findAndCount( + queryOptions, + sharedContext + )) as [TEntity[], number] } @InjectTransactionManager(shouldForceTransaction, "ruleTypeRepository_") diff --git a/packages/types/src/pricing/common/index.ts b/packages/types/src/pricing/common/index.ts index c5aec182e6..bc6e09c7d3 100644 --- a/packages/types/src/pricing/common/index.ts +++ b/packages/types/src/pricing/common/index.ts @@ -1,5 +1,7 @@ export * from "./currency" export * from "./money-amount" export * from "./price-set" +export * from "./price-set-money-amount" +export * from "./price-set-money-amount-rules" export * from "./rule-type" export * from './price-rule' diff --git a/packages/types/src/pricing/common/price-set-money-amount-rules.ts b/packages/types/src/pricing/common/price-set-money-amount-rules.ts new file mode 100644 index 0000000000..a315e61e71 --- /dev/null +++ b/packages/types/src/pricing/common/price-set-money-amount-rules.ts @@ -0,0 +1,31 @@ +import { BaseFilterable } from "../../dal" +import { PriceSetMoneyAmountDTO } from "./price-set-money-amount" +import { RuleTypeDTO } from "./rule-type" + +export interface PriceSetMoneyAmountRulesDTO { + id: string + price_set_money_amount: PriceSetMoneyAmountDTO + rule_type: RuleTypeDTO + value: string +} + +export interface CreatePriceSetMoneyAmountRulesDTO { + price_set_money_amount: string + rule_type: string + value: string +} + +export interface UpdatePriceSetMoneyAmountRulesDTO { + id: string + price_set_money_amount?: string + rule_type?: string + value?: string +} + +export interface FilterablePriceSetMoneyAmountRulesProps + extends BaseFilterable { + id?: string[] + rule_type_id?: string[] + price_set_money_amount_id?: string[] + value?: string[] +} diff --git a/packages/types/src/pricing/common/price-set-money-amount.ts b/packages/types/src/pricing/common/price-set-money-amount.ts new file mode 100644 index 0000000000..a041d90f77 --- /dev/null +++ b/packages/types/src/pricing/common/price-set-money-amount.ts @@ -0,0 +1,9 @@ +import { MoneyAmountDTO } from "./money-amount" +import { PriceSetDTO } from "./price-set" + +export interface PriceSetMoneyAmountDTO { + id: string + title?: string + price_set?: PriceSetDTO + rule_type?: MoneyAmountDTO +} diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts index 8195b472a5..47d084080e 100644 --- a/packages/types/src/pricing/common/price-set.ts +++ b/packages/types/src/pricing/common/price-set.ts @@ -2,9 +2,7 @@ import { BaseFilterable } from "../../dal" import { FilterableMoneyAmountProps, MoneyAmountDTO } from "./money-amount" export interface PricingContext { - context?: { - currency_code?: string - } + context?: Record } export interface PricingFilters { diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index 7b32d3b25c..0eefffcad8 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -4,16 +4,19 @@ import { CreateMoneyAmountDTO, CreatePriceRuleDTO, CreatePriceSetDTO, + CreatePriceSetMoneyAmountRulesDTO, CreateRuleTypeDTO, CurrencyDTO, FilterableCurrencyProps, FilterableMoneyAmountProps, FilterablePriceRuleProps, + FilterablePriceSetMoneyAmountRulesProps, FilterablePriceSetProps, FilterableRuleTypeProps, MoneyAmountDTO, PriceRuleDTO, PriceSetDTO, + PriceSetMoneyAmountRulesDTO, PricingContext, PricingFilters, RuleTypeDTO, @@ -21,12 +24,13 @@ import { UpdateMoneyAmountDTO, UpdatePriceRuleDTO, UpdatePriceSetDTO, + UpdatePriceSetMoneyAmountRulesDTO, UpdateRuleTypeDTO, } from "./common" -import { Context } from "../shared-context" import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" +import { Context } from "../shared-context" export interface IPricingModuleService { __joinerConfig(): ModuleJoinerConfig @@ -157,11 +161,41 @@ export interface IPricingModuleService { sharedContext?: Context ): Promise - deleteRuleTypes( - ruleTypeIds: string[], + deleteRuleTypes(ruleTypeIds: string[], sharedContext?: Context): Promise + + retrievePriceSetMoneyAmountRules( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listPriceSetMoneyAmountRules( + filters?: FilterablePriceSetMoneyAmountRulesProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountPriceSetMoneyAmountRules( + filters?: FilterablePriceSetMoneyAmountRulesProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[PriceSetMoneyAmountRulesDTO[], number]> + + createPriceSetMoneyAmountRules( + data: CreatePriceSetMoneyAmountRulesDTO[], + sharedContext?: Context + ): Promise + + updatePriceSetMoneyAmountRules( + data: UpdatePriceSetMoneyAmountRulesDTO[], + sharedContext?: Context + ): Promise + + deletePriceSetMoneyAmountRules( + ids: string[], sharedContext?: Context ): Promise - + retrievePriceRule( id: string, config?: FindConfig, diff --git a/packages/utils/src/common/__tests__/group-by.spec.ts b/packages/utils/src/common/__tests__/group-by.spec.ts new file mode 100644 index 0000000000..b25e0db8b9 --- /dev/null +++ b/packages/utils/src/common/__tests__/group-by.spec.ts @@ -0,0 +1,50 @@ +import { groupBy } from "../group-by" + +const array = [ + { + id: "test-id-1", + property: "test-id-1-property-1", + }, + { + id: "test-id-1", + property: "test-id-1-property-2", + }, + { + id: "test-id-2", + property: "test-id-2-property-1", + }, + { + id: "test-id-2", + property: "test-id-2-property-2", + }, + { + id: "test-id-3", + property: "test-id-3-property-1", + }, +] + +const mapToObject = (map: Map) => Object.fromEntries(map.entries()) + +describe("groupBy", function () { + it("should return a map grouped by an identifier", function () { + const response = mapToObject(groupBy(array, "id")) + + expect(response).toEqual({ + "test-id-1": [ + { id: "test-id-1", property: "test-id-1-property-1" }, + { id: "test-id-1", property: "test-id-1-property-2" }, + ], + "test-id-2": [ + { id: "test-id-2", property: "test-id-2-property-1" }, + { id: "test-id-2", property: "test-id-2-property-2" }, + ], + "test-id-3": [{ id: "test-id-3", property: "test-id-3-property-1" }], + }) + }) + + it("should return empty map if identifier is not found in array", function () { + const response = mapToObject(groupBy(array, "doesnotexist")) + + expect(response).toEqual({}) + }) +}) diff --git a/packages/utils/src/common/group-by.ts b/packages/utils/src/common/group-by.ts new file mode 100644 index 0000000000..49e2bddf9e --- /dev/null +++ b/packages/utils/src/common/group-by.ts @@ -0,0 +1,20 @@ +export function groupBy( + array: Record[], + attribute: string | number +): Map { + return array.reduce>((map, obj) => { + const key = obj[attribute] + + if (!key) { + return map + } + + if (!map.get(key)) { + map.set(key, []) + } + + map.get(key).push(obj) + + return map + }, new Map()) +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index de50c3db51..39e0488f4e 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -4,6 +4,7 @@ export * from "./deduplicate" export * from "./errors" export * from "./generate-entity-id" export * from "./get-config-file" +export * from "./group-by" export * from "./handle-postgres-database-error" export * from "./is-date" export * from "./is-defined"