From edf90eecb487f6e031f2e2d0899de5ca2504cb12 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:58:31 +0200 Subject: [PATCH] feat(pricing) Add Price Set Rule Type (#4977) * initial * initial service * update pricing module service * add integration test for rule-type * update pricing-module integration tests * update pricing service interface * feat(pricing): PriceSets as entry point to pricing module * chore: add price set money amount * chore: add price set money amount * chore: change name of test * chore: added changeset * chore: use filterable props from money amount in price sets * chore: update migrations * test update integration test * fix weird behavior * Update packages/pricing/integration-tests/__fixtures__/rule-type/index.ts Co-authored-by: Riqwan Thamir * Apply suggestions from code review Co-authored-by: Riqwan Thamir * move rule-type to common * chore: reset migration * chore: remove incorrect conflicts * chore: address review * chore: remove ghost price list * Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * update id prefix * use persist not persistAndflush * rename key_value to rule_attribute * more renaming --------- Co-authored-by: Riqwan Thamir Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/tiny-bottles-repair.md | 6 + .../__fixtures__/price-list/data.ts | 36 --- .../__fixtures__/price-list/index.ts | 25 -- .../__fixtures__/rule-type/data.ts | 12 + .../__fixtures__/rule-type/index.ts | 20 ++ .../services/pricing-module/rule-type.spec.ts | 252 +++++++++++++++++ .../services/rule-type.ts/index.spec.ts | 257 ++++++++++++++++++ packages/pricing/src/loaders/container.ts | 4 + .../migrations/.snapshot-medusa-pricing.json | 110 +++++++- ...07144224.ts => Migration20230913123118.ts} | 24 +- packages/pricing/src/models/index.ts | 1 + .../src/models/price-set-money-amount.ts | 9 +- packages/pricing/src/models/rule-type.ts | 34 +++ packages/pricing/src/repositories/index.ts | 1 + .../pricing/src/repositories/rule-type.ts | 127 +++++++++ packages/pricing/src/services/index.ts | 1 + .../pricing/src/services/pricing-module.ts | 116 +++++++- packages/pricing/src/services/rule-type.ts | 106 ++++++++ packages/types/src/pricing/common/index.ts | 1 + .../types/src/pricing/common/rule-type.ts | 28 ++ packages/types/src/pricing/index.ts | 2 +- packages/types/src/pricing/service.ts | 34 +++ 22 files changed, 1120 insertions(+), 86 deletions(-) create mode 100644 .changeset/tiny-bottles-repair.md delete mode 100644 packages/pricing/integration-tests/__fixtures__/price-list/data.ts delete mode 100644 packages/pricing/integration-tests/__fixtures__/price-list/index.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/rule-type/data.ts create mode 100644 packages/pricing/integration-tests/__fixtures__/rule-type/index.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts create mode 100644 packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts rename packages/pricing/src/migrations/{Migration20230907144224.ts => Migration20230913123118.ts} (61%) create mode 100644 packages/pricing/src/models/rule-type.ts create mode 100644 packages/pricing/src/repositories/rule-type.ts create mode 100644 packages/pricing/src/services/rule-type.ts create mode 100644 packages/types/src/pricing/common/rule-type.ts diff --git a/.changeset/tiny-bottles-repair.md b/.changeset/tiny-bottles-repair.md new file mode 100644 index 0000000000..a244759885 --- /dev/null +++ b/.changeset/tiny-bottles-repair.md @@ -0,0 +1,6 @@ +--- +"@medusajs/pricing": patch +"@medusajs/types": patch +--- + +feat(pricing,types): Add price set rule type diff --git a/packages/pricing/integration-tests/__fixtures__/price-list/data.ts b/packages/pricing/integration-tests/__fixtures__/price-list/data.ts deleted file mode 100644 index 67fe3cd354..0000000000 --- a/packages/pricing/integration-tests/__fixtures__/price-list/data.ts +++ /dev/null @@ -1,36 +0,0 @@ - -export const defaultPriceListData = [ - { - name: 'pl-1', - description: 'pl-1', - prices: [ { - id: "money-amount-USD", - currency_code: "USD", - amount: 500, - min_quantity: 1, - max_quantity: 10, - price_list_id: 'pl-1' - }, - { - id: "money-amount-EUR", - currency_code: "EUR", - amount: 400, - min_quantity: 1, - max_quantity: 5, - price_list_id: 'pl-1' - }] - }, - { - id: 'pl-2', - name: 'pl-2', - description: 'pl-2', - prices: [{ - id: "money-amount-CAD", - currency_code: "CAD", - amount: 600, - min_quantity: 1, - max_quantity: 8, - price_list_id: 'pl-2' - }] - } -] \ No newline at end of file diff --git a/packages/pricing/integration-tests/__fixtures__/price-list/index.ts b/packages/pricing/integration-tests/__fixtures__/price-list/index.ts deleted file mode 100644 index 45caafde49..0000000000 --- a/packages/pricing/integration-tests/__fixtures__/price-list/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" -import { MoneyAmount, PriceList } from "@models" -import { defaultPriceListData } from "./data" - -export async function createPriceLists( - manager: SqlEntityManager, - priceListsData: any[] = defaultPriceListData, -): Promise { - const priceLists: PriceList[] = [] - const moneyAmounts: MoneyAmount[] = [] - - for (let priceListdata of priceListsData) { - const { prices, ...rest } = priceListdata - const priceList = manager.create(MoneyAmount, rest) - priceLists.push(priceList) - - const createdPrices = manager.create(MoneyAmount, prices) - moneyAmounts.push(createdPrices) - } - - await manager.persistAndFlush(priceLists) - await manager.persistAndFlush(moneyAmounts) - - return priceLists -} diff --git a/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts b/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts new file mode 100644 index 0000000000..52fb0664e0 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/rule-type/data.ts @@ -0,0 +1,12 @@ +export const defaultRuleTypesData = [ + { + id: "rule-type-1", + name: "rule 1", + rule_attribute: "region_id", + }, + { + id: "rule-type-2", + name: "rule 2", + rule_attribute: "currency_code", + }, +] diff --git a/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts new file mode 100644 index 0000000000..458c6f5449 --- /dev/null +++ b/packages/pricing/integration-tests/__fixtures__/rule-type/index.ts @@ -0,0 +1,20 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { RuleType } from "@models" +import { defaultRuleTypesData } from "./data" + +export async function createRuleTypes( + manager: SqlEntityManager, + ruletypesData: any[] = defaultRuleTypesData +): Promise { + const RuleTypes: RuleType[] = [] + + for (let ruleTypeData of ruletypesData) { + const ruleType = manager.create(RuleType, ruleTypeData) + + RuleTypes.push(ruleType) + } + + await manager.persistAndFlush(RuleTypes) + + return RuleTypes +} 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 new file mode 100644 index 0000000000..04b5d763d3 --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts @@ -0,0 +1,252 @@ +import { IPricingModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { initialize } from "../../../../src" +import { createRuleTypes } from "../../../__fixtures__/rule-type" +import { DB_URL, MikroOrmWrapper } from "../../../utils" + +describe("PricingModuleService ruleType", () => { + let service: IPricingModuleService + let testManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + MikroOrmWrapper.forkManager() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRICING_DB_SCHEMA, + }, + }) + + testManager = MikroOrmWrapper.forkManager() + + await createRuleTypes(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("listRuleTypes", () => { + it("should list rule types", async () => { + const ruleTypeResult = await service.listRuleTypes() + + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("should list rule types by id", async () => { + const ruleTypeResult = await service.listRuleTypes({ + id: ["rule-type-1"], + }) + + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + ]) + }) + }) + + describe("listAndCountRuleTypes", () => { + it("should return rule types and count", async () => { + const [ruleTypeResult, count] = await service.listAndCountRuleTypes() + + expect(count).toEqual(2) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("should return rule types and count when filtered", async () => { + const [ruleTypeResult, count] = await service.listAndCountRuleTypes({ + id: ["rule-type-1"], + }) + + expect(count).toEqual(1) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + ]) + }) + + it("should return rule types and count when using skip and take", async () => { + const [ruleTypeResult, count] = await service.listAndCountRuleTypes( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(2) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [ruleTypeResult, count] = await service.listAndCountRuleTypes( + {}, + { + take: 1, + select: ["name"], + } + ) + + const serialized = JSON.parse(JSON.stringify(ruleTypeResult)) + + expect(count).toEqual(2) + expect(serialized).toEqual([ + { + id: "rule-type-1", + name: "rule 1", + }, + ]) + }) + }) + + describe("retrieveRuleType", () => { + it("should return ruleType for the given id", async () => { + const ruleType = await service.retrieveRuleType("rule-type-1") + + expect(ruleType).toEqual( + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }) + ) + }) + + it("should throw an error when ruleType with id does not exist", async () => { + let error + + try { + await service.retrieveRuleType("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "RuleType 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.retrieveRuleType(undefined as unknown as string) + } catch (e) { + error = e + } + + expect(error.message).toEqual('"ruleTypeId" must be defined') + }) + + it("should return ruleType based on config select param", async () => { + const ruleTypeResult = await service.retrieveRuleType("rule-type-1", { + select: ["name"], + }) + + const serialized = JSON.parse(JSON.stringify(ruleTypeResult)) + + expect(serialized).toEqual({ + name: "rule 1", + id: "rule-type-1", + }) + }) + }) + + describe("deleteRuleTypes", () => { + const id = "rule-type-1" + + it("should delete the ruleTypes given an id successfully", async () => { + await service.deleteRuleTypes([id]) + + const currencies = await service.listRuleTypes({ + id: [id], + }) + + expect(currencies).toHaveLength(0) + }) + }) + + describe("updateRuleTypes", () => { + const id = "rule-type-1" + + it("should update the name of the ruleType successfully", async () => { + await service.updateRuleTypes([ + { + id, + name: "rule 3", + }, + ]) + + const ruletype = await service.retrieveRuleType(id) + + expect(ruletype.name).toEqual("rule 3") + }) + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.updateRuleTypes([ + { + id: "does-not-exist", + name: "rule 3", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'RuleType with id "does-not-exist" not found' + ) + }) + }) + + describe("createRuleTypes", () => { + it("should create a ruleType successfully", async () => { + await service.createRuleTypes([ + { + name: "Test Rule", + rule_attribute: "region_id", + }, + ]) + + const [ruleType] = await service.listRuleTypes({ + name: ["Test Rule"], + }) + + expect(ruleType).toEqual( + expect.objectContaining({ + name: "Test Rule", + rule_attribute: "region_id", + }) + ) + }) + }) +}) diff --git a/packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts new file mode 100644 index 0000000000..c6f8f21dab --- /dev/null +++ b/packages/pricing/integration-tests/__tests__/services/rule-type.ts/index.spec.ts @@ -0,0 +1,257 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { RuleTypeRepository } from "@repositories" +import { RuleTypeService } from "@services" + +import { MikroOrmWrapper } from "../../../utils" +import { createRuleTypes } from "../../../__fixtures__/rule-type" + +jest.setTimeout(30000) + +describe("RuleType Service", () => { + let service: RuleTypeService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + + const ruleTypeRepository = new RuleTypeRepository({ + manager: repositoryManager, + }) + + service = new RuleTypeService({ + ruleTypeRepository, + }) + + testManager = await MikroOrmWrapper.forkManager() + + await createRuleTypes(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("list", () => { + it("list rule types", async () => { + const ruleTypeResult = await service.list() + + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("list rule types by id", async () => { + const ruleTypeResult = await service.list({ id: ["rule-type-1"] }) + + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + ]) + }) + }) + + describe("listAndCount", () => { + it("should return rule types and count", async () => { + const [ruleTypeResult, count] = await service.listAndCount() + + expect(count).toEqual(2) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("should return rule types and count when filtered", async () => { + const [ruleTypeResult, count] = await service.listAndCount({ + id: ["rule-type-1"], + }) + + expect(count).toEqual(1) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }), + ]) + }) + + it("should return rule types and count when using skip and take", async () => { + const [ruleTypeResult, count] = await service.listAndCount( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(2) + expect(ruleTypeResult).toEqual([ + expect.objectContaining({ + id: "rule-type-2", + name: "rule 2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [ruleTypeResult, count] = await service.listAndCount( + {}, + { + take: 1, + select: ["name"], + } + ) + + const serialized = JSON.parse(JSON.stringify(ruleTypeResult)) + + expect(count).toEqual(2) + expect(serialized).toEqual([ + { + id: "rule-type-1", + name: "rule 1", + }, + ]) + }) + }) + + describe("retrieve", () => { + + it("should return ruleType for the given id", async () => { + const ruleType = await service.retrieve('rule-type-1') + + expect(ruleType).toEqual( + expect.objectContaining({ + id: "rule-type-1", + name: "rule 1", + }) + ) + }) + + it("should throw an error when ruleType with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "RuleType 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('"ruleTypeId" must be defined') + }) + + it("should return ruleType based on config select param", async () => { + 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' + }) + }) + }) + + describe("delete", () => { + const id = "rule-type-1" + + it("should delete the ruleTypes given an id successfully", async () => { + await service.delete([id]) + + const currencies = await service.list({ + id: [id], + }) + + expect(currencies).toHaveLength(0) + }) + }) + + describe("update", () => { + const id = "rule-type-1" + + it("should update the name of the ruleType successfully", async () => { + await service.update([ + { + id, + name: "rule 3", + }, + ]) + + const ruletype = await service.retrieve(id) + + expect(ruletype.name).toEqual("rule 3") + }) + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + name: "rule 3", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'RuleType with id "does-not-exist" not found' + ) + }) + }) + + describe("create", () => { + it("should create a ruleType successfully", async () => { + await service.create([ + { + name: "Test Rule", + rule_attribute: 'region_id', + }, + ]) + + const [ruleType] = await service.list({ + name: ["Test Rule"], + }) + + expect(ruleType).toEqual( + expect.objectContaining({ + name: "Test Rule", + rule_attribute: 'region_id', + }) + ) + }) + }) +}) diff --git a/packages/pricing/src/loaders/container.ts b/packages/pricing/src/loaders/container.ts index 7b1698480f..020322d01b 100644 --- a/packages/pricing/src/loaders/container.ts +++ b/packages/pricing/src/loaders/container.ts @@ -21,6 +21,7 @@ export default async ({ currencyService: asClass(defaultServices.CurrencyService).singleton(), moneyAmountService: asClass(defaultServices.MoneyAmountService).singleton(), priceSetService: asClass(defaultServices.PriceSetService).singleton(), + ruleTypeService: asClass(defaultServices.RuleTypeService).singleton(), }) if (customRepositories) { @@ -46,5 +47,8 @@ function loadDefaultRepositories({ container }) { priceSetRepository: asClass( defaultRepositories.PriceSetRepository ).singleton(), + ruleTypeRepository: asClass( + defaultRepositories.RuleTypeRepository + ).singleton(), }) } diff --git a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json index 0e55108cd4..c5b2fd0c72 100644 --- a/packages/pricing/src/migrations/.snapshot-medusa-pricing.json +++ b/packages/pricing/src/migrations/.snapshot-medusa-pricing.json @@ -185,13 +185,22 @@ "nullable": false, "mappedType": "text" }, + "title": { + "name": "title", + "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, + "nullable": false, "mappedType": "text" }, "money_amount_id": { @@ -200,15 +209,6 @@ "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" } @@ -217,13 +217,29 @@ "schema": "public", "indexes": [ { - "keyName": "price_set_money_amount_pkey", "columnNames": [ - "id", - "price_set_id", + "price_set_id" + ], + "composite": false, + "keyName": "IDX_price_set_money_amount_price_set_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ "money_amount_id" ], - "composite": true, + "composite": false, + "keyName": "IDX_price_set_money_amount_money_amount_id", + "primary": false, + "unique": false + }, + { + "keyName": "price_set_money_amount_pkey", + "columnNames": [ + "id" + ], + "composite": false, "primary": true, "unique": true } @@ -240,6 +256,7 @@ "id" ], "referencedTableName": "public.price_set", + "deleteRule": "cascade", "updateRule": "cascade" }, "price_set_money_amount_money_amount_id_foreign": { @@ -255,6 +272,71 @@ "updateRule": "cascade" } } + }, + { + "columns": { + "id": { + "name": "id", + "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" + }, + "rule_attribute": { + "name": "rule_attribute", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "default_priority": { + "name": "default_priority", + "type": "integer", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "integer" + } + }, + "name": "rule_type", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "rule_attribute" + ], + "composite": false, + "keyName": "IDX_rule_type_rule_attribute", + "primary": false, + "unique": false + }, + { + "keyName": "rule_type_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} } ] } diff --git a/packages/pricing/src/migrations/Migration20230907144224.ts b/packages/pricing/src/migrations/Migration20230913123118.ts similarity index 61% rename from packages/pricing/src/migrations/Migration20230907144224.ts rename to packages/pricing/src/migrations/Migration20230913123118.ts index 0085467840..fa5abed0d9 100644 --- a/packages/pricing/src/migrations/Migration20230907144224.ts +++ b/packages/pricing/src/migrations/Migration20230913123118.ts @@ -1,6 +1,6 @@ import { Migration } from "@mikro-orm/migrations" -export class Migration20230907144224 extends Migration { +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"));' @@ -9,6 +9,7 @@ export class Migration20230907144224 extends Migration { 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");' ) @@ -18,7 +19,23 @@ export class Migration20230907144224 extends Migration { ) 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"));' + '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( @@ -26,9 +43,8 @@ export class Migration20230907144224 extends Migration { ) 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;' + '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/models/index.ts b/packages/pricing/src/models/index.ts index 8504e5a196..08b2d18e18 100644 --- a/packages/pricing/src/models/index.ts +++ b/packages/pricing/src/models/index.ts @@ -2,3 +2,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" +export { default as RuleType } from "./rule-type" diff --git a/packages/pricing/src/models/price-set-money-amount.ts b/packages/pricing/src/models/price-set-money-amount.ts index 47b117e300..601e628d74 100644 --- a/packages/pricing/src/models/price-set-money-amount.ts +++ b/packages/pricing/src/models/price-set-money-amount.ts @@ -19,10 +19,15 @@ export default class PriceSetMoneyAmount { @Property({ columnType: "text" }) title!: string - @ManyToOne(() => PriceSet, { onDelete: "cascade" }) + @ManyToOne(() => PriceSet, { + onDelete: "cascade", + index: "IDX_price_set_money_amount_price_set_id", + }) price_set?: PriceSet - @ManyToOne(() => MoneyAmount, {}) + @ManyToOne(() => MoneyAmount, { + index: "IDX_price_set_money_amount_money_amount_id", + }) money_amount?: MoneyAmount @BeforeCreate() diff --git a/packages/pricing/src/models/rule-type.ts b/packages/pricing/src/models/rule-type.ts new file mode 100644 index 0000000000..0093919158 --- /dev/null +++ b/packages/pricing/src/models/rule-type.ts @@ -0,0 +1,34 @@ +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +type OptionalFields = "default_priority" + +@Entity() +class RuleType { + [OptionalProps]?: OptionalFields + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + name: string + + @Property({ columnType: "text", index: "IDX_rule_type_rule_attribute" }) + rule_attribute: string + + @Property({ columnType: "integer", default: 0 }) + default_priority: number + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "rul-typ") + } +} + +export default RuleType diff --git a/packages/pricing/src/repositories/index.ts b/packages/pricing/src/repositories/index.ts index f8689b004d..6e66f5cd35 100644 --- a/packages/pricing/src/repositories/index.ts +++ b/packages/pricing/src/repositories/index.ts @@ -2,3 +2,4 @@ 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" diff --git a/packages/pricing/src/repositories/rule-type.ts b/packages/pricing/src/repositories/rule-type.ts new file mode 100644 index 0000000000..7cab1bf8ff --- /dev/null +++ b/packages/pricing/src/repositories/rule-type.ts @@ -0,0 +1,127 @@ +import { + Context, + CreateRuleTypeDTO, + DAL, + UpdateRuleTypeDTO, +} 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 { RuleType } from "@models" + +export class RuleTypeRepository 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( + RuleType, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise<[RuleType[], number]> { + const manager = this.getActiveManager(context) + + const findOptions_ = { ...findOptions } + findOptions_.options ??= {} + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await manager.findAndCount( + RuleType, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(RuleType, { id: { $in: ids } }, {}) + } + + async create( + data: CreateRuleTypeDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const ruleTypes = data.map((ruleTypeData) => { + return manager.create(RuleType, ruleTypeData) + }) + + manager.persist(ruleTypes) + + return ruleTypes + } + + async update( + data: UpdateRuleTypeDTO[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const ruleTypeIds = data.map((ruleType) => ruleType.id) + const existingRuleTypes = await this.find( + { + where: { + id: { + $in: ruleTypeIds, + }, + }, + }, + context + ) + + const existingRuleTypesMap = new Map( + existingRuleTypes.map<[string, RuleType]>((ruleType) => [ + ruleType.id, + ruleType, + ]) + ) + + const ruleTypes = data.map((ruleTypeData) => { + const existingRuleType = existingRuleTypesMap.get(ruleTypeData.id) + + if (!existingRuleType) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `RuleType with id "${ruleTypeData.id}" not found` + ) + } + + return manager.assign(existingRuleType, ruleTypeData) + }) + + manager.persist(ruleTypes) + + return ruleTypes + } +} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index a524635fa7..4b5f3692a1 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -2,3 +2,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" +export { default as RuleTypeService } from "./rule-type" \ No newline at end of file diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index da4fdd3cee..e47943563e 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -8,34 +8,42 @@ import { PricingFilters, PricingTypes, } from "@medusajs/types" -import { Currency, MoneyAmount, PriceSet } from "@models" -import { CurrencyService, MoneyAmountService, PriceSetService } from "@services" +import { Currency, MoneyAmount, PriceSet, RuleType } from "@models" +import { + CurrencyService, + MoneyAmountService, + PriceSetService, + RuleTypeService, +} from "@services" import { InjectManager, InjectTransactionManager, MedusaContext, + shouldForceTransaction, } from "@medusajs/utils" -import { shouldForceTransaction } from "@medusajs/utils" import { joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService currencyService: CurrencyService moneyAmountService: MoneyAmountService + ruleTypeService: RuleTypeService priceSetService: PriceSetService } export default class PricingModuleService< TPriceSet extends PriceSet = PriceSet, TMoneyAmount extends MoneyAmount = MoneyAmount, - TCurrency extends Currency = Currency + TCurrency extends Currency = Currency, + TRuleType extends RuleType = RuleType > implements PricingTypes.IPricingModuleService { protected baseRepository_: DAL.RepositoryService protected readonly currencyService_: CurrencyService protected readonly moneyAmountService_: MoneyAmountService + protected readonly ruleTypeService_: RuleTypeService protected readonly priceSetService_: PriceSetService constructor( @@ -43,6 +51,7 @@ export default class PricingModuleService< baseRepository, moneyAmountService, currencyService, + ruleTypeService, priceSetService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration @@ -50,6 +59,7 @@ export default class PricingModuleService< this.baseRepository_ = baseRepository this.currencyService_ = currencyService this.moneyAmountService_ = moneyAmountService + this.ruleTypeService_ = ruleTypeService this.priceSetService_ = priceSetService } @@ -412,4 +422,102 @@ export default class PricingModuleService< ): Promise { await this.currencyService_.delete(currencyCodes, sharedContext) } + + @InjectManager("baseRepository_") + async retrieveRuleType( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const ruleType = await this.ruleTypeService_.retrieve( + id, + config, + sharedContext + ) + + return this.baseRepository_.serialize(ruleType, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async listRuleTypes( + filters: PricingTypes.FilterableRuleTypeProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const ruleTypes = await this.ruleTypeService_.list( + filters, + config, + sharedContext + ) + + return this.baseRepository_.serialize( + ruleTypes, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async listAndCountRuleTypes( + filters: PricingTypes.FilterableRuleTypeProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[PricingTypes.RuleTypeDTO[], number]> { + const [ruleTypes, count] = await this.ruleTypeService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + ruleTypes, + { + populate: true, + } + ), + count, + ] + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async createRuleTypes( + data: PricingTypes.CreateRuleTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const ruleTypes = await this.ruleTypeService_.create(data, sharedContext) + + return this.baseRepository_.serialize( + ruleTypes, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async updateRuleTypes( + data: PricingTypes.UpdateRuleTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const ruleTypes = await this.ruleTypeService_.update(data, sharedContext) + + return this.baseRepository_.serialize( + ruleTypes, + { + populate: true, + } + ) + } + + @InjectTransactionManager(shouldForceTransaction, "baseRepository_") + async deleteRuleTypes( + ruleTypes: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.ruleTypeService_.delete(ruleTypes, sharedContext) + } } diff --git a/packages/pricing/src/services/rule-type.ts b/packages/pricing/src/services/rule-type.ts new file mode 100644 index 0000000000..ffecebce50 --- /dev/null +++ b/packages/pricing/src/services/rule-type.ts @@ -0,0 +1,106 @@ +import { Context, DAL, FindConfig, PricingTypes } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + ModulesSdkUtils, + retrieveEntity, +} from "@medusajs/utils" +import { RuleType } from "@models" + +import { doNotForceTransaction, shouldForceTransaction } from "@medusajs/utils" + +type InjectedDependencies = { + ruleTypeRepository: DAL.RepositoryService +} + +export default class RuleTypeService { + protected readonly ruleTypeRepository_: DAL.RepositoryService + + constructor({ ruleTypeRepository }: InjectedDependencies) { + this.ruleTypeRepository_ = ruleTypeRepository + } + + @InjectManager("ruleTypeRepository_") + async retrieve( + ruleTypeId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await retrieveEntity({ + id: ruleTypeId, + identifierColumn: "id", + entityName: RuleType.name, + repository: this.ruleTypeRepository_, + config, + sharedContext, + })) as TEntity + } + + @InjectManager("ruleTypeRepository_") + async list( + filters: PricingTypes.FilterableRuleTypeProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.ruleTypeRepository_.find( + this.buildQueryForList(filters, config), + sharedContext + )) as TEntity[] + } + + @InjectManager("ruleTypeRepository_") + async listAndCount( + filters: PricingTypes.FilterableRuleTypeProps = {}, + 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 + } + + @InjectTransactionManager(shouldForceTransaction, "ruleTypeRepository_") + async create( + data: PricingTypes.CreateRuleTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.ruleTypeRepository_.create( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(shouldForceTransaction, "ruleTypeRepository_") + async update( + data: PricingTypes.UpdateRuleTypeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await this.ruleTypeRepository_.update( + data, + sharedContext + )) as TEntity[] + } + + @InjectTransactionManager(doNotForceTransaction, "ruleTypeRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.ruleTypeRepository_.delete(ids, sharedContext) + } +} diff --git a/packages/types/src/pricing/common/index.ts b/packages/types/src/pricing/common/index.ts index f036d6bdfa..b50ecce4ba 100644 --- a/packages/types/src/pricing/common/index.ts +++ b/packages/types/src/pricing/common/index.ts @@ -1,3 +1,4 @@ export * from "./currency" export * from "./money-amount" export * from "./price-set" +export * from "./rule-type" diff --git a/packages/types/src/pricing/common/rule-type.ts b/packages/types/src/pricing/common/rule-type.ts new file mode 100644 index 0000000000..dbc9619f3a --- /dev/null +++ b/packages/types/src/pricing/common/rule-type.ts @@ -0,0 +1,28 @@ +import { BaseFilterable } from "../../dal" + +export interface RuleTypeDTO { + id: string + name: string + rule_attribute: string + default_priority: number +} + +export interface CreateRuleTypeDTO { + id?: string + name: string + rule_attribute: string + default_priority?: number +} + +export interface UpdateRuleTypeDTO { + id: string + name?: string + rule_attribute?: string + default_priority?: number +} + +export interface FilterableRuleTypeProps + extends BaseFilterable { + id?: string[] + name?: string[] +} diff --git a/packages/types/src/pricing/index.ts b/packages/types/src/pricing/index.ts index eade309433..5d6d17eded 100644 --- a/packages/types/src/pricing/index.ts +++ b/packages/types/src/pricing/index.ts @@ -1,2 +1,2 @@ export * from "./common" -export * from "./service" +export * from "./service" \ No newline at end of file diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index 0f87e40abc..2d164a0500 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -6,17 +6,21 @@ import { CreateCurrencyDTO, CreateMoneyAmountDTO, CreatePriceSetDTO, + CreateRuleTypeDTO, CurrencyDTO, FilterableCurrencyProps, FilterableMoneyAmountProps, FilterablePriceSetProps, + FilterableRuleTypeProps, MoneyAmountDTO, PriceSetDTO, PricingContext, PricingFilters, + RuleTypeDTO, UpdateCurrencyDTO, UpdateMoneyAmountDTO, UpdatePriceSetDTO, + UpdateRuleTypeDTO, } from "./common" export interface IPricingModuleService { @@ -120,4 +124,34 @@ export interface IPricingModuleService { currencyCodes: string[], sharedContext?: Context ): Promise + + retrieveRuleType( + code: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listRuleTypes( + filters?: FilterableRuleTypeProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCountRuleTypes( + filters?: FilterableRuleTypeProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[RuleTypeDTO[], number]> + + createRuleTypes( + data: CreateRuleTypeDTO[], + sharedContext?: Context + ): Promise + + updateRuleTypes( + data: UpdateRuleTypeDTO[], + sharedContext?: Context + ): Promise + + deleteRuleTypes(ruleTypes: string[], sharedContext?: Context): Promise }