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"