diff --git a/.changeset/yellow-queens-jam.md b/.changeset/yellow-queens-jam.md new file mode 100644 index 0000000000..3e5a2d8334 --- /dev/null +++ b/.changeset/yellow-queens-jam.md @@ -0,0 +1,7 @@ +--- +"@medusajs/pricing": patch +"@medusajs/types": patch +"@medusajs/link-modules": patch +--- + +feat(pricing,types,link-modules): PriceSets as an entry point to pricing module diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 0e2147e2c5..e62cfe17fa 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -1,4 +1,4 @@ export * from "./inventory-level-stock-location" export * from "./product-variant-inventory-item" -export * from "./product-variant-money-amount" +export * from "./product-variant-price-set" export * from "./product-shipping-profile" diff --git a/packages/link-modules/src/definitions/product-variant-money-amount.ts b/packages/link-modules/src/definitions/product-variant-price-set.ts similarity index 60% rename from packages/link-modules/src/definitions/product-variant-money-amount.ts rename to packages/link-modules/src/definitions/product-variant-price-set.ts index 6d977d7f98..f64e23f393 100644 --- a/packages/link-modules/src/definitions/product-variant-money-amount.ts +++ b/packages/link-modules/src/definitions/product-variant-price-set.ts @@ -2,22 +2,22 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { LINKS } from "../links" -export const ProductVariantMoneyAmount: ModuleJoinerConfig = { - serviceName: LINKS.ProductVariantMoneyAmount, +export const ProductVariantPriceSet: ModuleJoinerConfig = { + serviceName: LINKS.ProductVariantPriceSet, isLink: true, databaseConfig: { - tableName: "product_variant_money_amount", - idPrefix: "pvma", + tableName: "product_variant_price_set", + idPrefix: "pvps", }, alias: [ { - name: "product_variant_money_amount", + name: "product_variant_price_set", }, { - name: "product_variant_money_amounts", + name: "product_variant_price_sets", }, ], - primaryKeys: ["id", "variant_id", "money_amount_id"], + primaryKeys: ["id", "variant_id", "price_set_id"], relationships: [ { serviceName: Modules.PRODUCT, @@ -31,8 +31,8 @@ export const ProductVariantMoneyAmount: ModuleJoinerConfig = { { serviceName: Modules.PRICING, primaryKey: "id", - foreignKey: "money_amount_id", - alias: "money_amount", + foreignKey: "price_set_id", + alias: "price_set", deleteCascade: true, }, ], @@ -40,18 +40,18 @@ export const ProductVariantMoneyAmount: ModuleJoinerConfig = { { serviceName: Modules.PRODUCT, relationship: { - serviceName: LINKS.ProductVariantMoneyAmount, + serviceName: LINKS.ProductVariantPriceSet, primaryKey: "variant_id", foreignKey: "id", alias: "prices", - isList: true, + isList: false, }, }, { serviceName: Modules.PRICING, relationship: { - serviceName: LINKS.ProductVariantMoneyAmount, - primaryKey: "money_amount_id", + serviceName: LINKS.ProductVariantPriceSet, + primaryKey: "price_set_id", foreignKey: "id", alias: "variant_link", }, diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index 5b92e23cb9..3c8c3ea942 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -8,11 +8,11 @@ export const LINKS = { Modules.INVENTORY, "inventory_item_id" ), - ProductVariantMoneyAmount: composeLinkName( + ProductVariantPriceSet: composeLinkName( Modules.PRODUCT, "variant_id", Modules.PRICING, - "money_amount_id" + "price_set_id" ), // Internal services diff --git a/packages/pricing/integration-tests/__fixtures__/price-set/data.ts b/packages/pricing/integration-tests/__fixtures__/price-set/data.ts new file mode 100644 index 0000000000..ce4d60c77f --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set/data.ts @@ -0,0 +1,13 @@ +import { CreatePriceSetDTO } from "@medusajs/types" + +export const defaultPriceSetsData = [ + { + id: "price-set-1", + }, + { + id: "price-set-2", + }, + { + id: "price-set-3", + }, +] as unknown as CreatePriceSetDTO[] diff --git a/packages/pricing/integration-tests/__fixtures__/price-set/index.ts b/packages/pricing/integration-tests/__fixtures__/price-set/index.ts new file mode 100644 index 0000000000..656c42d865 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/price-set/index.ts @@ -0,0 +1,38 @@ +import { CreatePriceSetDTO } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSet, PriceSetMoneyAmount } from "@models" +import { defaultPriceSetsData } from "./data" + +export async function createPriceSets( + manager: SqlEntityManager, + priceSetsData: CreatePriceSetDTO[] = defaultPriceSetsData +): Promise { + const priceSets: PriceSet[] = [] + + for (let priceSetData of priceSetsData) { + const priceSetDataClone = { ...priceSetData } + const moneyAmountsData = priceSetDataClone.money_amounts || [] + delete priceSetDataClone.money_amounts + + let priceSet = manager.create(PriceSet, priceSetDataClone) as PriceSet + + await manager.persist(priceSet).flush() + + for (let moneyAmount of moneyAmountsData) { + const price_set = (await manager.findOne( + PriceSet, + priceSet.id + )) as PriceSet + + const psma = manager.create(PriceSetMoneyAmount, { + price_set: price_set.id, + money_amount: moneyAmount.id, + title: "test", + }) + + manager.persist(psma).flush() + } + } + + return priceSets +} diff --git a/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts new file mode 100644 index 0000000000..bb6cf1a222 --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts @@ -0,0 +1,388 @@ +import { CreatePriceSetDTO } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { MoneyAmount, PriceSet } from "@models" +import { PriceSetRepository } from "@repositories" +import { PriceSetService } from "@services" + +import { createCurrencies } from "../../../__fixtures__/currency" +import { createMoneyAmounts } from "../../../__fixtures__/money-amount" +import { createPriceSets } from "../../../__fixtures__/price-set" +import { MikroOrmWrapper } from "../../../utils" + +jest.setTimeout(30000) + +describe("PriceSet Service", () => { + let service: PriceSetService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let data!: PriceSet[] + let moneyAmountsData!: MoneyAmount[] + + const moneyAmountsInputData = [ + { + id: "money-amount-USD", + currency_code: "USD", + 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: "price-set-3", + money_amounts: [], + }, + ] + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + testManager = await MikroOrmWrapper.forkManager() + + const priceSetRepository = new PriceSetRepository({ + manager: repositoryManager, + }) + + service = new PriceSetService({ + priceSetRepository, + }) + + await createCurrencies(testManager) + + moneyAmountsData = await createMoneyAmounts( + testManager, + moneyAmountsInputData + ) + + data = await createPriceSets(testManager, priceSetInputData) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("list", () => { + it("should list priceSets", async () => { + const priceSetsResult = await service.list() + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(serialized).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + expect.objectContaining({ + id: "price-set-2", + }), + expect.objectContaining({ + id: "price-set-3", + }), + ]) + }) + + it("should list priceSets by id", async () => { + const priceSetsResult = await service.list({ + id: ["price-set-1"], + }) + + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + ]) + }) + + it("should list priceSets with relations and selects", async () => { + const priceSetsResult = await service.list( + { + id: ["price-set-1"], + }, + { + select: ["id", "money_amounts.id"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(serialized).toEqual([ + { + id: "price-set-1", + money_amounts: [ + { + id: "money-amount-USD", + }, + ], + }, + ]) + }) + + it("should scope priceSets with currency_code of money amounts", async () => { + const priceSetsResult = await service.list( + { + money_amounts: { + currency_code: ["USD"], + }, + }, + { + select: ["id", "money_amounts.id"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(serialized).toEqual([ + { + id: "price-set-1", + money_amounts: [ + { + id: "money-amount-USD", + }, + ], + }, + ]) + }) + }) + + it("should not return price sets if money amounts with a currency code dont exist", async () => { + const priceSetsResult = await service.list( + { + money_amounts: { + currency_code: ["DOESNOTEXIST"], + }, + }, + { + select: ["id", "money_amounts.id"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(serialized).toEqual([]) + }) + + describe("listAndCount", () => { + it("should return priceSets and count", async () => { + const [priceSetsResult, count] = await service.listAndCount() + + expect(count).toEqual(3) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + expect.objectContaining({ + id: "price-set-2", + }), + expect.objectContaining({ + id: "price-set-3", + }), + ]) + }) + + it("should return priceSets and count when filtered", async () => { + const [priceSetsResult, count] = await service.listAndCount({ + id: ["price-set-1"], + }) + + expect(count).toEqual(1) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + ]) + }) + + it("should list priceSets with relations and selects", async () => { + const [priceSetsResult, count] = await service.listAndCount( + { + id: ["price-set-1"], + }, + { + select: ["id", "min_quantity", "money_amounts.id"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(count).toEqual(1) + expect(serialized).toEqual([ + { + id: "price-set-1", + money_amounts: [ + { + id: "money-amount-USD", + }, + ], + }, + ]) + }) + + it("should return priceSets and count when using skip and take", async () => { + const [priceSetsResult, count] = await service.listAndCount( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(3) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [priceSetsResult, count] = await service.listAndCount( + {}, + { + take: 1, + select: ["id"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(count).toEqual(3) + expect(serialized).toEqual([ + { + id: "price-set-1", + }, + ]) + }) + }) + + describe("retrieve", () => { + const id = "price-set-1" + + it("should return priceSet for the given id", async () => { + const priceSet = await service.retrieve(id) + + expect(priceSet).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when priceSet with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceSet with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.retrieve(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"priceSetId" must be defined') + }) + + it("should return priceSet based on config select param", async () => { + const priceSet = await service.retrieve(id, { + select: ["id"], + }) + + const serialized = JSON.parse(JSON.stringify(priceSet)) + + expect(serialized).toEqual({ + id, + }) + }) + }) + + describe("delete", () => { + const id = "price-set-1" + + it("should delete the priceSets given an id successfully", async () => { + await service.delete([id]) + + const priceSets = await service.list({ + id: [id], + }) + + expect(priceSets).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "price-set-1" + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceSet with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + random: "does-not-exist", + } as any, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual('PriceSet with id "undefined" not found') + }) + + it("should create a priceSet successfully", async () => { + await service.create([ + { + id: "price-set-new", + } as unknown as CreatePriceSetDTO, + ]) + + const [priceSet] = await service.list({ + id: ["price-set-new"], + }) + + expect(priceSet).toEqual( + expect.objectContaining({ + id: "price-set-new", + } as unknown as CreatePriceSetDTO) + ) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts index bf808c0f1a..7da19d47bc 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts @@ -6,7 +6,7 @@ import { initialize } from "../../../../src" import { createCurrencies } from "../../../__fixtures__/currency" import { DB_URL, MikroOrmWrapper } from "../../../utils" -describe("PricingModuleService currency", () => { +describe("PricingModule Service - Currency", () => { let service: IPricingModuleService let testManager: SqlEntityManager let repositoryManager: SqlEntityManager diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts index 03d85972f0..fb2889b9b4 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts @@ -9,7 +9,7 @@ import { DB_URL, MikroOrmWrapper } from "../../../utils" jest.setTimeout(30000) -describe("MoneyAmount Service", () => { +describe("PricingModule Service - MoneyAmount", () => { let service: IPricingModuleService let testManager: SqlEntityManager let repositoryManager: SqlEntityManager @@ -38,9 +38,9 @@ describe("MoneyAmount Service", () => { await MikroOrmWrapper.clearDatabase() }) - describe("list", () => { + describe("listMoneyAmounts", () => { it("list moneyAmounts", async () => { - const moneyAmountsResult = await service.list() + const moneyAmountsResult = await service.listMoneyAmounts() expect(moneyAmountsResult).toEqual([ expect.objectContaining({ @@ -58,8 +58,8 @@ describe("MoneyAmount Service", () => { ]) }) - it("list moneyAmounts by id", async () => { - const moneyAmountsResult = await service.list({ + it("should list moneyAmounts by id", async () => { + const moneyAmountsResult = await service.listMoneyAmounts({ id: ["money-amount-USD"], }) @@ -70,8 +70,8 @@ describe("MoneyAmount Service", () => { ]) }) - it("list moneyAmounts with relations and selects", async () => { - const moneyAmountsResult = await service.list( + it("should list moneyAmounts with relations and selects", async () => { + const moneyAmountsResult = await service.listMoneyAmounts( { id: ["money-amount-USD"], }, @@ -96,9 +96,10 @@ describe("MoneyAmount Service", () => { }) }) - describe("listAndCount", () => { + describe("listAndCountMoneyAmounts", () => { it("should return moneyAmounts and count", async () => { - const [moneyAmountsResult, count] = await service.listAndCount() + const [moneyAmountsResult, count] = + await service.listAndCountMoneyAmounts() expect(count).toEqual(3) expect(moneyAmountsResult).toEqual([ @@ -115,9 +116,10 @@ describe("MoneyAmount Service", () => { }) it("should return moneyAmounts and count when filtered", async () => { - const [moneyAmountsResult, count] = await service.listAndCount({ - id: ["money-amount-USD"], - }) + const [moneyAmountsResult, count] = + await service.listAndCountMoneyAmounts({ + id: ["money-amount-USD"], + }) expect(count).toEqual(1) expect(moneyAmountsResult).toEqual([ @@ -128,15 +130,16 @@ describe("MoneyAmount Service", () => { }) it("list moneyAmounts with relations and selects", async () => { - const [moneyAmountsResult, count] = await service.listAndCount( - { - id: ["money-amount-USD"], - }, - { - select: ["id", "min_quantity", "currency.code"], - relations: ["currency"], - } - ) + const [moneyAmountsResult, count] = + await service.listAndCountMoneyAmounts( + { + id: ["money-amount-USD"], + }, + { + select: ["id", "min_quantity", "currency.code"], + relations: ["currency"], + } + ) const serialized = JSON.parse(JSON.stringify(moneyAmountsResult)) @@ -154,10 +157,8 @@ describe("MoneyAmount Service", () => { }) it("should return moneyAmounts and count when using skip and take", async () => { - const [moneyAmountsResult, count] = await service.listAndCount( - {}, - { skip: 1, take: 1 } - ) + const [moneyAmountsResult, count] = + await service.listAndCountMoneyAmounts({}, { skip: 1, take: 1 }) expect(count).toEqual(3) expect(moneyAmountsResult).toEqual([ @@ -168,13 +169,14 @@ describe("MoneyAmount Service", () => { }) it("should return requested fields", async () => { - const [moneyAmountsResult, count] = await service.listAndCount( - {}, - { - take: 1, - select: ["id"], - } - ) + const [moneyAmountsResult, count] = + await service.listAndCountMoneyAmounts( + {}, + { + take: 1, + select: ["id"], + } + ) const serialized = JSON.parse(JSON.stringify(moneyAmountsResult)) @@ -187,12 +189,12 @@ describe("MoneyAmount Service", () => { }) }) - describe("retrieve", () => { + describe("retrieveMoneyAmount", () => { const id = "money-amount-USD" const amount = "500" it("should return moneyAmount for the given id", async () => { - const moneyAmount = await service.retrieve(id) + const moneyAmount = await service.retrieveMoneyAmount(id) expect(moneyAmount).toEqual( expect.objectContaining({ @@ -205,7 +207,7 @@ describe("MoneyAmount Service", () => { let error try { - await service.retrieve("does-not-exist") + await service.retrieveMoneyAmount("does-not-exist") } catch (e) { error = e } @@ -219,7 +221,7 @@ describe("MoneyAmount Service", () => { let error try { - await service.retrieve(undefined as unknown as string) + await service.retrieveMoneyAmount(undefined as unknown as string) } catch (e) { error = e } @@ -228,7 +230,7 @@ describe("MoneyAmount Service", () => { }) it("should return moneyAmount based on config select param", async () => { - const moneyAmount = await service.retrieve(id, { + const moneyAmount = await service.retrieveMoneyAmount(id, { select: ["id", "amount"], }) @@ -241,13 +243,13 @@ describe("MoneyAmount Service", () => { }) }) - describe("delete", () => { + describe("deleteMoneyAmounts", () => { const id = "money-amount-USD" it("should delete the moneyAmounts given an id successfully", async () => { - await service.delete([id]) + await service.deleteMoneyAmounts([id]) - const moneyAmounts = await service.list({ + const moneyAmounts = await service.listMoneyAmounts({ id: [id], }) @@ -255,31 +257,31 @@ describe("MoneyAmount Service", () => { }) }) - describe("update", () => { + describe("updateMoneyAmounts", () => { const id = "money-amount-USD" it("should update the amount of the moneyAmount successfully", async () => { - await service.update([ + await service.updateMoneyAmounts([ { id, amount: 700, }, ]) - const moneyAmount = await service.retrieve(id) + const moneyAmount = await service.retrieveMoneyAmount(id) expect(moneyAmount.amount).toEqual("700") }) it("should update the currency of the moneyAmount successfully", async () => { - await service.update([ + await service.updateMoneyAmounts([ { id, currency_code: "EUR", }, ]) - const moneyAmount = await service.retrieve(id, { + const moneyAmount = await service.retrieveMoneyAmount(id, { relations: ["currency"], }) @@ -291,7 +293,7 @@ describe("MoneyAmount Service", () => { let error try { - await service.update([ + await service.updateMoneyAmounts([ { id: "does-not-exist", amount: 666, @@ -307,9 +309,9 @@ describe("MoneyAmount Service", () => { }) }) - describe("create", () => { + describe("createMoneyAmounts", () => { it("should create a moneyAmount successfully", async () => { - await service.create([ + await service.createMoneyAmounts([ { id: "money-amount-TESM", currency_code: "USD", @@ -319,7 +321,7 @@ describe("MoneyAmount Service", () => { }, ]) - const [moneyAmount] = await service.list({ + const [moneyAmount] = await service.listMoneyAmounts({ id: ["money-amount-TESM"], }) 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 new file mode 100644 index 0000000000..9295122bac --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -0,0 +1,404 @@ +import { CreatePriceSetDTO, IPricingModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { PriceSet } from "@models" + +import { 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" + +jest.setTimeout(30000) + +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) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("calculatePrices", () => { + it("retrieves the calculated prices when no context is set", async () => { + const priceSetsResult = await service.calculatePrices( + ["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( + ["price-set-1", "price-set-2"], + { + 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( + ["price-set-doesnotexist", "price-set-1"], + { + 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() + + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + expect.objectContaining({ + id: "price-set-2", + }), + expect.objectContaining({ + id: "price-set-3", + }), + ]) + }) + + it("list priceSets by id", async () => { + const priceSetsResult = await service.list({ + id: ["price-set-1"], + }) + + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + ]) + }) + + it("list priceSets with relations and selects", async () => { + const priceSetsResult = await service.list( + { + id: ["price-set-1"], + }, + { + select: ["id", "money_amounts.id", "money_amounts.amount"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(serialized).toEqual([ + { + id: "price-set-1", + money_amounts: [{ id: "money-amount-USD", amount: "500" }], + }, + ]) + }) + }) + + describe("listAndCount", () => { + it("should return priceSets and count", async () => { + const [priceSetsResult, count] = await service.listAndCount() + + expect(count).toEqual(3) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + expect.objectContaining({ + id: "price-set-2", + }), + expect.objectContaining({ + id: "price-set-3", + }), + ]) + }) + + it("should return priceSets and count when filtered", async () => { + const [priceSetsResult, count] = await service.listAndCount({ + id: ["price-set-1"], + }) + + expect(count).toEqual(1) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-1", + }), + ]) + }) + + it("list priceSets with relations and selects", async () => { + const [priceSetsResult, count] = await service.listAndCount( + { + id: ["price-set-1"], + }, + { + select: ["id", "min_quantity", "money_amounts.id"], + relations: ["money_amounts"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(count).toEqual(1) + expect(serialized).toEqual([ + { + id: "price-set-1", + money_amounts: [{ id: "money-amount-USD" }], + }, + ]) + }) + + it("should return priceSets and count when using skip and take", async () => { + const [priceSetsResult, count] = await service.listAndCount( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(3) + expect(priceSetsResult).toEqual([ + expect.objectContaining({ + id: "price-set-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [priceSetsResult, count] = await service.listAndCount( + {}, + { + take: 1, + select: ["id"], + } + ) + + const serialized = JSON.parse(JSON.stringify(priceSetsResult)) + + expect(count).toEqual(3) + expect(serialized).toEqual([ + { + id: "price-set-1", + }, + ]) + }) + }) + + describe("retrieve", () => { + const id = "price-set-1" + + it("should return priceSet for the given id", async () => { + const priceSet = await service.retrieve(id) + + expect(priceSet).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when priceSet with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "PriceSet with id: does-not-exist was not found" + ) + }) + + it("should throw an error when a id is not provided", async () => { + let error + + try { + await service.retrieve(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"priceSetId" must be defined') + }) + + it("should return priceSet based on config select param", async () => { + const priceSet = await service.retrieve(id, { + select: ["id"], + }) + + const serialized = JSON.parse(JSON.stringify(priceSet)) + + expect(serialized).toEqual({ + id, + }) + }) + }) + + describe("delete", () => { + const id = "price-set-1" + + it("should delete the priceSets given an id successfully", async () => { + await service.delete([id]) + + const priceSets = await service.list({ + id: [id], + }) + + expect(priceSets).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "price-set-1" + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'PriceSet with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + random: "does-not-exist", + } as any, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual('PriceSet with id "undefined" not found') + }) + + it("should create a priceSet successfully", async () => { + await service.create([ + { + id: "price-set-new", + } as unknown as CreatePriceSetDTO, + ]) + + const [priceSet] = await service.list({ + id: ["price-set-new"], + }) + + expect(priceSet).toEqual( + expect.objectContaining({ + id: "price-set-new", + }) + ) + }) + }) +}) diff --git a/packages/pricing/src/joiner-config.ts b/packages/pricing/src/joiner-config.ts index 80e0c8dc83..58675499f9 100644 --- a/packages/pricing/src/joiner-config.ts +++ b/packages/pricing/src/joiner-config.ts @@ -1,16 +1,22 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Currency, MoneyAmount } from "@models" +import * as Models from "@models" export enum LinkableKeys { MONEY_AMOUNT_ID = "money_amount_id", CURRENCY_CODE = "currency_code", + PRICE_SET_ID = "price_set_id", } export const entityNameToLinkableKeysMap: MapToConfig = { - [Currency.name]: [{ mapTo: LinkableKeys.CURRENCY_CODE, valueFrom: "code" }], - [MoneyAmount.name]: [ + [Models.PriceSet.name]: [ + { mapTo: LinkableKeys.PRICE_SET_ID, valueFrom: "id" }, + ], + [Models.Currency.name]: [ + { mapTo: LinkableKeys.CURRENCY_CODE, valueFrom: "code" }, + ], + [Models.MoneyAmount.name]: [ { mapTo: LinkableKeys.MONEY_AMOUNT_ID, valueFrom: "id" }, ], } @@ -20,11 +26,23 @@ export const joinerConfig: ModuleJoinerConfig = { primaryKeys: ["id", "currency_code"], linkableKeys: Object.values(LinkableKeys), alias: [ + { + name: "price_set", + }, + { + name: "price_sets", + }, { name: "money_amount", + args: { + methodSuffix: "MoneyAmounts", + }, }, { name: "money_amounts", + args: { + methodSuffix: "MoneyAmounts", + }, }, { name: "currency", diff --git a/packages/pricing/src/loaders/container.ts b/packages/pricing/src/loaders/container.ts index d4afa75721..7b1698480f 100644 --- a/packages/pricing/src/loaders/container.ts +++ b/packages/pricing/src/loaders/container.ts @@ -1,11 +1,6 @@ import { ModulesSdkTypes } from "@medusajs/types" import * as defaultRepositories from "@repositories" -import { - BaseRepository, - CurrencyRepository, - MoneyAmountRepository, -} from "@repositories" -import { CurrencyService, MoneyAmountService } from "@services" +import * as defaultServices from "@services" import { LoaderOptions } from "@medusajs/modules-sdk" import { loadCustomRepositories } from "@medusajs/utils" @@ -23,8 +18,9 @@ export default async ({ )?.repositories container.register({ - currencyService: asClass(CurrencyService).singleton(), - moneyAmountService: asClass(MoneyAmountService).singleton(), + currencyService: asClass(defaultServices.CurrencyService).singleton(), + moneyAmountService: asClass(defaultServices.MoneyAmountService).singleton(), + priceSetService: asClass(defaultServices.PriceSetService).singleton(), }) if (customRepositories) { @@ -40,8 +36,15 @@ export default async ({ function loadDefaultRepositories({ container }) { container.register({ - baseRepository: asClass(BaseRepository).singleton(), - currencyRepository: asClass(CurrencyRepository).singleton(), - moneyAmountRepository: asClass(MoneyAmountRepository).singleton(), + baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), + currencyRepository: asClass( + defaultRepositories.CurrencyRepository + ).singleton(), + moneyAmountRepository: asClass( + defaultRepositories.MoneyAmountRepository + ).singleton(), + priceSetRepository: asClass( + defaultRepositories.PriceSetRepository + ).singleton(), }) } diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json new file mode 100644 index 0000000000..0e55108cd4 --- /dev/null +++ b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json @@ -0,0 +1,260 @@ +{ + "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" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "price_set", + "schema": "public", + "indexes": [ + { + "keyName": "price_set_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "price_set_id": { + "name": "price_set_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "money_amount_id": { + "name": "money_amount_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "price_set_money_amount", + "schema": "public", + "indexes": [ + { + "keyName": "price_set_money_amount_pkey", + "columnNames": [ + "id", + "price_set_id", + "money_amount_id" + ], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "price_set_money_amount_price_set_id_foreign": { + "constraintName": "price_set_money_amount_price_set_id_foreign", + "columnNames": [ + "price_set_id" + ], + "localTableName": "public.price_set_money_amount", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.price_set", + "updateRule": "cascade" + }, + "price_set_money_amount_money_amount_id_foreign": { + "constraintName": "price_set_money_amount_money_amount_id_foreign", + "columnNames": [ + "money_amount_id" + ], + "localTableName": "public.price_set_money_amount", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.money_amount", + "updateRule": "cascade" + } + } + } + ] +} diff --git a/packages/pricing/src/migrations/Migration20230830085850.ts b/packages/pricing/src/migrations/Migration20230830085850.ts deleted file mode 100644 index ca68e2698e..0000000000 --- a/packages/pricing/src/migrations/Migration20230830085850.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Migration } from '@mikro-orm/migrations'; - -export class Migration20230830085850 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('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;'); - } - -} diff --git a/packages/pricing/src/migrations/Migration20230907144224.ts b/packages/pricing/src/migrations/Migration20230907144224.ts new file mode 100644 index 0000000000..0085467840 --- /dev/null +++ b/packages/pricing/src/migrations/Migration20230907144224.ts @@ -0,0 +1,36 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20230907144224 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, "price_set_id" text null, "money_amount_id" text null, "title" text not null, constraint "price_set_money_amount_pkey" primary key ("id", "price_set_id", "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;' + ) + + 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/models/index.ts b/packages/pricing/src/models/index.ts index fa2b5baf19..8504e5a196 100644 --- a/packages/pricing/src/models/index.ts +++ b/packages/pricing/src/models/index.ts @@ -1,2 +1,4 @@ export { default as Currency } from "./currency" export { default as MoneyAmount } from "./money-amount" +export { default as PriceSet } from "./price-set" +export { default as PriceSetMoneyAmount } from "./price-set-money-amount" diff --git a/packages/pricing/src/models/money-amount.ts b/packages/pricing/src/models/money-amount.ts index c9c53f6dc8..2f4b77758e 100644 --- a/packages/pricing/src/models/money-amount.ts +++ b/packages/pricing/src/models/money-amount.ts @@ -1,13 +1,16 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Collection, Entity, + ManyToMany, ManyToOne, PrimaryKey, Property, } from "@mikro-orm/core" import Currency from "./currency" +import PriceSet from "./price-set" @Entity() class MoneyAmount { @@ -17,6 +20,12 @@ class MoneyAmount { @Property({ columnType: "text", nullable: true }) currency_code?: string + @ManyToMany({ + entity: () => PriceSet, + mappedBy: (ps) => ps.money_amounts, + }) + price_sets = new Collection(this) + @ManyToOne(() => Currency, { nullable: true, index: "IDX_money_amount_currency_code", diff --git a/packages/pricing/src/models/price-set-money-amount.ts b/packages/pricing/src/models/price-set-money-amount.ts new file mode 100644 index 0000000000..47b117e300 --- /dev/null +++ b/packages/pricing/src/models/price-set-money-amount.ts @@ -0,0 +1,39 @@ +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + ManyToOne, + PrimaryKey, + PrimaryKeyType, + Property, +} from "@mikro-orm/core" + +import MoneyAmount from "./money-amount" +import PriceSet from "./price-set" + +@Entity() +export default class PriceSetMoneyAmount { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + title!: string + + @ManyToOne(() => PriceSet, { onDelete: "cascade" }) + price_set?: PriceSet + + @ManyToOne(() => MoneyAmount, {}) + money_amount?: MoneyAmount + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "psma") + } + + [PrimaryKeyType]?: [string, string] + + constructor(money_amount: MoneyAmount, price_set: PriceSet) { + this.money_amount = money_amount + this.price_set = price_set + } +} diff --git a/packages/pricing/src/models/price-set.ts b/packages/pricing/src/models/price-set.ts new file mode 100644 index 0000000000..7d7374b865 --- /dev/null +++ b/packages/pricing/src/models/price-set.ts @@ -0,0 +1,28 @@ +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Collection, + Entity, + ManyToMany, + PrimaryKey, +} from "@mikro-orm/core" + +import MoneyAmount from "./money-amount" +import PriceSetMoneyAmount from "./price-set-money-amount" + +@Entity() +export default class PriceSet { + @PrimaryKey({ columnType: "text" }) + id!: string + + @ManyToMany({ + entity: () => MoneyAmount, + pivotEntity: () => PriceSetMoneyAmount, + }) + money_amounts = new Collection(this) + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "pset") + } +} diff --git a/packages/pricing/src/repositories/index.ts b/packages/pricing/src/repositories/index.ts index adee24e032..f8689b004d 100644 --- a/packages/pricing/src/repositories/index.ts +++ b/packages/pricing/src/repositories/index.ts @@ -1,3 +1,4 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export { CurrencyRepository } from "./currency" export { MoneyAmountRepository } from "./money-amount" +export { PriceSetRepository } from "./price-set" diff --git a/packages/pricing/src/repositories/price-set.ts b/packages/pricing/src/repositories/price-set.ts new file mode 100644 index 0000000000..eb856e102e --- /dev/null +++ b/packages/pricing/src/repositories/price-set.ts @@ -0,0 +1,127 @@ +import { + Context, + CreatePriceSetDTO, + DAL, + UpdatePriceSetDTO, +} 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 { PriceSet } from "@models" + +export class PriceSetRepository 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( + PriceSet, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[PriceSet[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await manager.findAndCount( + PriceSet, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(PriceSet, { id: { $in: ids } }, {}) + } + + async create( + data: CreatePriceSetDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const priceSets = data.map((priceSetData) => { + return manager.create(PriceSet, priceSetData) + }) + + manager.persist(priceSets) + + return priceSets + } + + async update( + data: UpdatePriceSetDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const priceSetIds = data.map((priceSetData) => priceSetData.id) + const existingPriceSets = await this.find( + { + where: { + id: { + $in: priceSetIds, + }, + }, + }, + context + ) + + const existingPriceSetMap = new Map( + existingPriceSets.map<[string, PriceSet]>((priceSet) => [ + priceSet.id, + priceSet, + ]) + ) + + const priceSets = data.map((priceSetData) => { + const existingPriceSet = existingPriceSetMap.get(priceSetData.id) + + if (!existingPriceSet) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `PriceSet with id "${priceSetData.id}" not found` + ) + } + + return manager.assign(existingPriceSet, priceSetData) + }) + + manager.persist(priceSets) + + return priceSets + } +} diff --git a/packages/pricing/src/scripts/seed.ts b/packages/pricing/src/scripts/seed.ts index e0a5762954..6aac156a3f 100644 --- a/packages/pricing/src/scripts/seed.ts +++ b/packages/pricing/src/scripts/seed.ts @@ -22,11 +22,14 @@ export async function run({ logger.info(`Loading seed data from ${path}...`) - const { currenciesData, moneyAmountsData } = await import( - resolve(process.cwd(), path) - ).catch((e) => { + const { + currenciesData, + moneyAmountsData, + priceSetsData, + priceSetMoneyAmountsData, + } = await import(resolve(process.cwd(), path)).catch((e) => { logger?.error( - `Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following: currenciesData, moneyAmountsData.${EOL}${e}` + `Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following: priceSetsData, currenciesData, moneyAmountsData and priceSetMoneyAmountsData.${EOL}${e}` ) throw e }) @@ -44,10 +47,12 @@ export async function run({ const manager = orm.em.fork() try { - logger.info("Inserting currencies & money_amounts") + logger.info("Inserting price_sets, currencies & money_amounts") await createCurrencies(manager, currenciesData) await createMoneyAmounts(manager, moneyAmountsData) + await createPriceSets(manager, priceSetsData) + await createPriceSetMoneyAmounts(manager, priceSetMoneyAmountsData) } catch (e) { logger.error( `Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}` @@ -82,3 +87,32 @@ async function createMoneyAmounts( return moneyAmounts } + +async function createPriceSets( + manager: SqlEntityManager, + data: RequiredEntityData[] +) { + const priceSets = data.map((priceSetData) => { + return manager.create(PricingModels.PriceSet, priceSetData) + }) + + await manager.persistAndFlush(priceSets) + + return priceSets +} + +async function createPriceSetMoneyAmounts( + manager: SqlEntityManager, + data: RequiredEntityData[] +) { + const priceSetMoneyAmounts = data.map((priceSetMoneyAmountData) => { + return manager.create( + PricingModels.PriceSetMoneyAmount, + priceSetMoneyAmountData + ) + }) + + await manager.persistAndFlush(priceSetMoneyAmounts) + + return priceSetMoneyAmounts +} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index 2f9e5e521c..a524635fa7 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -1,3 +1,4 @@ export { default as CurrencyService } from "./currency" export { default as MoneyAmountService } from "./money-amount" +export { default as PriceSetService } from "./price-set" export { default as PricingModuleService } from "./pricing-module" diff --git a/packages/pricing/src/services/price-set.ts b/packages/pricing/src/services/price-set.ts new file mode 100644 index 0000000000..e03697717d --- /dev/null +++ b/packages/pricing/src/services/price-set.ts @@ -0,0 +1,106 @@ +import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + doNotForceTransaction, + retrieveEntity, + shouldForceTransaction, +} from "@medusajs/utils" +import { PriceSet } from "@models" +import { PriceSetRepository } from "@repositories" + +type InjectedDependencies = { + priceSetRepository: DAL.RepositoryService +} + +export default class PriceSetService { + protected readonly priceSetRepository_: DAL.RepositoryService + + constructor({ priceSetRepository }: InjectedDependencies) { + this.priceSetRepository_ = priceSetRepository + } + + @InjectManager("priceSetRepository_") + async retrieve( + priceSetId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity({ + id: priceSetId, + entityName: PriceSet.name, + repository: this.priceSetRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("priceSetRepository_") + async list( + filters: PricingTypes.FilterablePriceSetProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.priceSetRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("priceSetRepository_") + async listAndCount( + filters: PricingTypes.FilterablePriceSetProps = {}, + 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 + } + + @InjectTransactionManager(shouldForceTransaction, "priceSetRepository_") + async create( + data: PricingTypes.CreatePriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.priceSetRepository_ as PriceSetRepository).create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "priceSetRepository_") + async update( + data: PricingTypes.UpdatePriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await (this.priceSetRepository_ as PriceSetRepository).update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(doNotForceTransaction, "priceSetRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceSetRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 041e38ae18..3bf80428cb 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -6,8 +6,8 @@ import { ModuleJoinerConfig, PricingTypes, } from "@medusajs/types" -import { Currency, MoneyAmount } from "@models" -import { CurrencyService, MoneyAmountService } from "@services" +import { Currency, MoneyAmount, PriceSet } from "@models" +import { CurrencyService, MoneyAmountService, PriceSetService } from "@services" import { InjectManager, @@ -22,9 +22,15 @@ type InjectedDependencies = { baseRepository: DAL.RepositoryService currencyService: CurrencyService moneyAmountService: MoneyAmountService + priceSetService: PriceSetService +} + +type PricingContext = { + currency_code?: string } export default class PricingModuleService< + TPriceSet extends PriceSet = PriceSet, TMoneyAmount extends MoneyAmount = MoneyAmount, TCurrency extends Currency = Currency > implements PricingTypes.IPricingModuleService @@ -32,26 +38,180 @@ export default class PricingModuleService< protected baseRepository_: DAL.RepositoryService protected readonly currencyService_: CurrencyService protected readonly moneyAmountService_: MoneyAmountService + protected readonly priceSetService_: PriceSetService constructor( { baseRepository, moneyAmountService, currencyService, + priceSetService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { this.baseRepository_ = baseRepository this.currencyService_ = currencyService this.moneyAmountService_ = moneyAmountService + this.priceSetService_ = priceSetService } __joinerConfig(): ModuleJoinerConfig { return joinerConfig } + @InjectManager("baseRepository_") + async calculatePrices( + priceSetIds: string[], + pricingContext: PricingContext, + @MedusaContext() sharedContext: Context = {} + ): Promise { + // 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 priceSetFilters: PricingTypes.FilterablePriceSetProps = { + id: priceSetIds, + } + + 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 + ) + + 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) => + pricingContext.currency_code && + ma.currency_code === pricingContext.currency_code + ) + + 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, + } + } + ) + + return JSON.parse(JSON.stringify(calculatedPrices)) + } + @InjectManager("baseRepository_") async retrieve( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceSet = await this.priceSetService_.retrieve( + id, + config, + sharedContext + ) + + return this.baseRepository_.serialize(priceSet, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async list( + filters: PricingTypes.FilterablePriceSetProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const priceSets = await this.priceSetService_.list( + filters, + config, + sharedContext + ) + + return this.baseRepository_.serialize( + priceSets, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async listAndCount( + filters: PricingTypes.FilterablePriceSetProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PricingTypes.PriceSetDTO[], number]> { + const [priceSets, count] = await this.priceSetService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + priceSets, + { + populate: true, + } + ), + count, + ] + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async create( + data: PricingTypes.CreatePriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ) { + const priceSets = await this.priceSetService_.create(data, sharedContext) + + return this.baseRepository_.serialize( + priceSets, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async update( + data: PricingTypes.UpdatePriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ) { + const priceSets = await this.priceSetService_.update(data, sharedContext) + + return this.baseRepository_.serialize( + priceSets, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.priceSetService_.delete(ids, sharedContext) + } + + @InjectManager("baseRepository_") + async retrieveMoneyAmount( id: string, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} @@ -71,7 +231,7 @@ export default class PricingModuleService< } @InjectManager("baseRepository_") - async list( + async listMoneyAmounts( filters: PricingTypes.FilterableMoneyAmountProps = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} @@ -91,7 +251,7 @@ export default class PricingModuleService< } @InjectManager("baseRepository_") - async listAndCount( + async listAndCountMoneyAmounts( filters: PricingTypes.FilterableMoneyAmountProps = {}, config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} @@ -114,7 +274,7 @@ export default class PricingModuleService< } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") - async create( + async createMoneyAmounts( data: PricingTypes.CreateMoneyAmountDTO[], @MedusaContext() sharedContext: Context = {} ) { @@ -132,7 +292,7 @@ export default class PricingModuleService< } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") - async update( + async updateMoneyAmounts( data: PricingTypes.UpdateMoneyAmountDTO[], @MedusaContext() sharedContext: Context = {} ) { @@ -150,7 +310,7 @@ export default class PricingModuleService< } @InjectTransactionManager(shouldForceTransaction, "baseRepository_") - async delete( + async deleteMoneyAmounts( ids: string[], @MedusaContext() sharedContext: Context = {} ): Promise { diff --git a/packages/pricing/tsconfig.spec.json b/packages/pricing/tsconfig.spec.json index b887bbfa39..48e47e8cbb 100644 --- a/packages/pricing/tsconfig.spec.json +++ b/packages/pricing/tsconfig.spec.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.json", "include": ["src", "integration-tests"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } } diff --git a/packages/types/src/pricing/common/index.ts b/packages/types/src/pricing/common/index.ts index 4baa6d178b..f036d6bdfa 100644 --- a/packages/types/src/pricing/common/index.ts +++ b/packages/types/src/pricing/common/index.ts @@ -1,2 +1,3 @@ export * from "./currency" export * from "./money-amount" +export * from "./price-set" diff --git a/packages/types/src/pricing/common/price-set.ts b/packages/types/src/pricing/common/price-set.ts new file mode 100644 index 0000000000..71d8f87dc3 --- /dev/null +++ b/packages/types/src/pricing/common/price-set.ts @@ -0,0 +1,32 @@ +import { BaseFilterable } from "../../dal" +import { FilterableMoneyAmountProps, MoneyAmountDTO } from "./money-amount" + +export interface PricingContext { + currency_code?: string +} +export interface PriceSetDTO { + id: string + money_amounts?: MoneyAmountDTO[] +} + +export interface CalculatedPriceSetDTO { + id: string + amount: number | null + currency_code: string | null + min_quantity: number | null + max_quantity: number | null +} + +export interface CreatePriceSetDTO { + money_amounts?: MoneyAmountDTO[] +} + +export interface UpdatePriceSetDTO { + id: string +} + +export interface FilterablePriceSetProps + extends BaseFilterable { + id?: string[] + money_amounts?: FilterableMoneyAmountProps +} diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index e86195c2f8..edab03a3a7 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -2,48 +2,90 @@ import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" import { Context } from "../shared-context" import { + CalculatedPriceSetDTO, CreateCurrencyDTO, CreateMoneyAmountDTO, + CreatePriceSetDTO, CurrencyDTO, FilterableCurrencyProps, FilterableMoneyAmountProps, + FilterablePriceSetProps, MoneyAmountDTO, + PriceSetDTO, + PricingContext, UpdateCurrencyDTO, UpdateMoneyAmountDTO, + UpdatePriceSetDTO, } from "./common" export interface IPricingModuleService { __joinerConfig(): ModuleJoinerConfig + calculatePrices( + priceSetIds: string[], + pricingContext: PricingContext, + sharedContext?: Context + ): Promise + retrieve( - id: string, + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: FilterablePriceSetProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: FilterablePriceSetProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[PriceSetDTO[], number]> + + create( + data: CreatePriceSetDTO[], + sharedContext?: Context + ): Promise + + update( + data: UpdatePriceSetDTO[], + sharedContext?: Context + ): Promise + + delete(ids: string[], sharedContext?: Context): Promise + + retrieveMoneyAmount( + id: string, config?: FindConfig, sharedContext?: Context ): Promise - list( + listMoneyAmounts( filters?: FilterableMoneyAmountProps, config?: FindConfig, sharedContext?: Context ): Promise - listAndCount( + listAndCountMoneyAmounts( filters?: FilterableMoneyAmountProps, config?: FindConfig, sharedContext?: Context ): Promise<[MoneyAmountDTO[], number]> - create( + createMoneyAmounts( data: CreateMoneyAmountDTO[], sharedContext?: Context ): Promise - update( + updateMoneyAmounts( data: UpdateMoneyAmountDTO[], sharedContext?: Context ): Promise - delete(ids: string[], sharedContext?: Context): Promise + deleteMoneyAmounts(ids: string[], sharedContext?: Context): Promise retrieveCurrency( code: string,