From 2b62686ec6aca1460528630d2cbbb3193f04fb18 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Wed, 5 Jun 2024 17:33:49 +0530 Subject: [PATCH] implement events to the pricing module (#7584) --- packages/core/modules-sdk/src/definitions.ts | 2 +- packages/core/utils/src/pricing/events.ts | 27 +++ packages/core/utils/src/pricing/index.ts | 1 + .../pricing-module/price-list.spec.ts | 78 ++++++- .../services/pricing-module/price-set.spec.ts | 73 +++++- .../pricing/src/services/pricing-module.ts | 218 +++++++++++++++++- packages/modules/pricing/src/utils/events.ts | 52 +++++ packages/modules/pricing/src/utils/index.ts | 1 + 8 files changed, 435 insertions(+), 17 deletions(-) create mode 100644 packages/core/utils/src/pricing/events.ts create mode 100644 packages/modules/pricing/src/utils/events.ts diff --git a/packages/core/modules-sdk/src/definitions.ts b/packages/core/modules-sdk/src/definitions.ts index 4aaf9eeaa8..b10dcd2a9a 100644 --- a/packages/core/modules-sdk/src/definitions.ts +++ b/packages/core/modules-sdk/src/definitions.ts @@ -159,7 +159,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = label: upperCaseFirst(ModuleRegistrationName.PRICING), isRequired: false, isQueryable: true, - dependencies: ["logger"], + dependencies: [ModuleRegistrationName.EVENT_BUS, "logger"], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, diff --git a/packages/core/utils/src/pricing/events.ts b/packages/core/utils/src/pricing/events.ts new file mode 100644 index 0000000000..4597c30308 --- /dev/null +++ b/packages/core/utils/src/pricing/events.ts @@ -0,0 +1,27 @@ +import { buildEventNamesFromEntityName } from "../event-bus" +import { Modules } from "../modules-sdk" + +const eventBaseNames: [ + "priceListRuleValue", + "priceListRule", + "priceList", + "priceRule", + "priceSetRuleType", + "priceSet", + "price", + "ruleType" +] = [ + "priceListRuleValue", + "priceListRule", + "priceList", + "priceRule", + "priceSetRuleType", + "priceSet", + "price", + "ruleType", +] + +export const PricingEvents = buildEventNamesFromEntityName( + eventBaseNames, + Modules.PRICING +) diff --git a/packages/core/utils/src/pricing/index.ts b/packages/core/utils/src/pricing/index.ts index 02a4467e94..258ed59fe2 100644 --- a/packages/core/utils/src/pricing/index.ts +++ b/packages/core/utils/src/pricing/index.ts @@ -1,3 +1,4 @@ export * from "./builders" export * from "./price-list" export * from "./rule-type" +export * from "./events" diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts index d9847cf63a..72ac1df640 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts @@ -1,8 +1,13 @@ import { Modules } from "@medusajs/modules-sdk" import { IPricingModuleService } from "@medusajs/types" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { + MockEventBusService, + moduleIntegrationTestRunner, + SuiteOptions, +} from "medusa-test-utils" import { createPriceLists } from "../../../__fixtures__/price-list" import { createPriceSets } from "../../../__fixtures__/price-set" +import { CommonEvents, composeMessage, PricingEvents } from "@medusajs/utils" jest.setTimeout(30000) @@ -12,6 +17,16 @@ moduleIntegrationTestRunner({ MikroOrmWrapper, service, }: SuiteOptions) => { + let eventBusEmitSpy + + beforeEach(() => { + eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") + }) + + afterEach(() => { + jest.clearAllMocks() + }) + describe("PriceList Service", () => { beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() @@ -400,8 +415,8 @@ moduleIntegrationTestRunner({ { title: "test", description: "test", - starts_at: new Date("10/01/2023"), - ends_at: new Date("10/30/2023"), + starts_at: new Date("10/01/2023").toISOString(), + ends_at: new Date("10/30/2023").toISOString(), rules: { customer_group_id: [ "vip-customer-group-id", @@ -488,6 +503,41 @@ moduleIntegrationTestRunner({ ]), }) ) + + const events = eventBusEmitSpy.mock.calls[0][0] + expect(events).toHaveLength(4) + expect(events[0]).toEqual( + composeMessage(PricingEvents.price_list_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list", + data: { id: priceList.id }, + }) + ) + expect(events[1]).toEqual( + composeMessage(PricingEvents.price_list_rule_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list_rule", + data: { id: priceList.price_list_rules?.[0].id }, + }) + ) + expect(events[2]).toEqual( + composeMessage(PricingEvents.price_list_rule_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list_rule", + data: { id: priceList.price_list_rules?.[1].id }, + }) + ) + expect(events[3]).toEqual( + composeMessage(PricingEvents.price_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + data: { id: priceList.prices![0].id }, + }) + ) }) it("should create a price list with granular rules within prices", async () => { @@ -745,6 +795,8 @@ moduleIntegrationTestRunner({ }, ]) + jest.clearAllMocks() + await service.addPriceListPrices([ { price_list_id: "price-list-1", @@ -809,6 +861,26 @@ moduleIntegrationTestRunner({ price_list_rules: [], }) ) + + const events = eventBusEmitSpy.mock.calls[0][0] + + expect(events).toHaveLength(2) + expect(events[0]).toEqual( + composeMessage(PricingEvents.price_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + data: { id: priceList.prices![0].id }, + }) + ) + expect(events[1]).toEqual( + composeMessage(PricingEvents.price_rule_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + data: { id: priceList.prices![0].price_rules![0].id }, + }) + ) }) }) diff --git a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts index e23c6e7487..8bf0751ab3 100644 --- a/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts +++ b/packages/modules/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -5,9 +5,14 @@ import { IPricingModuleService, } from "@medusajs/types" import { SqlEntityManager } from "@mikro-orm/postgresql" -import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" +import { + MockEventBusService, + moduleIntegrationTestRunner, + SuiteOptions, +} from "medusa-test-utils" import { PriceSetRuleType } from "../../../../src" import { seedPriceData } from "../../../__fixtures__/seed-price-data" +import { CommonEvents, PricingEvents, composeMessage } from "@medusajs/utils" jest.setTimeout(30000) @@ -32,6 +37,16 @@ moduleIntegrationTestRunner({ MikroOrmWrapper, service, }: SuiteOptions) => { + let eventBusEmitSpy + + beforeEach(() => { + eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") + }) + + afterEach(() => { + jest.clearAllMocks() + }) + describe("PricingModule Service - PriceSet", () => { beforeEach(async () => { const testManager = await MikroOrmWrapper.forkManager() @@ -422,6 +437,41 @@ moduleIntegrationTestRunner({ ], }) ) + + const [priceRules] = await service.listPriceRules({ + price_set_id: [priceSet.id], + }) + + const events = eventBusEmitSpy.mock.calls[0][0] + expect(events).toHaveLength(3) + expect(events[0]).toEqual( + composeMessage(PricingEvents.price_set_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_set", + data: { id: priceSet.id }, + }) + ) + + expect(events[1]).toEqual( + composeMessage(PricingEvents.price_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + data: { id: priceSet.prices![0].id }, + }) + ) + + expect(events[2]).toEqual( + composeMessage(PricingEvents.price_rule_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + data: { + id: priceRules.id, + }, + }) + ) }) it("should create a price set with money amounts with and without rules", async () => { @@ -625,7 +675,7 @@ moduleIntegrationTestRunner({ const [priceSet] = await service.list( { id: ["price-set-1"] }, - { relations: ["prices"] } + { relations: ["prices", "prices.price_rules"] } ) expect(priceSet).toEqual( @@ -639,6 +689,25 @@ moduleIntegrationTestRunner({ ]), }) ) + + const events = eventBusEmitSpy.mock.calls[0][0] + expect(events).toHaveLength(2) + expect(events[0]).toEqual( + composeMessage(PricingEvents.price_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + data: { id: priceSet.prices![1].id }, + }) + ) + expect(events[1]).toEqual( + composeMessage(PricingEvents.price_rule_created, { + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + data: { id: priceSet.prices![1].price_rules[0].id }, + }) + ) }) it("should add prices to multiple existing price set", async () => { diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 7318d0ce4e..67fe6dc687 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -21,6 +21,7 @@ import { import { arrayDifference, deduplicate, + EmitEvents, generateEntityId, groupBy, InjectManager, @@ -47,11 +48,11 @@ import { } from "@models" import { PriceListService, RuleTypeService } from "@services" -import { validatePriceListDates } from "@utils" -import { UpdatePriceSetInput } from "src/types/services" +import { validatePriceListDates, eventBuilders } from "@utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { PriceListIdPrefix } from "../models/price-list" import { PriceSetIdPrefix } from "../models/price-set" +import { ServiceTypes } from "@types" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -326,6 +327,7 @@ export default class PricingModuleService< ): Promise @InjectManager("baseRepository_") + @EmitEvents() async create( data: PricingTypes.CreatePriceSetDTO | PricingTypes.CreatePriceSetDTO[], @MedusaContext() sharedContext: Context = {} @@ -336,7 +338,14 @@ export default class PricingModuleService< // TODO: Remove the need to refetch the data here const dbPriceSets = await this.list( { id: priceSets.map((p) => p.id) }, - { relations: ["rule_types", "prices", "price_rules"] }, + { + relations: [ + "rule_types", + "prices", + "price_rules", + "prices.price_rules", + ], + }, sharedContext ) @@ -366,7 +375,7 @@ export default class PricingModuleService< ): Promise { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( - (priceSet): priceSet is UpdatePriceSetInput => !!priceSet.id + (priceSet): priceSet is ServiceTypes.UpdatePriceSetInput => !!priceSet.id ) const forCreate = input.filter( (priceSet): priceSet is CreatePriceSetDTO => !priceSet.id @@ -404,7 +413,7 @@ export default class PricingModuleService< data: PricingTypes.UpdatePriceSetDTO, @MedusaContext() sharedContext: Context = {} ): Promise { - let normalizedInput: UpdatePriceSetInput[] = [] + let normalizedInput: ServiceTypes.UpdatePriceSetInput[] = [] if (isString(idOrSelector)) { // Check if the ID exists, it will throw if not. await this.priceSetService_.retrieve(idOrSelector, {}, sharedContext) @@ -431,7 +440,7 @@ export default class PricingModuleService< } private async normalizeUpdateData( - data: UpdatePriceSetInput[], + data: ServiceTypes.UpdatePriceSetInput[], sharedContext ) { const ruleAttributes = data @@ -480,7 +489,7 @@ export default class PricingModuleService< @InjectTransactionManager("baseRepository_") protected async update_( - data: UpdatePriceSetInput[], + data: ServiceTypes.UpdatePriceSetInput[], @MedusaContext() sharedContext: Context = {} ): Promise { // TODO: We are not handling rule types, rules, etc. here, add support after data models are finalized @@ -560,6 +569,7 @@ export default class PricingModuleService< ): Promise @InjectManager("baseRepository_") + @EmitEvents() async addPrices( data: AddPricesDTO | AddPricesDTO[], @MedusaContext() sharedContext: Context = {} @@ -626,6 +636,7 @@ export default class PricingModuleService< } @InjectManager("baseRepository_") + @EmitEvents() async createPriceLists( data: PricingTypes.CreatePriceListDTO[], @MedusaContext() sharedContext: Context = {} @@ -668,6 +679,7 @@ export default class PricingModuleService< } @InjectManager("baseRepository_") + @EmitEvents() async addPriceListPrices( data: PricingTypes.AddPriceListPricesDTO[], @MedusaContext() sharedContext: Context = {} @@ -823,6 +835,49 @@ export default class PricingModuleService< sharedContext ) + const eventsData = createdPriceSets.reduce( + (eventsData, priceSet) => { + eventsData.priceSets.push({ + id: priceSet.id, + }) + + priceSet.prices.map((price) => { + eventsData.prices.push({ + id: price.id, + }) + price.price_rules.map((priceRule) => { + eventsData.priceRules.push({ + id: priceRule.id, + }) + }) + }) + + return eventsData + }, + { + priceSets: [], + priceRules: [], + prices: [], + } as { + priceSets: { id: string }[] + priceRules: { id: string }[] + prices: { id: string }[] + } + ) + + eventBuilders.createdPriceSet({ + data: eventsData.priceSets, + sharedContext, + }) + eventBuilders.createdPrice({ + data: eventsData.prices, + sharedContext, + }) + eventBuilders.createdPriceRule({ + data: eventsData.priceRules, + sharedContext, + }) + if (ruleSetRuleTypeToCreateMap.size) { await this.priceSetRuleTypeService_.create( Array.from(ruleSetRuleTypeToCreateMap.values()), @@ -982,12 +1037,53 @@ export default class PricingModuleService< price_set_id: priceSetId, title: "test", // TODO: accept title rules_count: numberOfRules, - priceRules, + price_rules: priceRules, } }) ) - await this.priceService_.create(pricesToCreate, sharedContext) + const prices = await this.priceService_.create( + pricesToCreate, + sharedContext + ) + + /** + * Preparing data for emitting events + */ + const eventsData = prices.reduce( + (eventsData, price) => { + eventsData.prices.push({ + id: price.id, + }) + price.price_rules.map((priceRule) => { + eventsData.priceRules.push({ + id: priceRule.id, + }) + }) + return eventsData + }, + { + priceRules: [], + prices: [], + } as { + priceRules: { id: string }[] + prices: { id: string }[] + } + ) + + /** + * Emitting events for all created entities + */ + eventBuilders.createdPrice({ + data: eventsData.prices, + sharedContext, + }) + eventBuilders.createdPriceRule({ + data: eventsData.priceRules, + sharedContext, + }) + + return prices } @InjectTransactionManager("baseRepository_") @@ -1085,10 +1181,73 @@ export default class PricingModuleService< } ) - return await this.priceListService_.create( + const priceLists = await this.priceListService_.create( priceListsToCreate, sharedContext ) + + /** + * Preparing data for emitting events + */ + const eventsData = priceLists.reduce( + (eventsData, priceList) => { + eventsData.priceList.push({ + id: priceList.id, + }) + + priceList.price_list_rules.map((listRule) => { + eventsData.priceListRules.push({ + id: listRule.id, + }) + }) + + priceList.prices.map((price) => { + eventsData.prices.push({ + id: price.id, + }) + price.price_rules.map((priceRule) => { + eventsData.priceRules.push({ + id: priceRule.id, + }) + }) + }) + + return eventsData + }, + { + priceList: [], + priceListRules: [], + priceRules: [], + prices: [], + } as { + priceList: { id: string }[] + priceListRules: { id: string }[] + priceRules: { id: string }[] + prices: { id: string }[] + } + ) + + /** + * Emitting events for all created entities + */ + eventBuilders.createdPriceList({ + data: eventsData.priceList, + sharedContext, + }) + eventBuilders.createdPriceListRule({ + data: eventsData.priceListRules, + sharedContext, + }) + eventBuilders.createdPrice({ + data: eventsData.prices, + sharedContext, + }) + eventBuilders.createdPriceRule({ + data: eventsData.priceRules, + sharedContext, + }) + + return priceLists } @InjectTransactionManager("baseRepository_") @@ -1468,7 +1627,44 @@ export default class PricingModuleService< pricesToCreate.push(...priceListPricesToCreate) } - return await this.priceService_.create(pricesToCreate, sharedContext) + const createdPrices = await this.priceService_.create( + pricesToCreate, + sharedContext + ) + + const eventsData = createdPrices.reduce( + (eventsData, price) => { + eventsData.prices.push({ + id: price.id, + }) + + price.price_rules.map((priceRule) => { + eventsData.priceRules.push({ + id: priceRule.id, + }) + }) + + return eventsData + }, + { + priceRules: [], + prices: [], + } as { + priceRules: { id: string }[] + prices: { id: string }[] + } + ) + + eventBuilders.createdPrice({ + data: eventsData.prices, + sharedContext, + }) + eventBuilders.createdPriceRule({ + data: eventsData.priceRules, + sharedContext, + }) + + return createdPrices } @InjectTransactionManager("baseRepository_") diff --git a/packages/modules/pricing/src/utils/events.ts b/packages/modules/pricing/src/utils/events.ts new file mode 100644 index 0000000000..5126c28060 --- /dev/null +++ b/packages/modules/pricing/src/utils/events.ts @@ -0,0 +1,52 @@ +import {} from "@models" +import { + Modules, + CommonEvents, + PricingEvents, + eventBuilderFactory, +} from "@medusajs/utils" + +export const eventBuilders = { + createdPriceSet: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_set", + eventsEnum: PricingEvents, + }), + createdPriceSetRuleType: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_set_rule_type", + eventsEnum: PricingEvents, + }), + createdPrice: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + eventsEnum: PricingEvents, + }), + createdPriceRule: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + eventsEnum: PricingEvents, + }), + createdPriceList: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list", + eventsEnum: PricingEvents, + }), + createdPriceListRule: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list_rule", + eventsEnum: PricingEvents, + }), + attachedPriceListRule: eventBuilderFactory({ + service: Modules.PRICING, + action: CommonEvents.ATTACHED, + object: "price_list_rule", + eventsEnum: PricingEvents, + }), +} diff --git a/packages/modules/pricing/src/utils/index.ts b/packages/modules/pricing/src/utils/index.ts index 5c60adb105..0bdeeb988b 100644 --- a/packages/modules/pricing/src/utils/index.ts +++ b/packages/modules/pricing/src/utils/index.ts @@ -1 +1,2 @@ export * from "./validate-price-list-dates" +export * from "./events"