From e8822f3e693bde33fdfd8b0d6140b4e59b40af98 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 10 Sep 2025 14:37:38 +0200 Subject: [PATCH] chore(): Module Internal Events (#13296) * chore(): Ensure the product module emits all necessary events * chore(): Ensure the product module emits all necessary events * Update events tests * more events and fixes * more tests and category fixes * more tests and category fixes * Add todo * update updateProduct_ event emitting and adjust test * Adjust update products implementation to rely on already computed events * rm unnecessary update variants events * Fix formatting in changeset for product events * refactor: Manage event emitting automatically (WIP) * refactor: Manage event emitting automatically (WIP) * chore(api-key): Add missing emit events and refactoring * chore(cart): Add missing emit events and refactoring * chore(customer): Add missing emit events and refactoring * chore(fufillment, utils): Add missing emit events and refactoring * chore(fufillment, utils): Add missing emit events and refactoring * chore(inventory): Add missing emit events and refactoring * chore(notification): Add missing emit events and refactoring * chore(utils): Remove medusa service event handling legacy * chore(product): Add missing emit events and refactoring * chore(order): Add missing emit events and refactoring * chore(payment): Add missing emit events and refactoring * chore(pricing, util): Add missing emit events and refactoring, fix internal service upsertWithReplace event dispatching * chore(promotions): Add missing emit events and refactoring * chore(region): Add missing emit events and refactoring * chore(sales-channel): Add missing emit events and refactoring * chore(settings): Add missing emit events and refactoring * chore(stock-location): Add missing emit events and refactoring * chore(store): Add missing emit events and refactoring * chore(taxes): Add missing emit events and refactoring * chore(user): Add missing emit events and refactoring * fix unit tests * rm changeset for regeneration * Create changeset for Medusa.js patch updates Add a changeset for patch updates to multiple Medusa.js modules. * rm unused product event builders * address feedback * remove old changeset * fix event action for token generated * fix user module events * fix import * fix promotion events * add new module integration tests shard * fix medusa service * revert shard * fix event action * fix pipeline * fix pipeline --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/angry-bikes-perform.md | 15 + .../utils/src/event-bus/message-aggregator.ts | 28 +- .../__tests__/medusa-internal-service.ts | 4 +- ...reate-medusa-mikro-orm-event-subscriber.ts | 71 + packages/core/utils/src/modules-sdk/index.ts | 1 + .../modules-sdk/medusa-internal-service.ts | 182 ++- .../utils/src/modules-sdk/medusa-service.ts | 190 +-- .../src/modules-sdk/types/medusa-service.ts | 23 +- .../src/services/api-key-module-service.ts | 36 +- .../modules/cart/src/services/cart-module.ts | 20 + .../customer/src/services/customer-module.ts | 179 ++- .../fulfillment-set.spec.ts | 54 +- .../fulfillment.spec.ts | 36 +- .../geo-zone.spec.ts | 8 +- .../service-zone.spec.ts | 6 +- .../shipping-option.spec.ts | 190 +-- .../shipping-profile.spec.ts | 6 +- .../services/fulfillment-module-service.ts | 331 +---- .../inventory-module-service.spec.ts | 40 +- .../src/services/inventory-module.ts | 219 +--- .../notification-module-service/index.spec.ts | 20 +- .../services/notification-module-service.ts | 6 - .../modules/notification/src/utils/events.ts | 15 - .../modules/notification/src/utils/index.ts | 1 - .../src/services/order-module-service.ts | 883 +++++++++---- .../payment/src/services/payment-module.ts | 75 +- .../pricing-module/price-list.spec.ts | 115 +- .../pricing/src/services/pricing-module.ts | 464 +++---- packages/modules/pricing/src/utils/events.ts | 99 -- packages/modules/pricing/src/utils/index.ts | 1 - .../product/data/create-product.ts | 5 +- ...t-category.ts => product-category.spec.ts} | 0 .../product-module-service/events.spec.ts | 1150 +++++++++++++++++ .../product-categories.spec.ts | 77 -- .../product-collections.spec.ts | 77 -- .../product-tags.spec.ts | 61 - .../product-variants.spec.ts | 41 - .../product-module-service/products.spec.ts | 130 -- .../__tests__/{product.ts => product.spec.ts} | 0 packages/modules/product/package.json | 2 +- .../src/repositories/product-category.ts | 4 +- .../product/src/repositories/product.ts | 12 +- .../product/src/services/product-category.ts | 58 +- .../src/services/product-module-service.ts | 384 +++--- packages/modules/product/src/utils/events.ts | 126 -- .../src/services/promotion-module.ts | 17 +- .../region/src/services/region-module.ts | 24 +- .../src/services/sales-channel-module.ts | 32 +- .../src/services/settings-module-service.ts | 58 +- .../src/services/stock-location-module.ts | 14 +- .../src/services/store-module-service.ts | 26 +- .../tax/src/services/tax-module-service.ts | 202 ++- .../__tests__/invite.spec.ts | 57 +- .../integration-tests/__tests__/user.spec.ts | 21 +- .../modules/user/src/services/user-module.ts | 71 +- 55 files changed, 3614 insertions(+), 2353 deletions(-) create mode 100644 .changeset/angry-bikes-perform.md create mode 100644 packages/core/utils/src/modules-sdk/create-medusa-mikro-orm-event-subscriber.ts delete mode 100644 packages/modules/notification/src/utils/events.ts delete mode 100644 packages/modules/notification/src/utils/index.ts delete mode 100644 packages/modules/pricing/src/utils/events.ts rename packages/modules/product/integration-tests/__tests__/{product-category.ts => product-category.spec.ts} (100%) create mode 100644 packages/modules/product/integration-tests/__tests__/product-module-service/events.spec.ts rename packages/modules/product/integration-tests/__tests__/{product.ts => product.spec.ts} (100%) diff --git a/.changeset/angry-bikes-perform.md b/.changeset/angry-bikes-perform.md new file mode 100644 index 0000000000..57fdf647db --- /dev/null +++ b/.changeset/angry-bikes-perform.md @@ -0,0 +1,15 @@ +--- +"@medusajs/api-key": patch +"@medusajs/cart": patch +"@medusajs/customer": patch +"@medusajs/fulfillment": patch +"@medusajs/inventory": patch +"@medusajs/notification": patch +"@medusajs/order": patch +"@medusajs/payment": patch +"@medusajs/pricing": patch +"@medusajs/product": patch +"@medusajs/utils": patch +--- + +chore(): Module Internal Events diff --git a/packages/core/utils/src/event-bus/message-aggregator.ts b/packages/core/utils/src/event-bus/message-aggregator.ts index 4c70127273..8436061096 100644 --- a/packages/core/utils/src/event-bus/message-aggregator.ts +++ b/packages/core/utils/src/event-bus/message-aggregator.ts @@ -9,12 +9,13 @@ import { import { composeMessage } from "./build-event-messages" export class MessageAggregator implements IMessageAggregator { - private messages: Message[] = [] + #messagesHash: Set = new Set() + #messages: Message[] = [] constructor() {} count(): number { - return this.messages.length + return this.#messages.length } save(msg: Message | Message[]): void { @@ -23,7 +24,19 @@ export class MessageAggregator implements IMessageAggregator { return } - this.messages.push(...messages) + for (const message of messages) { + try { + const hash = JSON.stringify(message) + if (!this.#messagesHash.has(hash)) { + this.#messagesHash.add(hash) + this.#messages.push(message) + } + } catch (e) { + // noop: if the message is not serializable, we don't want to deduplicate it + // It should not happen, but we don't want to fail the whole process + this.#messages.push(message) + } + } } saveRawMessageData( @@ -55,15 +68,15 @@ export class MessageAggregator implements IMessageAggregator { const { groupBy, sortBy } = format ?? {} if (sortBy) { - this.messages.sort((a, b) => this.compareMessages(a, b, sortBy)) + this.#messages.sort((a, b) => this.compareMessages(a, b, sortBy)) } let messages: { [group: string]: Message[] } = { - default: [...this.messages], + default: [...this.#messages], } if (groupBy) { - messages = this.messages.reduce<{ + messages = this.#messages.reduce<{ [key: string]: Message[] }>((acc, msg) => { const key = groupBy @@ -91,7 +104,8 @@ export class MessageAggregator implements IMessageAggregator { clearMessages(): void { // Ensure no references are left over in case something rely on messages - this.messages.length = 0 + this.#messages.length = 0 + this.#messagesHash.clear() } private getValueFromPath(obj: any, path: string): any { diff --git a/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts index eb7eeef4e0..6044862726 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/medusa-internal-service.ts @@ -20,7 +20,7 @@ describe("Internal Module Service Factory", () => { findAndCount: jest.fn(), create: jest.fn(), update: jest.fn(), - delete: jest.fn(), + delete: jest.fn().mockImplementation((ids) => Promise.resolve(ids)), softDelete: jest.fn(), restore: jest.fn(), upsert: jest.fn(), @@ -32,7 +32,7 @@ describe("Internal Module Service Factory", () => { findAndCount: jest.fn(), create: jest.fn(), update: jest.fn(), - delete: jest.fn(), + delete: jest.fn().mockImplementation((ids) => Promise.resolve(ids)), softDelete: jest.fn(), restore: jest.fn(), upsert: jest.fn(), diff --git a/packages/core/utils/src/modules-sdk/create-medusa-mikro-orm-event-subscriber.ts b/packages/core/utils/src/modules-sdk/create-medusa-mikro-orm-event-subscriber.ts new file mode 100644 index 0000000000..832c56a7fa --- /dev/null +++ b/packages/core/utils/src/modules-sdk/create-medusa-mikro-orm-event-subscriber.ts @@ -0,0 +1,71 @@ +import { Context } from "@medusajs/types" +import { EventArgs, EventSubscriber } from "@mikro-orm/core" +type Service = { + interceptEntityMutationEvents: ( + event: "afterCreate" | "afterUpdate" | "afterUpsert" | "afterDelete", + args: EventArgs, + context: Context + ) => void +} + +export type MedusaMikroOrmEventSubscriber = { + new (context: Context): EventSubscriber +} + +/** + * Build a new mikro orm event subscriber for the given models + * @param models + * @returns + */ +export function createMedusaMikroOrmEventSubscriber( + keys: string[], + service: Service +): MedusaMikroOrmEventSubscriber { + const klass = class MikroOrmEventSubscriber implements EventSubscriber { + #context: Context + #service: Service = service + + constructor(context: Context) { + this.#context = context + } + + async afterCreate(args: EventArgs): Promise { + this.#service.interceptEntityMutationEvents( + "afterCreate", + args, + this.#context + ) + } + + async afterUpdate(args: EventArgs): Promise { + this.#service.interceptEntityMutationEvents( + "afterUpdate", + args, + this.#context + ) + } + + async afterUpsert(args: EventArgs): Promise { + this.#service.interceptEntityMutationEvents( + "afterUpsert", + args, + this.#context + ) + } + + async afterDelete(args: EventArgs): Promise { + this.#service.interceptEntityMutationEvents( + "afterDelete", + args, + this.#context + ) + } + } + + Object.defineProperty(klass, "name", { + value: keys.join(","), + writable: false, + }) + + return klass +} diff --git a/packages/core/utils/src/modules-sdk/index.ts b/packages/core/utils/src/modules-sdk/index.ts index 2d66bc413d..d17c895743 100644 --- a/packages/core/utils/src/modules-sdk/index.ts +++ b/packages/core/utils/src/modules-sdk/index.ts @@ -21,3 +21,4 @@ export * from "./types/links-config" export * from "./types/medusa-service" export * from "./module-provider-registration-key" export * from "./modules-to-container-types" +export * from "./create-medusa-mikro-orm-event-subscriber" diff --git a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts index 2edc4af678..09defb3c2e 100644 --- a/packages/core/utils/src/modules-sdk/medusa-internal-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-internal-service.ts @@ -9,7 +9,12 @@ import { PerformedActions, UpsertWithReplaceConfig, } from "@medusajs/types" -import type { EntityClass, EntitySchema } from "@mikro-orm/core" +import { + EventType, + type EntityClass, + type EntityManager, + type EntitySchema, +} from "@mikro-orm/core" import { isDefined, isObject, @@ -27,23 +32,60 @@ import { InjectTransactionManager, MedusaContext, } from "./decorators" +import { MedusaMikroOrmEventSubscriber } from "./create-medusa-mikro-orm-event-subscriber" + +type InternalService = { + new ( + container: TContainer + ): ModulesSdkTypes.IMedusaInternalService + + setEventSubscriber(subscriber: MedusaMikroOrmEventSubscriber): void +} type SelectorAndData = { selector: FilterQuery | BaseFilterable> data: any } +export function registerInternalServiceEventSubscriber( + context: Context, + subscriber?: MedusaMikroOrmEventSubscriber +) { + const manager = (context.transactionManager ?? + context.manager) as EntityManager + if (manager && subscriber) { + const subscriberInstance = new subscriber(context) + // There is no public API to unregister subscribers or check if a subscriber is already + // registered. This means that we need to manually check if the subscriber is already + // registered, otherwise we will register the same subscriber twice. + const hasListeners = (manager.getEventManager() as any).subscribers.some( + (s) => s.constructor.name === subscriberInstance.constructor.name + ) + if (!hasListeners) { + manager.getEventManager().registerSubscriber(subscriberInstance) + } + } +} + +export const MedusaInternalServiceSymbol = Symbol.for( + "MedusaInternalServiceSymbol" +) + +/** + * Check if a value is a Medusa internal service + * @param value + */ +export function isMedusaInternalService(value: any): value is InternalService { + return ( + !!value?.[MedusaInternalServiceSymbol] || + !!value?.prototype?.[MedusaInternalServiceSymbol] + ) +} + export function MedusaInternalService< TContainer extends object = object, TEntity extends object = any ->( - rawModel: any -): { - new (container: TContainer): ModulesSdkTypes.IMedusaInternalService< - TEntity, - TContainer - > -} { +>(rawModel: any): InternalService { const model = DmlEntity.isDmlEntity(rawModel) ? toMikroORMEntity(rawModel) : rawModel @@ -54,6 +96,10 @@ export function MedusaInternalService< class AbstractService_ implements ModulesSdkTypes.IMedusaInternalService { + [MedusaInternalServiceSymbol] = true + + #eventSubscriber?: MedusaMikroOrmEventSubscriber + readonly __container__: TContainer; [key: string]: any @@ -62,6 +108,10 @@ export function MedusaInternalService< this[propertyRepositoryName] = container[injectedRepositoryName] } + setEventSubscriber(subscriber: MedusaMikroOrmEventSubscriber) { + this.#eventSubscriber = subscriber + } + static applyFreeTextSearchFilter( filters: FilterQuery & { q?: string }, config: FindConfig @@ -223,6 +273,11 @@ export function MedusaInternalService< | InferEntityType[] } + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + const data_ = Array.isArray(data) ? data : [data] const entities = await this[propertyRepositoryName].create( data_, @@ -260,6 +315,11 @@ export function MedusaInternalService< | InferEntityType[] } + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + let shouldReturnArray = false if (Array.isArray(input)) { shouldReturnArray = true @@ -419,6 +479,11 @@ export function MedusaInternalService< return [] } + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) if ( @@ -467,10 +532,29 @@ export function MedusaInternalService< return [] } - return await this[propertyRepositoryName].delete( + const deletedIds = await this[propertyRepositoryName].delete( deleteCriteria, sharedContext ) + + // Delete are handled a bit differently since we are going to the DB directly, therefore + // just like upsert with replace, we need to dispatch the events manually. + if (deletedIds.length) { + const manager = (sharedContext.transactionManager ?? + sharedContext.manager) as EntityManager + const eventManager = manager.getEventManager() + + deletedIds.forEach((id) => { + eventManager.dispatchEvent(EventType.afterDelete, { + entity: { id }, + meta: { + className: model.name, + } as Parameters[2], + }) + }) + } + + return deletedIds } @InjectTransactionManager(propertyRepositoryName) @@ -489,6 +573,11 @@ export function MedusaInternalService< return [[], {}] } + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + return await this[propertyRepositoryName].softDelete( idsOrFilter, sharedContext @@ -520,6 +609,11 @@ export function MedusaInternalService< data: any | any[], @MedusaContext() sharedContext: Context = {} ): Promise | InferEntityType[]> { + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + const data_ = Array.isArray(data) ? data : [data] const entities = await this[propertyRepositoryName].upsert( data_, @@ -556,10 +650,74 @@ export function MedusaInternalService< entities: InferEntityType | InferEntityType[] performedActions: PerformedActions }> { + registerInternalServiceEventSubscriber( + sharedContext, + this.#eventSubscriber + ) + const data_ = Array.isArray(data) ? data : [data] const { entities, performedActions } = await this[ propertyRepositoryName ].upsertWithReplace(data_, config, sharedContext) + + const manager = (sharedContext.transactionManager ?? + sharedContext.manager) as EntityManager + const eventManager = manager.getEventManager() + + /** + * Since the upsertWithReplace method is not emitting events, we need to do it manually + * by dispatching the events manually. + */ + + const createdEntities = !!Object.keys(performedActions.created).length + const updatedEntities = !!Object.keys(performedActions.updated).length + const deletedEntities = !!Object.keys(performedActions.deleted).length + + if (createdEntities) { + Object.entries( + performedActions.created as Record + ).forEach(([modelName, entities]) => { + entities.forEach((entity) => { + eventManager.dispatchEvent(EventType.afterCreate, { + entity, + meta: { + className: modelName, + } as Parameters[2], + }) + }) + }) + } + + if (updatedEntities) { + Object.entries( + performedActions.updated as Record + ).forEach(([modelName, entities]) => { + entities.forEach((entity) => { + eventManager.dispatchEvent(EventType.afterUpdate, { + entity, + meta: { + className: modelName, + } as Parameters[2], + }) + }) + }) + } + + if (deletedEntities) { + Object.entries( + performedActions.deleted as Record + ).forEach(([modelName, entities]) => { + entities.forEach((entity) => { + eventManager.dispatchEvent(EventType.afterDelete, { + entity, + meta: { + className: modelName, + } as Parameters[2], + }) + }) + }) + } + return { entities: Array.isArray(data) ? entities : entities[0], performedActions, @@ -567,5 +725,7 @@ export function MedusaInternalService< } } - return AbstractService_ as any + // We hide away the setEventSubscriber method from the public interface + // as it is not meant to be used by the user. + return AbstractService_ as unknown as InternalService } diff --git a/packages/core/utils/src/modules-sdk/medusa-service.ts b/packages/core/utils/src/modules-sdk/medusa-service.ts index 35971187a4..08561e7da7 100644 --- a/packages/core/utils/src/modules-sdk/medusa-service.ts +++ b/packages/core/utils/src/modules-sdk/medusa-service.ts @@ -10,9 +10,9 @@ import { RestoreReturn, SoftDeleteReturn, } from "@medusajs/types" +import { EventArgs } from "@mikro-orm/core" import { camelToSnakeCase, - isString, lowerCaseFirst, mapObjectTo, MapToConfig, @@ -21,10 +21,12 @@ import { } from "../common" import { DmlEntity } from "../dml" import { CommonEvents } from "../event-bus" +import { createMedusaMikroOrmEventSubscriber } from "./create-medusa-mikro-orm-event-subscriber" import { EmitEvents, InjectManager, MedusaContext } from "./decorators" import { Modules } from "./definition" import { moduleEventBuilderFactory } from "./event-builder-factory" import { buildModelsNameToLinkableKeysMap } from "./joiner-config-builder" +import { isMedusaInternalService } from "./medusa-internal-service" import { BaseMethods, ExtractKeysFromConfig, @@ -137,37 +139,6 @@ export function MedusaService< ? ModelConfigurationsToConfigTemplate : ModelsConfig > { - function emitSoftDeleteRestoreEvents( - this: AbstractModuleService_, - klassPrototype: any, - cascadedModelsMap: Record, - action: string, - sharedContext: Context - ) { - const joinerConfig = ( - typeof this.__joinerConfig === "function" - ? this.__joinerConfig() - : this.__joinerConfig - ) as ModuleJoinerConfig - - const emittedEntities = new Set() - - Object.entries(cascadedModelsMap).forEach(([linkableKey, ids]) => { - const entity = joinerConfig.linkableKeys?.[linkableKey]! - if (entity && !emittedEntities.has(entity)) { - emittedEntities.add(entity) - const linkableKeyEntity = camelToSnakeCase(entity).toLowerCase() - - klassPrototype.aggregatedEvents.bind(this)({ - action: action, - object: linkableKeyEntity, - data: { id: ids }, - context: sharedContext, - }) - } - }) - } - const buildAndAssignMethodImpl = function ( klassPrototype: any, method: string, @@ -220,16 +191,9 @@ export function MedusaService< sharedContext: Context = {} ): Promise { const service = this.__container__[serviceRegistrationName] - const models = await service.create(data, sharedContext) + const models_ = await service.create(data, sharedContext) - klassPrototype.aggregatedEvents.bind(this)({ - action: CommonEvents.CREATED, - object: camelToSnakeCase(modelName).toLowerCase(), - data: models, - context: sharedContext, - }) - - return await this.baseRepository_.serialize(models) + return await this.baseRepository_.serialize(models_) } applyMethod(methodImplementation, 1) @@ -244,13 +208,6 @@ export function MedusaService< const service = this.__container__[serviceRegistrationName] const response = await service.update(data, sharedContext) - klassPrototype.aggregatedEvents.bind(this)({ - action: CommonEvents.UPDATED, - object: camelToSnakeCase(modelName).toLowerCase(), - data: response, - context: sharedContext, - }) - return await this.baseRepository_.serialize(response) } @@ -300,19 +257,10 @@ export function MedusaService< ? primaryKeyValues : [primaryKeyValues] - const ids = await this.__container__[serviceRegistrationName].delete( + await this.__container__[serviceRegistrationName].delete( primaryKeyValues_, sharedContext ) - - ids.map((id) => - klassPrototype.aggregatedEvents.bind(this)({ - action: CommonEvents.DELETED, - object: camelToSnakeCase(modelName).toLowerCase(), - data: isString(id) ? { id: id } : id, - context: sharedContext, - }) - ) } applyMethod(methodImplementation, 1) @@ -343,15 +291,6 @@ export function MedusaService< } ) - if (mappedCascadedModelsMap) { - emitSoftDeleteRestoreEvents.bind(this)( - klassPrototype, - mappedCascadedModelsMap, - CommonEvents.DELETED, - sharedContext - ) - } - return mappedCascadedModelsMap ? mappedCascadedModelsMap : void 0 } @@ -384,15 +323,6 @@ export function MedusaService< } ) - if (mappedCascadedModelsMap) { - emitSoftDeleteRestoreEvents.bind(this)( - klassPrototype, - mappedCascadedModelsMap, - CommonEvents.CREATED, - sharedContext - ) - } - return mappedCascadedModelsMap ? mappedCascadedModelsMap : void 0 } @@ -424,6 +354,41 @@ export function MedusaService< this.__container__ = container this.baseRepository_ = container.baseRepository + const joinerConfig = this.__joinerConfig?.() + /** + * Create a global subscriber to listen to all the entities mutations + * and forward them to the module service interceptEntityMutationEvents + * method. + * + * Assign the global subscriber to all internal services or class that extends it + * such that it can attach it accordingly and forward the events to the module service. + */ + + if (joinerConfig?.serviceName !== "index") { + let globalSubscriber!: ReturnType< + typeof createMedusaMikroOrmEventSubscriber + > + + Object.keys(container) + .filter((key) => { + return key.endsWith("Service") + }) + .forEach((key: string) => { + globalSubscriber ??= createMedusaMikroOrmEventSubscriber( + ["__medusa_entities_subscriber__"], + this + ) + try { + const service = container[key] + if (isMedusaInternalService(service)) { + service.setEventSubscriber(globalSubscriber) + } + } catch (error) { + // Prevent circular issue which in that case would represent ourselves so we can skip + } + }) + } + const hasEventBusModuleService = Object.keys(this.__container__).find( (key) => key === Modules.EVENT_BUS ) @@ -433,9 +398,72 @@ export function MedusaService< : undefined this[MedusaServiceModelNameToLinkableKeysMapSymbol] = - buildModelsNameToLinkableKeysMap( - this.__joinerConfig?.()?.linkableKeys ?? {} - ) + buildModelsNameToLinkableKeysMap(joinerConfig?.linkableKeys ?? {}) + } + + /** + * @internal this method is meant to react to any event the orm might emit + * when an entity is being mutated (created, updated, deleted). + * The default implementation will handle all event to be emitted as part + * of the message aggregator from the context. + * + * If you want to handle the event differently, you can override this method. + * + * @example + * + * class MyService extends ModulesSdkUtils.MedusaService(models) { + * interceptEntityMutationEvents(event: "afterCreate" | "afterUpdate" | "afterUpsert" | "afterDelete", args: EventArgs, context: Context) { + * console.log("interceptEntityMutationEvents", event, args.entity) + * } + * } + * + * @param event - The event type + * @param args - The event arguments + * @param context - The context + */ + interceptEntityMutationEvents( + event: "afterCreate" | "afterUpdate" | "afterUpsert" | "afterDelete", + args: EventArgs, + context: Context + ) { + let action = "" + switch (event) { + case "afterCreate": + action = CommonEvents.CREATED + break + case "afterUpdate": + const isSoftDeleted = + !!args.changeSet?.entity.deleted_at && + !args.changeSet?.originalEntity?.deleted_at + + const isRestored = + !!args.changeSet?.originalEntity?.deleted_at && + !args.changeSet?.entity.deleted_at + + action = CommonEvents.UPDATED + + if (isSoftDeleted) { + action = CommonEvents.DELETED + } + + if (isRestored) { + action = CommonEvents.RESTORED + } + + break + case "afterDelete": + action = CommonEvents.DELETED + break + } + + const object = camelToSnakeCase(args.meta.className).toLowerCase() + + this.aggregatedEvents({ + action, + object, + data: { id: args.entity.id }, + context, + }) } /** @@ -463,6 +491,10 @@ export function MedusaService< data: { id: any } | { id: any }[] context: Context }) { + if (!context.messageAggregator) { + return + } + const __joinerConfig = ( typeof this.__joinerConfig === "function" ? this.__joinerConfig() diff --git a/packages/core/utils/src/modules-sdk/types/medusa-service.ts b/packages/core/utils/src/modules-sdk/types/medusa-service.ts index f6ec59eb59..9623429a8c 100644 --- a/packages/core/utils/src/modules-sdk/types/medusa-service.ts +++ b/packages/core/utils/src/modules-sdk/types/medusa-service.ts @@ -3,13 +3,14 @@ import { Context, FindConfig, IDmlEntity, + InferEntityForModuleService, + InferEntityType, Pluralize, Prettify, RestoreReturn, SoftDeleteReturn, - InferEntityType, - InferEntityForModuleService, } from "@medusajs/types" +import { EventArgs } from "@mikro-orm/core" import { DmlEntity } from "../../dml" export type BaseMethods = @@ -318,4 +319,22 @@ export type MedusaServiceReturnType> = data: { id: any } | { id: any }[] context: Context }): void + + /** + * this method is meant to react to any event the orm might emit + * when an entity is being mutated (created, updated, deleted). + * The default implementation will handle all event to be emitted as part + * of the message aggregator from the context. + * + * If you want to handle the event differently, you can override this method. + * + * @param event - The event type + * @param args - The event arguments + * @param context - The context + */ + interceptEntityMutationEvents( + event: "afterCreate" | "afterUpdate" | "afterUpsert" | "afterDelete", + args: EventArgs, + context: Context + ): void } diff --git a/packages/modules/api-key/src/services/api-key-module-service.ts b/packages/modules/api-key/src/services/api-key-module-service.ts index e2819af497..62f4be0470 100644 --- a/packages/modules/api-key/src/services/api-key-module-service.ts +++ b/packages/modules/api-key/src/services/api-key-module-service.ts @@ -12,6 +12,7 @@ import { } from "@medusajs/framework/types" import { ApiKeyType, + EmitEvents, InjectManager, InjectTransactionManager, isObject, @@ -65,11 +66,20 @@ export class ApiKeyModuleService return joinerConfig } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async deleteApiKeys( ids: string | string[], @MedusaContext() sharedContext: Context = {} + ) { + return await this.deleteApiKeys_(ids, sharedContext) + } + + @InjectTransactionManager() + protected async deleteApiKeys_( + ids: string | string[], + @MedusaContext() sharedContext: Context = {} ) { const apiKeyIds = Array.isArray(ids) ? ids : [ids] @@ -111,6 +121,7 @@ export class ApiKeyModuleService ): Promise @InjectManager() + @EmitEvents() //@ts-expect-error async createApiKeys( data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[], @@ -183,9 +194,20 @@ export class ApiKeyModuleService ): Promise @InjectManager() + @EmitEvents() async upsertApiKeys( data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[], @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.upsertApiKeys_(data, sharedContext) + + return await this.baseRepository_.serialize(result) + } + + @InjectTransactionManager() + protected async upsertApiKeys_( + data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[], + @MedusaContext() sharedContext: Context = {} ): Promise { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( @@ -205,9 +227,7 @@ export class ApiKeyModuleService ) const serializedResponse = await this.baseRepository_.serialize< ApiKeyTypes.ApiKeyDTO[] - >(createdApiKeys, { - populate: true, - }) + >(createdApiKeys) return serializedResponse.map( (key) => @@ -226,10 +246,7 @@ export class ApiKeyModuleService if (forUpdate.length) { const op = async () => { - const updateResp = await this.updateApiKeys_(forUpdate, sharedContext) - return await this.baseRepository_.serialize( - updateResp - ) + return await this.updateApiKeys_(forUpdate, sharedContext) } operations.push(op()) @@ -253,6 +270,7 @@ export class ApiKeyModuleService ): Promise @InjectManager() + @EmitEvents() //@ts-expect-error async updateApiKeys( idOrSelector: string | FilterableApiKeyProps, @@ -368,7 +386,9 @@ export class ApiKeyModuleService data: ApiKeyTypes.RevokeApiKeyDTO, sharedContext?: Context ): Promise + @InjectManager() + @EmitEvents() async revoke( idOrSelector: string | FilterableApiKeyProps, data: ApiKeyTypes.RevokeApiKeyDTO, diff --git a/packages/modules/cart/src/services/cart-module.ts b/packages/modules/cart/src/services/cart-module.ts index b8adcad015..85ddfcd39e 100644 --- a/packages/modules/cart/src/services/cart-module.ts +++ b/packages/modules/cart/src/services/cart-module.ts @@ -13,6 +13,7 @@ import { createRawPropertiesFromBigNumber, decorateCartTotals, deduplicate, + EmitEvents, generateEntityId, InjectManager, InjectTransactionManager, @@ -269,6 +270,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createCarts( data: CartTypes.CreateCartDTO[] | CartTypes.CreateCartDTO, @@ -340,6 +342,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateCarts( dataOrIdOrSelector: @@ -425,6 +428,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addLineItems( cartIdOrData: | string @@ -504,6 +508,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateLineItems( lineItemIdOrDataOrSelector: @@ -606,6 +611,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createAddresses( data: CartTypes.CreateAddressDTO[] | CartTypes.CreateAddressDTO, @@ -645,6 +651,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateAddresses( data: CartTypes.UpdateAddressDTO[] | CartTypes.UpdateAddressDTO, @@ -685,6 +692,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addShippingMethods( cartIdOrData: | string @@ -763,6 +771,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addLineItemAdjustments( cartIdOrData: | string @@ -827,6 +836,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async upsertLineItemTaxLines( taxLines: ( | CartTypes.CreateLineItemTaxLineDTO @@ -861,6 +871,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async upsertLineItemAdjustments( adjustments: ( | CartTypes.CreateLineItemAdjustmentDTO @@ -895,6 +906,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async upsertShippingMethodTaxLines( taxLines: ( | CartTypes.CreateShippingMethodTaxLineDTO @@ -929,6 +941,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async upsertShippingMethodAdjustments( adjustments: ( | CartTypes.CreateShippingMethodAdjustmentDTO @@ -963,6 +976,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async setLineItemAdjustments( cartId: string, adjustments: ( @@ -1036,6 +1050,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async setShippingMethodAdjustments( cartId: string, adjustments: ( @@ -1122,6 +1137,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addShippingMethodAdjustments( cartIdOrData: | string @@ -1212,6 +1228,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addLineItemTaxLines( cartIdOrData: | string @@ -1275,6 +1292,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async setLineItemTaxLines( cartId: string, taxLines: ( @@ -1357,6 +1375,7 @@ export default class CartModuleService ): Promise @InjectManager() + @EmitEvents() async addShippingMethodTaxLines( cartIdOrData: | string @@ -1422,6 +1441,7 @@ export default class CartModuleService } @InjectManager() + @EmitEvents() async setShippingMethodTaxLines( cartId: string, taxLines: ( diff --git a/packages/modules/customer/src/services/customer-module.ts b/packages/modules/customer/src/services/customer-module.ts index b80dafc23e..f676c5491a 100644 --- a/packages/modules/customer/src/services/customer-module.ts +++ b/packages/modules/customer/src/services/customer-module.ts @@ -14,13 +14,13 @@ import { } from "@medusajs/framework/types" import { + EmitEvents, InjectManager, InjectTransactionManager, isString, MedusaContext, MedusaService, } from "@medusajs/framework/utils" -import { EntityManager } from "@mikro-orm/core" import { Customer, CustomerAddress, @@ -101,6 +101,7 @@ export default class CustomerModuleService ): Promise @InjectManager() + @EmitEvents() async createCustomers( dataOrArray: | CustomerTypes.CreateCustomerDTO @@ -124,7 +125,7 @@ export default class CustomerModuleService | CustomerTypes.CreateCustomerDTO | CustomerTypes.CreateCustomerDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise[]> { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] const customerAttributes = data.map(({ addresses, ...rest }) => { return rest @@ -153,7 +154,7 @@ export default class CustomerModuleService sharedContext ) - return customers as unknown as CustomerTypes.CustomerDTO[] + return customers } // @ts-expect-error @@ -175,13 +176,33 @@ export default class CustomerModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async updateCustomers( idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps, data: CustomerTypes.CustomerUpdatableFields, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { + const customers = await this.updateCustomers_( + idsOrSelector, + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[] + >(customers) + } + + @InjectTransactionManager() + protected async updateCustomers_( + idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps, + data: CustomerTypes.CustomerUpdatableFields, + @MedusaContext() sharedContext: Context = {} + ): Promise< + InferEntityType[] | InferEntityType + > { let updateData: | CustomerTypes.UpdateCustomerDTO | CustomerTypes.UpdateCustomerDTO[] @@ -212,13 +233,7 @@ export default class CustomerModuleService sharedContext ) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[] - >(customers, { - populate: true, - }) - - return serialized + return customers } // @ts-expect-error @@ -233,24 +248,43 @@ export default class CustomerModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createCustomerGroups( dataOrArrayOfData: | CustomerTypes.CreateCustomerGroupDTO | CustomerTypes.CreateCustomerGroupDTO[], @MedusaContext() sharedContext: Context = {} - ) { - const groups = await this.customerGroupService_.create( + ): Promise< + CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] + > { + const groups = await this.createCustomerGroups_( dataOrArrayOfData, sharedContext ) return await this.baseRepository_.serialize< CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] - >(groups, { - populate: true, - }) + >(groups) + } + + @InjectTransactionManager() + protected async createCustomerGroups_( + dataOrArrayOfData: + | CustomerTypes.CreateCustomerGroupDTO + | CustomerTypes.CreateCustomerGroupDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType + > { + const groups = await this.customerGroupService_.create( + dataOrArrayOfData, + sharedContext + ) + + return groups } // @ts-expect-error @@ -272,7 +306,8 @@ export default class CustomerModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async updateCustomerGroups( groupIdOrSelector: @@ -281,7 +316,32 @@ export default class CustomerModuleService | CustomerTypes.FilterableCustomerGroupProps, data: CustomerTypes.CustomerGroupUpdatableFields, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise< + CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] + > { + const groups = await this.updateCustomerGroups_( + groupIdOrSelector, + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] + >(groups) + } + + @InjectTransactionManager() + protected async updateCustomerGroups_( + groupIdOrSelector: + | string + | string[] + | CustomerTypes.FilterableCustomerGroupProps, + data: CustomerTypes.CustomerGroupUpdatableFields, + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType + > { let updateData: | CustomerTypes.UpdateCustomerGroupDTO | CustomerTypes.UpdateCustomerGroupDTO[] @@ -311,15 +371,10 @@ export default class CustomerModuleService ) if (isString(groupIdOrSelector)) { - return await this.baseRepository_.serialize( - groups[0], - { populate: true } - ) + return groups[0] } - return await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupDTO[] - >(groups, { populate: true }) + return groups } async addCustomerToGroup( @@ -332,10 +387,21 @@ export default class CustomerModuleService sharedContext?: Context ): Promise<{ id: string }[]> - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async addCustomerToGroup( data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], @MedusaContext() sharedContext: Context = {} + ): Promise<{ id: string } | { id: string }[]> { + const groupCustomers = await this.addCustomerToGroup_(data, sharedContext) + + return groupCustomers + } + + @InjectTransactionManager() + protected async addCustomerToGroup_( + data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], + @MedusaContext() sharedContext: Context = {} ): Promise<{ id: string } | { id: string }[]> { const groupCustomers = await this.customerGroupCustomerService_.create( data, @@ -365,6 +431,7 @@ export default class CustomerModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createCustomerAddresses( data: @@ -419,7 +486,8 @@ export default class CustomerModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async updateCustomerAddresses( addressIdOrSelector: @@ -428,7 +496,32 @@ export default class CustomerModuleService | CustomerTypes.FilterableCustomerAddressProps, data: CustomerTypes.UpdateCustomerAddressDTO, @MedusaContext() sharedContext: Context = {} - ) { + ): Promise< + CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] + > { + const addresses = await this.updateCustomerAddresses_( + addressIdOrSelector, + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] + >(addresses) + } + + @InjectTransactionManager() + protected async updateCustomerAddresses_( + addressIdOrSelector: + | string + | string[] + | CustomerTypes.FilterableCustomerAddressProps, + data: CustomerTypes.UpdateCustomerAddressDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType + > { let updateData: | CustomerTypes.UpdateCustomerAddressDTO[] | { @@ -459,17 +552,11 @@ export default class CustomerModuleService sharedContext ) - await this.flush(sharedContext) - - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerAddressDTO[] - >(addresses, { populate: true }) - if (isString(addressIdOrSelector)) { - return serialized[0] + return addresses[0] } - return serialized + return addresses } async removeCustomerFromGroup( @@ -481,10 +568,19 @@ export default class CustomerModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async removeCustomerFromGroup( data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.removeCustomerFromGroup_(data, sharedContext) + } + + @InjectTransactionManager() + protected async removeCustomerFromGroup_( + data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], + @MedusaContext() sharedContext: Context = {} ): Promise { const pairs = Array.isArray(data) ? data : [data] const groupCustomers = await this.customerGroupCustomerService_.list({ @@ -495,9 +591,4 @@ export default class CustomerModuleService sharedContext ) } - - private async flush(context: Context) { - const em = (context.manager ?? context.transactionManager) as EntityManager - await em.flush() - } } diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts index d3e93e6bfb..414c672c90 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment-set.spec.ts @@ -219,7 +219,7 @@ moduleIntegrationTestRunner({ action: "created", object: "fulfillment_set", data: { - id: expect.arrayContaining([fulfillmentSets[i].id]), + id: fulfillmentSets[i].id, }, }), ]), @@ -338,7 +338,7 @@ moduleIntegrationTestRunner({ action: "created", object: "fulfillment_set", data: { - id: expect.arrayContaining([fulfillmentSets[i].id]), + id: fulfillmentSets[i].id, }, }), buildExpectedEventMessageShape({ @@ -346,9 +346,7 @@ moduleIntegrationTestRunner({ action: "created", object: "service_zone", data: { - id: expect.arrayContaining([ - fulfillmentSets[i].service_zones[0].id, - ]), + id: fulfillmentSets[i].service_zones[0].id, }, }), ]), @@ -514,7 +512,7 @@ moduleIntegrationTestRunner({ action: "created", object: "fulfillment_set", data: { - id: expect.arrayContaining([fulfillmentSets[i].id]), + id: fulfillmentSets[i].id, }, }), buildExpectedEventMessageShape({ @@ -522,9 +520,7 @@ moduleIntegrationTestRunner({ action: "created", object: "service_zone", data: { - id: expect.arrayContaining([ - fulfillmentSets[i].service_zones[0].id, - ]), + id: fulfillmentSets[i].service_zones[0].id, }, }), buildExpectedEventMessageShape({ @@ -532,9 +528,7 @@ moduleIntegrationTestRunner({ action: "created", object: "geo_zone", data: { - id: expect.arrayContaining([ - fulfillmentSets[i].service_zones[0].geo_zones[0].id, - ]), + id: fulfillmentSets[i].service_zones[0].geo_zones[0].id, }, }), ]), @@ -724,7 +718,7 @@ moduleIntegrationTestRunner({ }) expect(updatedFulfillmentSets).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(1) + expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(2) for (const data_ of updateData) { const currentFullfillmentSet = fullfillmentSets.find( @@ -746,7 +740,7 @@ moduleIntegrationTestRunner({ action: "updated", object: "fulfillment_set", data: { - id: expect.arrayContaining([currentFullfillmentSet.id]), + id: currentFullfillmentSet.id, }, }), ]), @@ -1075,7 +1069,7 @@ moduleIntegrationTestRunner({ ) expect(updatedFulfillmentSets).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(5) + expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(10) for (const data_ of updateData) { const expectedFulfillmentSet = updatedFulfillmentSets.find( @@ -1116,7 +1110,7 @@ moduleIntegrationTestRunner({ action: "updated", object: "fulfillment_set", data: { - id: expect.arrayContaining([expectedFulfillmentSet.id]), + id: expectedFulfillmentSet.id, }, }), buildExpectedEventMessageShape({ @@ -1124,9 +1118,7 @@ moduleIntegrationTestRunner({ action: "created", object: "service_zone", data: { - id: expect.arrayContaining([ - expectedFulfillmentSet.service_zones[0].id, - ]), + id: expectedFulfillmentSet.service_zones[0].id, }, }), buildExpectedEventMessageShape({ @@ -1134,9 +1126,8 @@ moduleIntegrationTestRunner({ action: "created", object: "geo_zone", data: { - id: expect.arrayContaining([ - expectedFulfillmentSet.service_zones[0].geo_zones[0].id, - ]), + id: expectedFulfillmentSet.service_zones[0].geo_zones[0] + .id, }, }), buildExpectedEventMessageShape({ @@ -1144,9 +1135,7 @@ moduleIntegrationTestRunner({ action: "deleted", object: "service_zone", data: { - id: expect.arrayContaining([ - originalFulfillmentSet.service_zones[0].id, - ]), + id: originalFulfillmentSet.service_zones[0].id, }, }), buildExpectedEventMessageShape({ @@ -1154,9 +1143,8 @@ moduleIntegrationTestRunner({ action: "deleted", object: "geo_zone", data: { - id: expect.arrayContaining([ - originalFulfillmentSet.service_zones[0].geo_zones[0].id, - ]), + id: originalFulfillmentSet.service_zones[0].geo_zones[0] + .id, }, }), ]), @@ -1245,7 +1233,7 @@ moduleIntegrationTestRunner({ ) expect(updatedFulfillmentSets).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(3) + expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(6) for (const data_ of updateData) { const expectedFulfillmentSet = updatedFulfillmentSets.find( @@ -1290,7 +1278,7 @@ moduleIntegrationTestRunner({ action: "updated", object: "fulfillment_set", data: { - id: expect.arrayContaining([expectedFulfillmentSet.id]), + id: expectedFulfillmentSet.id, }, }), buildExpectedEventMessageShape({ @@ -1298,7 +1286,7 @@ moduleIntegrationTestRunner({ action: "created", object: "service_zone", data: { - id: expect.arrayContaining([createdServiceZone.id]), + id: createdServiceZone.id, }, }), buildExpectedEventMessageShape({ @@ -1306,9 +1294,7 @@ moduleIntegrationTestRunner({ action: "created", object: "geo_zone", data: { - id: expect.arrayContaining([ - createdServiceZone.geo_zones[0].id, - ]), + id: createdServiceZone.geo_zones[0].id, }, }), ]), diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts index 4d8b4bdc03..54da02bf6f 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/fulfillment.spec.ts @@ -152,15 +152,21 @@ moduleIntegrationTestRunner({ }) ) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5) expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ + expect.arrayContaining([ buildExpectedEventMessageShape({ eventName: FulfillmentEvents.FULFILLMENT_CREATED, action: "created", object: "fulfillment", data: { id: fulfillment.id }, }), + buildExpectedEventMessageShape({ + eventName: FulfillmentEvents.FULFILLMENT_UPDATED, + action: "updated", + object: "fulfillment", + data: { id: fulfillment.id }, + }), buildExpectedEventMessageShape({ eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED, action: "created", @@ -179,7 +185,7 @@ moduleIntegrationTestRunner({ object: "fulfillment_label", data: { id: fulfillment.labels[0].id }, }), - ], + ]), { internal: true, } @@ -244,15 +250,21 @@ moduleIntegrationTestRunner({ }) ) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5) expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ + expect.arrayContaining([ buildExpectedEventMessageShape({ eventName: FulfillmentEvents.FULFILLMENT_CREATED, action: "created", object: "fulfillment", data: { id: fulfillment.id }, }), + buildExpectedEventMessageShape({ + eventName: FulfillmentEvents.FULFILLMENT_UPDATED, + action: "updated", + object: "fulfillment", + data: { id: fulfillment.id }, + }), buildExpectedEventMessageShape({ eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED, action: "created", @@ -271,7 +283,7 @@ moduleIntegrationTestRunner({ object: "fulfillment_label", data: { id: fulfillment.labels[0].id }, }), - ], + ]), { internal: true, } @@ -383,15 +395,9 @@ moduleIntegrationTestRunner({ }) ) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - buildExpectedEventMessageShape({ - eventName: FulfillmentEvents.FULFILLMENT_UPDATED, - action: "updated", - object: "fulfillment", - data: { id: updatedFulfillment.id }, - }), + expect.arrayContaining([ buildExpectedEventMessageShape({ eventName: FulfillmentEvents.FULFILLMENT_LABEL_DELETED, action: "deleted", @@ -410,7 +416,7 @@ moduleIntegrationTestRunner({ object: "fulfillment_label", data: { id: label4.id }, }), - ], + ]), { internal: true, } diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts index 5d24fb4c14..630a89e06c 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/geo-zone.spec.ts @@ -154,7 +154,7 @@ moduleIntegrationTestRunner({ const geoZones = await service.createGeoZones(data) expect(geoZones).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2) let i = 0 for (const data_ of data) { @@ -172,7 +172,7 @@ moduleIntegrationTestRunner({ eventName: FulfillmentEvents.GEO_ZONE_CREATED, action: "created", object: "geo_zone", - data: { id: expect.arrayContaining([geoZones[i].id]) }, + data: { id: geoZones[i].id }, }), ]), { @@ -331,7 +331,7 @@ moduleIntegrationTestRunner({ const updatedGeoZones = await service.updateGeoZones(updateData) expect(updatedGeoZones).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2) for (const data_ of updateData) { const expectedGeoZone = updatedGeoZones.find( @@ -352,7 +352,7 @@ moduleIntegrationTestRunner({ eventName: FulfillmentEvents.GEO_ZONE_UPDATED, action: "updated", object: "geo_zone", - data: { id: expect.arrayContaining([expectedGeoZone.id]) }, + data: { id: expectedGeoZone.id }, }), ]), { diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts index 845ecd9b19..f36e0d4669 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/service-zone.spec.ts @@ -371,7 +371,7 @@ moduleIntegrationTestRunner({ expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ + expect.arrayContaining([ buildExpectedEventMessageShape({ eventName: FulfillmentEvents.GEO_ZONE_DELETED, action: "deleted", @@ -396,7 +396,7 @@ moduleIntegrationTestRunner({ object: "geo_zone", data: { id: usGeoZone.id }, }), - ], + ]), { internal: true, } @@ -510,7 +510,7 @@ moduleIntegrationTestRunner({ expect(updatedServiceZones).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) // Since the update only calls create and update which are already tested, only check the length + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(6) // Since the update only calls create and update which are already tested, only check the length for (const data_ of updateData) { const expectedServiceZone = updatedServiceZones.find( diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts index ca93c76490..cfe7578aeb 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts @@ -522,7 +522,7 @@ moduleIntegrationTestRunner({ expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ + expect.arrayContaining([ buildExpectedEventMessageShape({ eventName: FulfillmentEvents.SHIPPING_OPTION_CREATED, action: "created", @@ -541,7 +541,7 @@ moduleIntegrationTestRunner({ object: "shipping_option_rule", data: { id: createdShippingOption.rules[0].id }, }), - ], + ]), { internal: true, } @@ -582,7 +582,7 @@ moduleIntegrationTestRunner({ ) expect(createdShippingOptions).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(6) let i = 0 for (const data_ of createData) { @@ -620,9 +620,7 @@ moduleIntegrationTestRunner({ action: "created", object: "shipping_option", data: { - id: expect.arrayContaining([ - createdShippingOptions[i].id, - ]), + id: createdShippingOptions[i].id, }, }), buildExpectedEventMessageShape({ @@ -630,9 +628,7 @@ moduleIntegrationTestRunner({ action: "created", object: "shipping_option_type", data: { - id: expect.arrayContaining([ - createdShippingOptions[i].type.id, - ]), + id: createdShippingOptions[i].type.id, }, }), buildExpectedEventMessageShape({ @@ -640,9 +636,7 @@ moduleIntegrationTestRunner({ action: "created", object: "shipping_option_rule", data: { - id: expect.arrayContaining([ - createdShippingOptions[i].rules[0].id, - ]), + id: createdShippingOptions[i].rules[0].id, }, }), ]), @@ -823,7 +817,7 @@ moduleIntegrationTestRunner({ ]) ) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy).toHaveBeenCalledWith( expect.arrayContaining([ buildExpectedEventMessageShape({ @@ -832,12 +826,6 @@ moduleIntegrationTestRunner({ object: "shipping_option", data: { id: updatedShippingOption.id }, }), - buildExpectedEventMessageShape({ - eventName: FulfillmentEvents.SHIPPING_OPTION_TYPE_DELETED, - action: "deleted", - object: "shipping_option_type", - data: { id: shippingOption.type.id }, - }), buildExpectedEventMessageShape({ eventName: FulfillmentEvents.SHIPPING_OPTION_TYPE_CREATED, action: "created", @@ -1374,14 +1362,16 @@ moduleIntegrationTestRunner({ ]) // Test with full address including postal expression - const shippingOptions = await service.listShippingOptionsForContext({ - address: { - country_code: "US", - province_code: "CA", - city: "Los Angeles", - postal_expression: "90210", - }, - }) + const shippingOptions = await service.listShippingOptionsForContext( + { + address: { + country_code: "US", + province_code: "CA", + city: "Los Angeles", + postal_expression: "90210", + }, + } + ) expect(shippingOptions).toHaveLength(1) }) @@ -1419,13 +1409,15 @@ moduleIntegrationTestRunner({ ]) // Test with city but no postal expression - const shippingOptions = await service.listShippingOptionsForContext({ - address: { - country_code: "US", - province_code: "CA", - city: "San Francisco", - }, - }) + const shippingOptions = await service.listShippingOptionsForContext( + { + address: { + country_code: "US", + province_code: "CA", + city: "San Francisco", + }, + } + ) expect(shippingOptions).toHaveLength(1) }) @@ -1462,12 +1454,14 @@ moduleIntegrationTestRunner({ ]) // Test with province but no city - const shippingOptions = await service.listShippingOptionsForContext({ - address: { - country_code: "US", - province_code: "NY", - }, - }) + const shippingOptions = await service.listShippingOptionsForContext( + { + address: { + country_code: "US", + province_code: "NY", + }, + } + ) expect(shippingOptions).toHaveLength(1) }) @@ -1503,11 +1497,13 @@ moduleIntegrationTestRunner({ ]) // Test with only country - const shippingOptions = await service.listShippingOptionsForContext({ - address: { - country_code: "CA", - }, - }) + const shippingOptions = await service.listShippingOptionsForContext( + { + address: { + country_code: "CA", + }, + } + ) expect(shippingOptions).toHaveLength(1) }) @@ -1758,14 +1754,16 @@ moduleIntegrationTestRunner({ ]) // Address with undefined fields should still match if country matches - const shippingOptions = await service.listShippingOptionsForContext({ - address: { - country_code: "US", - province_code: undefined, - city: undefined, - postal_expression: undefined, - }, - }) + const shippingOptions = await service.listShippingOptionsForContext( + { + address: { + country_code: "US", + province_code: undefined, + city: undefined, + postal_expression: undefined, + }, + } + ) expect(shippingOptions).toHaveLength(1) }) @@ -1831,26 +1829,27 @@ moduleIntegrationTestRunner({ }) // Create shipping options for each zone - const [usOption, europeOption, canadaOption] = await service.createShippingOptions([ - generateCreateShippingOptionsData({ - name: "US Shipping", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - generateCreateShippingOptionsData({ - name: "Europe Shipping", - service_zone_id: fulfillmentSet.service_zones[1].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - generateCreateShippingOptionsData({ - name: "Canada Shipping", - service_zone_id: fulfillmentSet.service_zones[2].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - ]) + const [usOption, europeOption, canadaOption] = + await service.createShippingOptions([ + generateCreateShippingOptionsData({ + name: "US Shipping", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + generateCreateShippingOptionsData({ + name: "Europe Shipping", + service_zone_id: fulfillmentSet.service_zones[1].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + generateCreateShippingOptionsData({ + name: "Canada Shipping", + service_zone_id: fulfillmentSet.service_zones[2].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + ]) // Test US ZIP code - should only match US zone let shippingOptions = await service.listShippingOptionsForContext({ @@ -1970,26 +1969,27 @@ moduleIntegrationTestRunner({ }) // Create shipping options with different prices for each zone - const [broadOption, californiaOption, laOption] = await service.createShippingOptions([ - generateCreateShippingOptionsData({ - name: "Standard US Shipping", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - generateCreateShippingOptionsData({ - name: "California Express", - service_zone_id: fulfillmentSet.service_zones[1].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - generateCreateShippingOptionsData({ - name: "LA Premium", - service_zone_id: fulfillmentSet.service_zones[2].id, - shipping_profile_id: shippingProfile.id, - provider_id: providerId, - }), - ]) + const [broadOption, californiaOption, laOption] = + await service.createShippingOptions([ + generateCreateShippingOptionsData({ + name: "Standard US Shipping", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + generateCreateShippingOptionsData({ + name: "California Express", + service_zone_id: fulfillmentSet.service_zones[1].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + generateCreateShippingOptionsData({ + name: "LA Premium", + service_zone_id: fulfillmentSet.service_zones[2].id, + shipping_profile_id: shippingProfile.id, + provider_id: providerId, + }), + ]) // Test LA ZIP code - should match all three zones let shippingOptions = await service.listShippingOptionsForContext({ @@ -2001,7 +2001,7 @@ moduleIntegrationTestRunner({ }, }) expect(shippingOptions).toHaveLength(3) - const laIds = shippingOptions.map(opt => opt.id) + const laIds = shippingOptions.map((opt) => opt.id) expect(laIds).toContain(broadOption.id) expect(laIds).toContain(californiaOption.id) expect(laIds).toContain(laOption.id) @@ -2015,7 +2015,7 @@ moduleIntegrationTestRunner({ }, }) expect(shippingOptions).toHaveLength(2) - const caIds = shippingOptions.map(opt => opt.id) + const caIds = shippingOptions.map((opt) => opt.id) expect(caIds).toContain(broadOption.id) expect(caIds).toContain(californiaOption.id) expect(caIds).not.toContain(laOption.id) diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts index f36596f9a0..cf4226ed30 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-profile.spec.ts @@ -100,7 +100,7 @@ moduleIntegrationTestRunner({ await service.createShippingProfiles(createData) expect(createdShippingProfiles).toHaveLength(2) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2) let i = 0 for (const data_ of createData) { @@ -118,9 +118,7 @@ moduleIntegrationTestRunner({ action: "created", object: "shipping_profile", data: { - id: expect.arrayContaining([ - createdShippingProfiles[i].id, - ]), + id: createdShippingProfiles[i].id, }, }), ]), diff --git a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts index 651190163c..2de8932ae0 100644 --- a/packages/modules/fulfillment/src/services/fulfillment-module-service.ts +++ b/packages/modules/fulfillment/src/services/fulfillment-module-service.ts @@ -47,18 +47,9 @@ import { ShippingOptionType, ShippingProfile, } from "@models" -import { - buildCreatedFulfillmentEvents, - buildCreatedFulfillmentSetEvents, - buildCreatedServiceZoneEvents, - eventBuilders, - isContextValid, - Rule, - validateAndNormalizeRules, -} from "@utils" +import { isContextValid, Rule, validateAndNormalizeRules } from "@utils" import { joinerConfig } from "../joiner-config" import { UpdateShippingOptionsInput } from "../types/service" -import { buildCreatedShippingOptionEvents } from "../utils/events" import FulfillmentProviderService from "./fulfillment-provider" const generateMethodForModels = { @@ -70,7 +61,7 @@ const generateMethodForModels = { ShippingOptionRule, ShippingOptionType, FulfillmentProvider, - // Not adding Fulfillment to not auto generate the methods under the hood and only provide the methods we want to expose8 + // Not adding Fulfillment to not auto generate the methods under the hood and only provide the methods we want to expose } type InjectedDependencies = { @@ -339,11 +330,6 @@ export default class FulfillmentModuleService sharedContext ) - buildCreatedFulfillmentSetEvents({ - fulfillmentSets: createdFulfillmentSets, - sharedContext, - }) - return createdFulfillmentSets } @@ -405,11 +391,6 @@ export default class FulfillmentModuleService sharedContext ) - buildCreatedServiceZoneEvents({ - serviceZones: createdServiceZones, - sharedContext, - }) - return createdServiceZones } @@ -468,11 +449,6 @@ export default class FulfillmentModuleService sharedContext ) - buildCreatedShippingOptionEvents({ - shippingOptions: createdSO, - sharedContext, - }) - return createdSO } @@ -487,7 +463,7 @@ export default class FulfillmentModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() @EmitEvents() // @ts-expect-error async createShippingProfiles( @@ -503,11 +479,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.createdShippingProfile({ - data: createdShippingProfiles, - sharedContext, - }) - return await this.baseRepository_.serialize< | FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[] @@ -561,11 +532,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.createdGeoZone({ - data: createdGeoZones, - sharedContext, - }) - return await this.baseRepository_.serialize( Array.isArray(data) ? createdGeoZones : createdGeoZones[0] ) @@ -629,11 +595,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.createdShippingOptionRule({ - data: createdSORules.map((sor) => ({ id: sor.id })), - sharedContext, - }) - return createdSORules } @@ -679,11 +640,6 @@ export default class FulfillmentModuleService throw error } - buildCreatedFulfillmentEvents({ - fulfillments: [fulfillment], - sharedContext, - }) - return await this.baseRepository_.serialize( fulfillment ) @@ -754,11 +710,6 @@ export default class FulfillmentModuleService throw error } - buildCreatedFulfillmentEvents({ - fulfillments: [fulfillment], - sharedContext, - }) - return await this.baseRepository_.serialize( fulfillment ) @@ -943,15 +894,6 @@ export default class FulfillmentModuleService }) if (serviceZoneIdsToDelete.length) { - eventBuilders.deletedServiceZone({ - data: serviceZoneIdsToDelete.map((id) => ({ id })), - sharedContext, - }) - eventBuilders.deletedGeoZone({ - data: geoZoneIdsToDelete.map((id) => ({ id })), - sharedContext, - }) - await promiseAll([ this.geoZoneService_.delete( { @@ -973,32 +915,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.updatedFulfillmentSet({ - data: updatedFulfillmentSets, - sharedContext, - }) - - const createdServiceZoneIds: string[] = [] - const createdGeoZoneIds = updatedFulfillmentSets - .flatMap((f) => - [...f.service_zones].flatMap((serviceZone) => { - if (!existingServiceZoneIds.includes(serviceZone.id)) { - createdServiceZoneIds.push(serviceZone.id) - } - return serviceZone.geo_zones.map((g) => g.id) - }) - ) - .filter((id) => !existingGeoZoneIds.includes(id)) - - eventBuilders.createdServiceZone({ - data: createdServiceZoneIds.map((id) => ({ id })), - sharedContext, - }) - eventBuilders.createdGeoZone({ - data: createdGeoZoneIds.map((id) => ({ id })), - sharedContext, - }) - return Array.isArray(data) ? updatedFulfillmentSets : updatedFulfillmentSets[0] @@ -1187,11 +1103,6 @@ export default class FulfillmentModuleService }) if (geoZoneIdsToDelete.length) { - eventBuilders.deletedGeoZone({ - data: geoZoneIdsToDelete.map((id) => ({ id })), - sharedContext, - }) - await this.geoZoneService_.delete( { id: geoZoneIdsToDelete, @@ -1205,26 +1116,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.updatedServiceZone({ - data: updatedServiceZones, - sharedContext, - }) - - const createdGeoZoneIds = updatedServiceZones - .flatMap((serviceZone) => { - return serviceZone.geo_zones.map((g) => g.id) - }) - .filter((id) => !existingGeoZoneIds.includes(id)) - - eventBuilders.createdGeoZone({ - data: createdGeoZoneIds.map((id) => ({ id })), - sharedContext, - }) - eventBuilders.updatedGeoZone({ - data: updatedGeoZoneIds.map((id) => ({ id })), - sharedContext, - }) - return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0] } @@ -1485,11 +1376,6 @@ export default class FulfillmentModuleService }) if (ruleIdsToDelete.length) { - eventBuilders.deletedShippingOptionRule({ - data: ruleIdsToDelete.map((id) => ({ id })), - sharedContext, - }) - await this.shippingOptionRuleService_.delete( ruleIdsToDelete, sharedContext @@ -1501,71 +1387,11 @@ export default class FulfillmentModuleService sharedContext ) - this.handleShippingOptionUpdateEvents({ - shippingOptionsData: dataArray, - updatedShippingOptions, - optionTypeDeletedIds, - updatedRuleIds, - existingRuleIds, - sharedContext, - }) - return Array.isArray(data) ? updatedShippingOptions : updatedShippingOptions[0] } - private handleShippingOptionUpdateEvents({ - shippingOptionsData, - updatedShippingOptions, - optionTypeDeletedIds, - updatedRuleIds, - existingRuleIds, - sharedContext, - }) { - eventBuilders.updatedShippingOption({ - data: updatedShippingOptions, - sharedContext, - }) - eventBuilders.deletedShippingOptionType({ - data: optionTypeDeletedIds.map((id) => ({ id })), - sharedContext, - }) - - const createdOptionTypeIds = updatedShippingOptions - .filter((so) => { - const updateData = shippingOptionsData.find((sod) => sod.id === so.id) - return isObject(updateData?.type) && !("id" in updateData.type) - }) - .map((so) => so.type.id) - - eventBuilders.createdShippingOptionType({ - data: createdOptionTypeIds.map((id) => ({ id })), - sharedContext, - }) - - const createdRuleIds = updatedShippingOptions - .flatMap((so) => - [...so.rules].map((rule) => { - if (existingRuleIds.includes(rule.id)) { - return - } - - return rule.id - }) - ) - .filter((id): id is string => !!id) - - eventBuilders.createdShippingOptionRule({ - data: createdRuleIds.map((id) => ({ id })), - sharedContext, - }) - eventBuilders.updatedShippingOptionRule({ - data: updatedRuleIds.map((id) => ({ id })), - sharedContext, - }) - } - async upsertShippingOptions( data: FulfillmentTypes.UpsertShippingOptionDTO[], sharedContext?: Context @@ -1652,7 +1478,8 @@ export default class FulfillmentModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertShippingOptionTypes( data: | FulfillmentTypes.UpsertShippingOptionTypeDTO[] @@ -1662,12 +1489,7 @@ export default class FulfillmentModuleService | FulfillmentTypes.ShippingOptionTypeDTO[] | FulfillmentTypes.ShippingOptionTypeDTO > { - const input = Array.isArray(data) ? data : [data] - - const results = await this.shippingOptionTypeService_.upsert( - input, - sharedContext - ) + const results = await this.updateShippingOptionTypes_(data, sharedContext) const allTypes = await this.baseRepository_.serialize< | FulfillmentTypes.ShippingOptionTypeDTO[] @@ -1677,6 +1499,23 @@ export default class FulfillmentModuleService return Array.isArray(data) ? allTypes : allTypes[0] } + @InjectTransactionManager() + protected async updateShippingOptionTypes_( + data: + | FulfillmentTypes.UpsertShippingOptionTypeDTO[] + | FulfillmentTypes.UpsertShippingOptionTypeDTO, + sharedContext: Context + ): Promise[]> { + const input = Array.isArray(data) ? data : [data] + + const results = await this.shippingOptionTypeService_.upsert( + input, + sharedContext + ) + + return results + } + // @ts-expect-error updateShippingOptionTypes( id: string, @@ -1691,6 +1530,7 @@ export default class FulfillmentModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateShippingOptionTypes( idOrSelector: string | FulfillmentTypes.FilterableShippingOptionTypeProps, @@ -1747,7 +1587,8 @@ export default class FulfillmentModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async updateShippingProfiles( idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps, @@ -1756,6 +1597,25 @@ export default class FulfillmentModuleService ): Promise< FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[] > { + const profiles = await this.updateShippingProfiles_( + idOrSelector, + data, + sharedContext + ) + + const updatedProfiles = await this.baseRepository_.serialize< + FulfillmentTypes.ShippingProfileDTO[] + >(profiles) + + return isString(idOrSelector) ? updatedProfiles[0] : updatedProfiles + } + + @InjectTransactionManager() + protected async updateShippingProfiles_( + idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps, + data: FulfillmentTypes.UpdateShippingProfileDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let normalizedInput: ({ id: string } & FulfillmentTypes.UpdateShippingProfileDTO)[] = [] @@ -1784,11 +1644,7 @@ export default class FulfillmentModuleService sharedContext ) - const updatedProfiles = await this.baseRepository_.serialize< - FulfillmentTypes.ShippingProfileDTO[] - >(profiles) - - return isString(idOrSelector) ? updatedProfiles[0] : updatedProfiles + return profiles } async upsertShippingProfiles( @@ -1800,7 +1656,8 @@ export default class FulfillmentModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertShippingProfiles( data: | FulfillmentTypes.UpsertShippingProfileDTO[] @@ -1808,6 +1665,24 @@ export default class FulfillmentModuleService @MedusaContext() sharedContext: Context = {} ): Promise< FulfillmentTypes.ShippingProfileDTO[] | FulfillmentTypes.ShippingProfileDTO + > { + const profiles = await this.upsertShippingProfiles_(data, sharedContext) + + return await this.baseRepository_.serialize< + | FulfillmentTypes.ShippingProfileDTO[] + | FulfillmentTypes.ShippingProfileDTO + >(Array.isArray(data) ? profiles : profiles[0]) + } + + @InjectTransactionManager() + protected async upsertShippingProfiles_( + data: + | FulfillmentTypes.UpsertShippingProfileDTO[] + | FulfillmentTypes.UpsertShippingProfileDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType > { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter((prof) => !!prof.id) @@ -1831,13 +1706,7 @@ export default class FulfillmentModuleService ) } - const result = [...created, ...updated] - const allProfiles = await this.baseRepository_.serialize< - | FulfillmentTypes.ShippingProfileDTO[] - | FulfillmentTypes.ShippingProfileDTO - >(result) - - return Array.isArray(data) ? allProfiles : allProfiles[0] + return [...created, ...updated] } // @ts-expect-error @@ -1873,11 +1742,6 @@ export default class FulfillmentModuleService sharedContext ) - eventBuilders.updatedGeoZone({ - data: updatedGeoZones, - sharedContext, - }) - const serialized = await this.baseRepository_.serialize< FulfillmentTypes.GeoZoneDTO[] >(updatedGeoZones) @@ -1940,11 +1804,6 @@ export default class FulfillmentModuleService const updatedShippingOptionRules = await this.shippingOptionRuleService_.update(data_, sharedContext) - eventBuilders.updatedShippingOptionRule({ - data: updatedShippingOptionRules.map((rule) => ({ id: rule.id })), - sharedContext, - }) - return Array.isArray(data) ? updatedShippingOptionRules : updatedShippingOptionRules[0] @@ -1980,9 +1839,6 @@ export default class FulfillmentModuleService ) const updatedLabelIds: string[] = [] - let deletedLabelIds: string[] = [] - - const existingLabelIds = existingFulfillment.labels.map((label) => label.id) /** * @note @@ -1995,12 +1851,6 @@ export default class FulfillmentModuleService * and we also need to emit the events later on. */ if (isDefined(data.labels) && isPresent(data.labels)) { - const dataLabelIds: string[] = data.labels - .filter((label): label is { id: string } => "id" in label) - .map((label) => label.id) - - deletedLabelIds = arrayDifference(existingLabelIds, dataLabelIds) - for (let label of data.labels) { if (!("id" in label)) { continue @@ -2029,49 +1879,9 @@ export default class FulfillmentModuleService sharedContext ) - this.handleFulfillmentUpdateEvents( - fulfillment, - existingLabelIds, - updatedLabelIds, - deletedLabelIds, - sharedContext - ) - return fulfillment } - private handleFulfillmentUpdateEvents( - fulfillment: InferEntityType, - existingLabelIds: string[], - updatedLabelIds: string[], - deletedLabelIds: string[], - sharedContext: Context - ) { - eventBuilders.updatedFulfillment({ - data: [{ id: fulfillment.id }], - sharedContext, - }) - - eventBuilders.deletedFulfillmentLabel({ - data: deletedLabelIds.map((id) => ({ id })), - sharedContext, - }) - - eventBuilders.updatedFulfillmentLabel({ - data: updatedLabelIds.map((id) => ({ id })), - sharedContext, - }) - - const createdLabels = fulfillment.labels.filter((label) => { - return !existingLabelIds.includes(label.id) - }) - - eventBuilders.createdFulfillmentLabel({ - data: createdLabels.map((label) => ({ id: label.id })), - sharedContext, - }) - } - @InjectManager() @EmitEvents() async cancelFulfillment( @@ -2106,11 +1916,6 @@ export default class FulfillmentModuleService }, sharedContext ) - - eventBuilders.updatedFulfillment({ - data: [{ id }], - sharedContext, - }) } const result = await this.baseRepository_.serialize( @@ -2211,6 +2016,7 @@ export default class FulfillmentModuleService } @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async deleteShippingProfiles( ids: string | string[], @@ -2226,6 +2032,7 @@ export default class FulfillmentModuleService } @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async softDeleteShippingProfiles< TReturnableLinkableKeys extends string = string diff --git a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts index f142c1bb00..6a229bcec2 100644 --- a/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts +++ b/packages/modules/inventory/integration-tests/__tests__/inventory-module-service.spec.ts @@ -436,17 +436,32 @@ moduleIntegrationTestRunner({ const updated = await service.updateReservationItems(update) + const inventoryLevel = + await service.retrieveInventoryLevelByItemAndLocation( + reservationItem.inventory_item_id, + "location-1" + ) + expect(updated).toEqual(expect.objectContaining(update)) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy.mock.calls[0][0]).toHaveLength(2) expect(eventBusSpy).toHaveBeenNthCalledWith( 1, - [ + expect.arrayContaining([ composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, { data: { id: reservationItem.id }, - object: "reservation-item", + object: "reservation_item", source: Modules.INVENTORY, action: CommonEvents.UPDATED, }), - ], + composeMessage(InventoryEvents.INVENTORY_LEVEL_UPDATED, { + data: { id: inventoryLevel.id }, + object: "inventory_level", + source: Modules.INVENTORY, + action: CommonEvents.UPDATED, + }), + ]), { internal: true, } @@ -457,19 +472,30 @@ moduleIntegrationTestRunner({ quantity: 10, } + eventBusSpy.mockClear() + const updated2 = await service.updateReservationItems(update2) expect(updated2).toEqual(expect.objectContaining(update2)) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy.mock.calls[0][0]).toHaveLength(2) expect(eventBusSpy).toHaveBeenNthCalledWith( - 2, - [ + 1, + expect.arrayContaining([ composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, { data: { id: reservationItem.id }, - object: "reservation-item", + object: "reservation_item", source: Modules.INVENTORY, action: CommonEvents.UPDATED, }), - ], + composeMessage(InventoryEvents.INVENTORY_LEVEL_UPDATED, { + data: { id: inventoryLevel.id }, + object: "inventory_level", + source: Modules.INVENTORY, + action: CommonEvents.UPDATED, + }), + ]), { internal: true, } diff --git a/packages/modules/inventory/src/services/inventory-module.ts b/packages/modules/inventory/src/services/inventory-module.ts index 6cbeb5da10..ba9de326e6 100644 --- a/packages/modules/inventory/src/services/inventory-module.ts +++ b/packages/modules/inventory/src/services/inventory-module.ts @@ -15,19 +15,15 @@ import { import { arrayDifference, BigNumber, - CommonEvents, EmitEvents, InjectManager, InjectTransactionManager, - InventoryEvents, isDefined, isString, MathBN, MedusaContext, MedusaError, MedusaService, - moduleEventBuilderFactory, - Modules, partitionArray, } from "@medusajs/framework/utils" import { InventoryItem, InventoryLevel, ReservationItem } from "@models" @@ -249,21 +245,9 @@ export default class InventoryModuleService const toCreate = Array.isArray(input) ? input : [input] const created = await this.createReservationItems_(toCreate, context) - moduleEventBuilderFactory({ - action: CommonEvents.CREATED, - object: "reservation-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.RESERVATION_ITEM_CREATED, - })({ - data: created, - sharedContext: context, - }) - const serializedReservations = await this.baseRepository_.serialize< InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO - >(created, { - populate: true, - }) + >(created) return Array.isArray(input) ? serializedReservations @@ -350,21 +334,9 @@ export default class InventoryModuleService ) const result = await this.createInventoryItems_(toCreate, context) - moduleEventBuilderFactory({ - action: CommonEvents.CREATED, - object: "inventory-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_ITEM_CREATED, - })({ - data: result, - sharedContext: context, - }) - const serializedItems = await this.baseRepository_.serialize< InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[] - >(result, { - populate: true, - }) + >(result) return Array.isArray(input) ? serializedItems : serializedItems[0] } @@ -374,7 +346,7 @@ export default class InventoryModuleService input: InventoryTypes.CreateInventoryItemInput[], @MedusaContext() context: Context = {} ): Promise { - return await this.inventoryItemService_.create(input) + return await this.inventoryItemService_.create(input, context) } // @ts-ignore @@ -405,21 +377,9 @@ export default class InventoryModuleService const created = await this.createInventoryLevels_(toCreate, context) - moduleEventBuilderFactory({ - action: CommonEvents.CREATED, - object: "inventory-level", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_LEVEL_CREATED, - })({ - data: created, - sharedContext: context, - }) - const serialized = await this.baseRepository_.serialize< InventoryTypes.InventoryLevelDTO[] | InventoryTypes.InventoryLevelDTO - >(created, { - populate: true, - }) + >(created) return Array.isArray(input) ? serialized : serialized[0] } @@ -460,21 +420,9 @@ export default class InventoryModuleService const result = await this.updateInventoryItems_(updates, context) - moduleEventBuilderFactory({ - action: CommonEvents.UPDATED, - object: "inventory-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_ITEM_UPDATED, - })({ - data: result, - sharedContext: context, - }) - const serializedItems = await this.baseRepository_.serialize< InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[] - >(result, { - populate: true, - }) + >(result) return Array.isArray(input) ? serializedItems : serializedItems[0] } @@ -489,7 +437,7 @@ export default class InventoryModuleService return await this.inventoryItemService_.update(input, context) } - @InjectTransactionManager() + @InjectManager() @EmitEvents() async deleteInventoryItemLevelByLocationId( locationId: string | string[], @@ -500,16 +448,6 @@ export default class InventoryModuleService context ) - moduleEventBuilderFactory({ - action: CommonEvents.DELETED, - object: "inventory-level", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_LEVEL_DELETED, - })({ - data: result[0], - sharedContext: context, - }) - return result } @@ -532,16 +470,6 @@ export default class InventoryModuleService context ) - moduleEventBuilderFactory({ - action: CommonEvents.DELETED, - object: "inventory-level", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_LEVEL_DELETED, - })({ - data: { id: inventoryLevel.id }, - sharedContext: context, - }) - if (!inventoryLevel) { return } @@ -577,21 +505,9 @@ export default class InventoryModuleService const levels = await this.updateInventoryLevels_(input, context) - moduleEventBuilderFactory({ - action: CommonEvents.UPDATED, - object: "inventory-level", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_LEVEL_UPDATED, - })({ - data: levels, - sharedContext: context, - }) - const updatedLevels = await this.baseRepository_.serialize< InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[] - >(levels, { - populate: true, - }) + >(levels) return Array.isArray(updates) ? updatedLevels : updatedLevels[0] } @@ -659,21 +575,9 @@ export default class InventoryModuleService const update = Array.isArray(input) ? input : [input] const result = await this.updateReservationItems_(update, context) - moduleEventBuilderFactory({ - action: CommonEvents.UPDATED, - object: "reservation-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.RESERVATION_ITEM_UPDATED, - })({ - data: result, - sharedContext: context, - }) - const serialized = await this.baseRepository_.serialize< InventoryTypes.ReservationItemDTO | InventoryTypes.ReservationItemDTO[] - >(result, { - populate: true, - }) + >(result) return Array.isArray(input) ? serialized : serialized[0] } @@ -807,33 +711,47 @@ export default class InventoryModuleService return result } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async softDeleteReservationItems( ids: string | string[], config?: SoftDeleteReturn, @MedusaContext() context: Context = {} + ): Promise { + return await this.softDeleteReservationItems_(ids, config, context) + } + + @InjectTransactionManager() + protected async softDeleteReservationItems_( + ids: string | string[], + config?: SoftDeleteReturn, + @MedusaContext() context: Context = {} ): Promise { const reservations: InventoryTypes.ReservationItemDTO[] = await super.listReservationItems({ id: ids }, {}, context) - const result = await super.softDeleteReservationItems( - { id: ids }, - config, - context - ) + await super.softDeleteReservationItems({ id: ids }, config, context) await this.adjustInventoryLevelsForReservationsDeletion( reservations, context ) + } - result + @InjectManager() + @EmitEvents() + // @ts-expect-error + async restoreReservationItems( + ids: string | string[], + config?: RestoreReturn, + @MedusaContext() context: Context = {} + ): Promise { + return await this.restoreReservationItems_(ids, config, context) } @InjectTransactionManager() - // @ts-expect-error - async restoreReservationItems( + protected async restoreReservationItems_( ids: string | string[], config?: RestoreReturn, @MedusaContext() context: Context = {} @@ -849,11 +767,19 @@ export default class InventoryModuleService ) } - @InjectTransactionManager() + @InjectManager() @EmitEvents() async deleteReservationItemByLocationId( locationId: string | string[], @MedusaContext() context: Context = {} + ): Promise { + return await this.deleteReservationItemByLocationId_(locationId, context) + } + + @InjectTransactionManager() + protected async deleteReservationItemByLocationId_( + locationId: string | string[], + @MedusaContext() context: Context = {} ): Promise { const reservations: InventoryTypes.ReservationItemDTO[] = await this.listReservationItems({ location_id: locationId }, {}, context) @@ -863,16 +789,6 @@ export default class InventoryModuleService context ) - moduleEventBuilderFactory({ - action: CommonEvents.DELETED, - object: "reservation-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.RESERVATION_ITEM_DELETED, - })({ - data: reservations, - sharedContext: context, - }) - await this.adjustInventoryLevelsForReservationsDeletion( reservations, context @@ -885,11 +801,19 @@ export default class InventoryModuleService * @param context */ - @InjectTransactionManager() + @InjectManager() @EmitEvents() async deleteReservationItemsByLineItem( lineItemId: string | string[], @MedusaContext() context: Context = {} + ): Promise { + return await this.deleteReservationItemsByLineItem_(lineItemId, context) + } + + @InjectTransactionManager() + protected async deleteReservationItemsByLineItem_( + lineItemId: string | string[], + @MedusaContext() context: Context = {} ): Promise { const reservations: InventoryTypes.ReservationItemDTO[] = await this.listReservationItems({ line_item_id: lineItemId }, {}, context) @@ -903,16 +827,6 @@ export default class InventoryModuleService reservations, context ) - - moduleEventBuilderFactory({ - action: CommonEvents.DELETED, - object: "reservation-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.RESERVATION_ITEM_DELETED, - })({ - data: reservations, - sharedContext: context, - }) } /** @@ -921,11 +835,19 @@ export default class InventoryModuleService * @param context */ - @InjectTransactionManager() + @InjectManager() @EmitEvents() async restoreReservationItemsByLineItem( lineItemId: string | string[], @MedusaContext() context: Context = {} + ): Promise { + return await this.restoreReservationItemsByLineItem_(lineItemId, context) + } + + @InjectTransactionManager() + protected async restoreReservationItemsByLineItem_( + lineItemId: string | string[], + @MedusaContext() context: Context = {} ): Promise { const reservations: InventoryTypes.ReservationItemDTO[] = await this.listReservationItems({ line_item_id: lineItemId }, {}, context) @@ -939,16 +861,6 @@ export default class InventoryModuleService reservations, context ) - - moduleEventBuilderFactory({ - action: CommonEvents.CREATED, - object: "reservation-item", - source: Modules.INVENTORY, - eventName: InventoryEvents.RESERVATION_ITEM_CREATED, - })({ - data: reservations, - sharedContext: context, - }) } /** @@ -1008,23 +920,10 @@ export default class InventoryModuleService context ) results.push(result) - - moduleEventBuilderFactory({ - action: CommonEvents.UPDATED, - object: "inventory-level", - source: Modules.INVENTORY, - eventName: InventoryEvents.INVENTORY_LEVEL_UPDATED, - })({ - data: { id: result.id }, - sharedContext: context, - }) } return await this.baseRepository_.serialize( - Array.isArray(inventoryItemIdOrData) ? results : results[0], - { - populate: true, - } + Array.isArray(inventoryItemIdOrData) ? results : results[0] ) } diff --git a/packages/modules/notification/integration-tests/__tests__/notification-module-service/index.spec.ts b/packages/modules/notification/integration-tests/__tests__/notification-module-service/index.spec.ts index 7865e45559..63d713f5a9 100644 --- a/packages/modules/notification/integration-tests/__tests__/notification-module-service/index.spec.ts +++ b/packages/modules/notification/integration-tests/__tests__/notification-module-service/index.spec.ts @@ -43,6 +43,10 @@ moduleIntegrationTestRunner({ eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") }) + afterEach(() => { + eventBusEmitSpy.mockClear() + }) + it(`should export the appropriate linkable configuration`, () => { const linkable = Module(Modules.NOTIFICATION, { service: NotificationModuleService, @@ -119,16 +123,24 @@ moduleIntegrationTestRunner({ const result = await service.createNotifications(notification) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ + expect(eventBusEmitSpy).toHaveBeenCalledTimes(1) + expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2) + expect(eventBusEmitSpy).toHaveBeenNthCalledWith( + 1, + expect.arrayContaining([ composeMessage(NotificationEvents.NOTIFICATION_CREATED, { data: { id: result.id }, object: "notification", source: Modules.NOTIFICATION, action: CommonEvents.CREATED, }), - ], + composeMessage(NotificationEvents.NOTIFICATION_UPDATED, { + data: { id: result.id }, + object: "notification", + source: Modules.NOTIFICATION, + action: CommonEvents.UPDATED, + }), + ]), { internal: true, } diff --git a/packages/modules/notification/src/services/notification-module-service.ts b/packages/modules/notification/src/services/notification-module-service.ts index ff1b2e91ac..875f5859c5 100644 --- a/packages/modules/notification/src/services/notification-module-service.ts +++ b/packages/modules/notification/src/services/notification-module-service.ts @@ -19,7 +19,6 @@ import { promiseAll, } from "@medusajs/framework/utils" import { Notification } from "@models" -import { eventBuilders } from "@utils" import NotificationProviderService from "./notification-provider" type InjectedDependencies = { @@ -91,11 +90,6 @@ export default class NotificationModuleService NotificationTypes.NotificationDTO[] >(createdNotifications) - eventBuilders.createdNotification({ - data: serialized, - sharedContext, - }) - return Array.isArray(data) ? serialized : serialized[0] } diff --git a/packages/modules/notification/src/utils/events.ts b/packages/modules/notification/src/utils/events.ts deleted file mode 100644 index e489059c9e..0000000000 --- a/packages/modules/notification/src/utils/events.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - CommonEvents, - moduleEventBuilderFactory, - Modules, - NotificationEvents, -} from "@medusajs/framework/utils" - -export const eventBuilders = { - createdNotification: moduleEventBuilderFactory({ - source: Modules.NOTIFICATION, - action: CommonEvents.CREATED, - object: "notification", - eventName: NotificationEvents.NOTIFICATION_CREATED, - }), -} diff --git a/packages/modules/notification/src/utils/index.ts b/packages/modules/notification/src/utils/index.ts deleted file mode 100644 index 92c2484024..0000000000 --- a/packages/modules/notification/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./events" diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index a5c05a4171..ad3f4424ce 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -27,6 +27,7 @@ import { DecorateCartLikeInputDTO, decorateCartTotals, deduplicate, + EmitEvents, InjectManager, InjectTransactionManager, isDefined, @@ -417,7 +418,7 @@ export default class OrderModuleService async retrieveOrder( id: string, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -442,7 +443,7 @@ export default class OrderModuleService async listOrders( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -459,7 +460,7 @@ export default class OrderModuleService async listAndCountOrders( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise<[OrderTypes.OrderDTO[], number]> { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -483,7 +484,7 @@ export default class OrderModuleService async retrieveReturn( id: string, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -500,7 +501,7 @@ export default class OrderModuleService async listReturns( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -517,7 +518,7 @@ export default class OrderModuleService async listAndCountReturns( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise<[OrderTypes.ReturnDTO[], number]> { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -541,7 +542,7 @@ export default class OrderModuleService async retrieveOrderClaim( id: string, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -562,7 +563,7 @@ export default class OrderModuleService async listOrderClaims( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -583,7 +584,7 @@ export default class OrderModuleService async listAndCountOrderClaims( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise<[OrderTypes.OrderClaimDTO[], number]> { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -607,7 +608,7 @@ export default class OrderModuleService async retrieveOrderExchange( id: string, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -628,7 +629,7 @@ export default class OrderModuleService async listOrderExchanges( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -649,7 +650,7 @@ export default class OrderModuleService async listAndCountOrderExchanges( filters?: any, config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined + sharedContext?: Context ): Promise<[OrderTypes.OrderExchangeDTO[], number]> { config ??= {} const includeTotals = this.shouldIncludeTotals(config) @@ -681,6 +682,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createOrders( data: OrderTypes.CreateOrderDTO[] | OrderTypes.CreateOrderDTO, @@ -852,6 +854,7 @@ export default class OrderModuleService } @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async deleteOrders( orderIds: string | string[], @@ -930,6 +933,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateOrders( dataOrIdOrSelector: @@ -947,9 +951,7 @@ export default class OrderModuleService const serializedResult = await this.baseRepository_.serialize< OrderTypes.OrderDTO[] - >(result, { - populate: true, - }) + >(result) return isString(dataOrIdOrSelector) ? serializedResult[0] : serializedResult } @@ -1008,6 +1010,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderLineItems( orderIdOrData: @@ -1053,10 +1056,7 @@ export default class OrderModuleService } return await this.baseRepository_.serialize( - items, - { - populate: true, - } + items ) } @@ -1134,6 +1134,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateOrderLineItems( lineItemIdOrDataOrSelector: @@ -1154,10 +1155,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - item, - { - populate: true, - } + item ) } @@ -1176,10 +1174,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - items, - { - populate: true, - } + items ) } @@ -1257,6 +1252,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() async updateOrderItem( orderItemIdOrDataOrSelector: | string @@ -1275,12 +1271,7 @@ export default class OrderModuleService sharedContext ) - return await this.baseRepository_.serialize( - item, - { - populate: true, - } - ) + return await this.baseRepository_.serialize(item) } const toUpdate = Array.isArray(orderItemIdOrDataOrSelector) @@ -1295,10 +1286,7 @@ export default class OrderModuleService items = await this.updateOrderItemWithSelector_(toUpdate, sharedContext) return await this.baseRepository_.serialize( - items, - { - populate: true, - } + items ) } @@ -1356,6 +1344,7 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderShippingMethods( orderIdOrData: @@ -1409,7 +1398,7 @@ export default class OrderModuleService return await this.baseRepository_.serialize< OrderTypes.OrderShippingMethodDTO[] - >(methods, { populate: true }) + >(methods) } @InjectTransactionManager() @@ -1453,11 +1442,27 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() // @ts-ignore async softDeleteOrderShippingMethods( ids: string | object | string[] | object[], config?: SoftDeleteReturn, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise | void> { + return await this.softDeleteOrderShippingMethods_( + ids, + config, + sharedContext + ) + } + + @InjectTransactionManager() + protected async softDeleteOrderShippingMethods_< + TReturnableLinkableKeys extends string + >( + ids: string | object | string[] | object[], + config?: SoftDeleteReturn, + @MedusaContext() sharedContext: Context = {} ): Promise | void> { const rel = await super.listOrderShippings( { @@ -1479,11 +1484,23 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() // @ts-ignore async restoreOrderShippingMethods( ids: string | object | string[] | object[], config?: RestoreReturn, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise | void> { + return await this.restoreOrderShippingMethods_(ids, config, sharedContext) + } + + @InjectTransactionManager() + protected async restoreOrderShippingMethods_< + TReturnableLinkableKeys extends string + >( + ids: string | object | string[] | object[], + config?: RestoreReturn, + @MedusaContext() sharedContext: Context = {} ): Promise | void> { const rel = await super.listOrderShippings( { @@ -1519,7 +1536,8 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderLineItemAdjustments( orderIdOrData: @@ -1529,6 +1547,26 @@ export default class OrderModuleService adjustments?: OrderTypes.CreateOrderLineItemAdjustmentDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const addedAdjustments = await this.createOrderLineItemAdjustments_( + orderIdOrData, + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderTypes.OrderLineItemAdjustmentDTO[] + >(addedAdjustments) + } + + @InjectTransactionManager() + protected async createOrderLineItemAdjustments_( + orderIdOrData: + | string + | OrderTypes.CreateOrderLineItemAdjustmentDTO[] + | OrderTypes.CreateOrderLineItemAdjustmentDTO, + adjustments?: OrderTypes.CreateOrderLineItemAdjustmentDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let addedAdjustments: InferEntityType[] = [] if (isString(orderIdOrData)) { const order = await this.retrieveOrder( @@ -1563,14 +1601,11 @@ export default class OrderModuleService ) } - return await this.baseRepository_.serialize< - OrderTypes.OrderLineItemAdjustmentDTO[] - >(addedAdjustments, { - populate: true, - }) + return addedAdjustments } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertOrderLineItemAdjustments( adjustments: ( | OrderTypes.CreateOrderLineItemAdjustmentDTO @@ -1578,19 +1613,32 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { - let result = await this.orderLineItemAdjustmentService_.upsert( + let result = await this.upsertOrderLineItemAdjustments_( adjustments, sharedContext ) return await this.baseRepository_.serialize< OrderTypes.OrderLineItemAdjustmentDTO[] - >(result, { - populate: true, - }) + >(result) } @InjectTransactionManager() + protected async upsertOrderLineItemAdjustments_( + adjustments: ( + | OrderTypes.CreateOrderLineItemAdjustmentDTO + | OrderTypes.UpdateOrderLineItemAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { + return await this.orderLineItemAdjustmentService_.upsert( + adjustments, + sharedContext + ) + } + + @InjectManager() + @EmitEvents() async setOrderLineItemAdjustments( orderId: string, adjustments: ( @@ -1599,6 +1647,26 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.setOrderLineItemAdjustments_( + orderId, + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderTypes.OrderLineItemAdjustmentDTO[] + >(result) + } + + @InjectTransactionManager() + protected async setOrderLineItemAdjustments_( + orderId: string, + adjustments: ( + | OrderTypes.CreateOrderLineItemAdjustmentDTO + | OrderTypes.UpdateOrderLineItemAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const order = await this.retrieveOrder( orderId, { select: ["id"], relations: ["items.item.adjustments"] }, @@ -1629,19 +1697,16 @@ export default class OrderModuleService await this.orderLineItemAdjustmentService_.delete(toDelete, sharedContext) } - let result = await this.orderLineItemAdjustmentService_.upsert( + const result = await this.orderLineItemAdjustmentService_.upsert( adjustments, sharedContext ) - return await this.baseRepository_.serialize< - OrderTypes.OrderLineItemAdjustmentDTO[] - >(result, { - populate: true, - }) + return result } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertOrderShippingMethodAdjustments( adjustments: ( | OrderTypes.CreateOrderShippingMethodAdjustmentDTO @@ -1649,19 +1714,32 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { - const result = await this.orderShippingMethodAdjustmentService_.upsert( + const result = await this.upsertOrderShippingMethodAdjustments_( adjustments, sharedContext ) return await this.baseRepository_.serialize< OrderTypes.OrderShippingMethodAdjustmentDTO[] - >(result, { - populate: true, - }) + >(result) } @InjectTransactionManager() + protected async upsertOrderShippingMethodAdjustments_( + adjustments: ( + | OrderTypes.CreateOrderShippingMethodAdjustmentDTO + | OrderTypes.UpdateOrderShippingMethodAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { + return await this.orderShippingMethodAdjustmentService_.upsert( + adjustments, + sharedContext + ) + } + + @InjectManager() + @EmitEvents() async setOrderShippingMethodAdjustments( orderId: string, adjustments: ( @@ -1670,6 +1748,26 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.setOrderShippingMethodAdjustments_( + orderId, + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderTypes.OrderShippingMethodAdjustmentDTO[] + >(result) + } + + @InjectTransactionManager() + protected async setOrderShippingMethodAdjustments_( + orderId: string, + adjustments: ( + | OrderTypes.CreateOrderShippingMethodAdjustmentDTO + | OrderTypes.UpdateOrderShippingMethodAdjustmentDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const order = await this.retrieveOrder( orderId, { select: ["id"], relations: ["shipping_methods.adjustments"] }, @@ -1710,11 +1808,7 @@ export default class OrderModuleService sharedContext ) - return await this.baseRepository_.serialize< - OrderTypes.OrderShippingMethodAdjustmentDTO[] - >(result, { - populate: true, - }) + return result } // @ts-ignore @@ -1732,7 +1826,8 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderShippingMethodAdjustments( orderIdOrData: @@ -1744,6 +1839,30 @@ export default class OrderModuleService ): Promise< | OrderTypes.OrderShippingMethodAdjustmentDTO[] | OrderTypes.OrderShippingMethodAdjustmentDTO + > { + const addedAdjustments = await this.createOrderShippingMethodAdjustments_( + orderIdOrData, + adjustments, + sharedContext + ) + + return await this.baseRepository_.serialize< + | OrderTypes.OrderShippingMethodAdjustmentDTO[] + | OrderTypes.OrderShippingMethodAdjustmentDTO + >(addedAdjustments) + } + + @InjectTransactionManager() + protected async createOrderShippingMethodAdjustments_( + orderIdOrData: + | string + | OrderTypes.CreateOrderShippingMethodAdjustmentDTO[] + | OrderTypes.CreateOrderShippingMethodAdjustmentDTO, + adjustments?: OrderTypes.CreateOrderShippingMethodAdjustmentDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType > { let addedAdjustments: InferEntityType< typeof OrderShippingMethodAdjustment @@ -1784,19 +1903,10 @@ export default class OrderModuleService } if (isObject(orderIdOrData)) { - return await this.baseRepository_.serialize( - addedAdjustments[0], - { - populate: true, - } - ) + return addedAdjustments[0] } - return await this.baseRepository_.serialize< - OrderTypes.OrderShippingMethodAdjustmentDTO[] - >(addedAdjustments, { - populate: true, - }) + return addedAdjustments } // @ts-ignore @@ -1816,7 +1926,8 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderLineItemTaxLines( orderIdOrData: @@ -1829,6 +1940,37 @@ export default class OrderModuleService @MedusaContext() sharedContext: Context = {} ): Promise< OrderTypes.OrderLineItemTaxLineDTO[] | OrderTypes.OrderLineItemTaxLineDTO + > { + const addedTaxLines = await this.createOrderLineItemTaxLines_( + orderIdOrData, + taxLines, + sharedContext + ) + + const serialized = await this.baseRepository_.serialize< + OrderTypes.OrderLineItemTaxLineDTO[] | OrderTypes.OrderLineItemTaxLineDTO + >(addedTaxLines) + + if (isObject(orderIdOrData)) { + return serialized[0] + } + + return serialized + } + + @InjectTransactionManager() + protected async createOrderLineItemTaxLines_( + orderIdOrData: + | string + | OrderTypes.CreateOrderLineItemTaxLineDTO[] + | OrderTypes.CreateOrderLineItemTaxLineDTO, + taxLines?: + | OrderTypes.CreateOrderLineItemTaxLineDTO[] + | OrderTypes.CreateOrderLineItemTaxLineDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise< + | InferEntityType[] + | InferEntityType > { let addedTaxLines: InferEntityType[] if (isString(orderIdOrData)) { @@ -1849,20 +1991,15 @@ export default class OrderModuleService ) } - const serialized = await this.baseRepository_.serialize< - OrderTypes.OrderLineItemTaxLineDTO[] - >(addedTaxLines, { - populate: true, - }) - if (isObject(orderIdOrData)) { - return serialized[0] + return addedTaxLines[0] } - return serialized + return addedTaxLines } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertOrderLineItemTaxLines( taxLines: ( | OrderTypes.CreateOrderLineItemTaxLineDTO @@ -1870,19 +2007,34 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { - const result = await this.orderLineItemTaxLineService_.upsert( - taxLines as UpdateOrderLineItemTaxLineDTO[], + const result = await this.upsertOrderLineItemTaxLines_( + taxLines, sharedContext ) return await this.baseRepository_.serialize< OrderTypes.OrderLineItemTaxLineDTO[] - >(result, { - populate: true, - }) + >(result) } @InjectTransactionManager() + protected async upsertOrderLineItemTaxLines_( + taxLines: ( + | OrderTypes.CreateOrderLineItemTaxLineDTO + | OrderTypes.UpdateOrderLineItemTaxLineDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { + const result = await this.orderLineItemTaxLineService_.upsert( + taxLines as UpdateOrderLineItemTaxLineDTO[], + sharedContext + ) + + return result + } + + @InjectManager() + @EmitEvents() async setOrderLineItemTaxLines( orderId: string, taxLines: ( @@ -1891,6 +2043,26 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.setOrderLineItemTaxLines_( + orderId, + taxLines, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderTypes.OrderLineItemTaxLineDTO[] + >(result) + } + + @InjectTransactionManager() + protected async setOrderLineItemTaxLines_( + orderId: string, + taxLines: ( + | OrderTypes.CreateOrderLineItemTaxLineDTO + | OrderTypes.UpdateOrderLineItemTaxLineDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const order = await this.retrieveOrder( orderId, { select: ["id"], relations: ["items.item.tax_lines"] }, @@ -1926,11 +2098,7 @@ export default class OrderModuleService sharedContext ) - return await this.baseRepository_.serialize< - OrderTypes.OrderLineItemTaxLineDTO[] - >(result, { - populate: true, - }) + return result } // @ts-ignore @@ -1950,7 +2118,8 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderShippingMethodTaxLines( orderIdOrData: @@ -1965,6 +2134,35 @@ export default class OrderModuleService | OrderTypes.OrderShippingMethodTaxLineDTO[] | OrderTypes.OrderShippingMethodTaxLineDTO > { + const addedTaxLines = await this.createOrderShippingMethodTaxLines_( + orderIdOrData, + taxLines, + sharedContext + ) + + const serialized = await this.baseRepository_.serialize< + | OrderTypes.OrderShippingMethodTaxLineDTO[] + | OrderTypes.OrderShippingMethodTaxLineDTO + >(addedTaxLines) + + if (isObject(orderIdOrData)) { + return serialized[0] + } + + return serialized + } + + @InjectTransactionManager() + protected async createOrderShippingMethodTaxLines_( + orderIdOrData: + | string + | OrderTypes.CreateOrderShippingMethodTaxLineDTO[] + | OrderTypes.CreateOrderShippingMethodTaxLineDTO, + taxLines?: + | OrderTypes.CreateOrderShippingMethodTaxLineDTO[] + | OrderTypes.CreateOrderShippingMethodTaxLineDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let addedTaxLines: InferEntityType[] if (isString(orderIdOrData)) { const lines = Array.isArray(taxLines) ? taxLines : [taxLines] @@ -1980,22 +2178,11 @@ export default class OrderModuleService ) } - const serialized = - await this.baseRepository_.serialize( - addedTaxLines[0], - { - populate: true, - } - ) - - if (isObject(orderIdOrData)) { - return serialized[0] - } - - return serialized + return addedTaxLines } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertOrderShippingMethodTaxLines( taxLines: ( | OrderTypes.CreateOrderShippingMethodTaxLineDTO @@ -2010,12 +2197,11 @@ export default class OrderModuleService return await this.baseRepository_.serialize< OrderTypes.OrderShippingMethodTaxLineDTO[] - >(result, { - populate: true, - }) + >(result) } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async setOrderShippingMethodTaxLines( orderId: string, taxLines: ( @@ -2024,6 +2210,26 @@ export default class OrderModuleService )[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.setOrderShippingMethodTaxLines_( + orderId, + taxLines, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderTypes.OrderShippingMethodTaxLineDTO[] + >(result) + } + + @InjectTransactionManager() + protected async setOrderShippingMethodTaxLines_( + orderId: string, + taxLines: ( + | OrderTypes.CreateOrderShippingMethodTaxLineDTO + | OrderTypes.UpdateOrderShippingMethodTaxLineDTO + )[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const order = await this.retrieveOrder( orderId, { select: ["id"], relations: ["shipping_methods.tax_lines"] }, @@ -2063,11 +2269,7 @@ export default class OrderModuleService sharedContext ) - return await this.baseRepository_.serialize< - OrderTypes.OrderShippingMethodTaxLineDTO[] - >(result, { - populate: true, - }) + return result } // @ts-ignore @@ -2082,11 +2284,12 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createReturns( data: OrderTypes.CreateOrderReturnDTO | OrderTypes.CreateOrderReturnDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const created = await this.createOrderRelatedEntity_( data, @@ -2095,10 +2298,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - !Array.isArray(data) ? created[0] : created, - { - populate: true, - } + !Array.isArray(data) ? created[0] : created ) } @@ -2114,11 +2314,12 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderClaims( data: OrderTypes.CreateOrderClaimDTO | OrderTypes.CreateOrderClaimDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const created = await this.createOrderRelatedEntity_( data, @@ -2127,10 +2328,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - !Array.isArray(data) ? created[0] : created, - { - populate: true, - } + !Array.isArray(data) ? created[0] : created ) } @@ -2146,13 +2344,14 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createOrderExchanges( data: | OrderTypes.CreateOrderExchangeDTO | OrderTypes.CreateOrderExchangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const created = await this.createOrderRelatedEntity_( data, @@ -2161,10 +2360,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - !Array.isArray(data) ? created[0] : created, - { - populate: true, - } + !Array.isArray(data) ? created[0] : created ) } @@ -2172,7 +2368,7 @@ export default class OrderModuleService private async createOrderRelatedEntity_( data: any, service: any, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ) { const data_ = Array.isArray(data) ? data : [data] @@ -2215,24 +2411,22 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() async createOrderChange( data: CreateOrderChangeDTO | CreateOrderChangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const changes = await this.createOrderChange_(data, sharedContext) return await this.baseRepository_.serialize( - Array.isArray(data) ? changes : changes[0], - { - populate: true, - } + Array.isArray(data) ? changes : changes[0] ) } @InjectTransactionManager() protected async createOrderChange_( data: CreateOrderChangeDTO | CreateOrderChangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise[]> { const dataArr = Array.isArray(data) ? data : [data] const orderIds: string[] = [] @@ -2291,7 +2485,10 @@ export default class OrderModuleService } @InjectManager() - async previewOrderChange(orderId: string, sharedContext?: Context) { + async previewOrderChange( + orderId: string, + @MedusaContext() sharedContext: Context = {} + ) { const order = await this.retrieveOrder( orderId, { @@ -2343,7 +2540,7 @@ export default class OrderModuleService order, itemsToUpsert, shippingMethodsToUpsert, - sharedContext + sharedContext: Context = {} ) { const addedItems = {} const addedShippingMethods = {} @@ -2470,13 +2667,14 @@ export default class OrderModuleService ): Promise @InjectTransactionManager() + @EmitEvents() async cancelOrderChange( orderChangeIdOrData: | string | string[] | OrderTypes.CancelOrderChangeDTO | OrderTypes.CancelOrderChangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const data = Array.isArray(orderChangeIdOrData) ? orderChangeIdOrData @@ -2509,14 +2707,35 @@ export default class OrderModuleService data: OrderTypes.ConfirmOrderChangeDTO[], sharedContext?: Context ) + @InjectManager() + @EmitEvents() async confirmOrderChange( orderChangeIdOrData: | string | string[] | OrderTypes.ConfirmOrderChangeDTO | OrderTypes.ConfirmOrderChangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.confirmOrderChange_( + orderChangeIdOrData, + sharedContext + ) + + return await this.baseRepository_.serialize( + result + ) + } + + @InjectTransactionManager() + protected async confirmOrderChange_( + orderChangeIdOrData: + | string + | string[] + | OrderTypes.ConfirmOrderChangeDTO + | OrderTypes.ConfirmOrderChangeDTO[], + @MedusaContext() sharedContext: Context = {} ): Promise { const data = Array.isArray(orderChangeIdOrData) ? orderChangeIdOrData @@ -2559,14 +2778,28 @@ export default class OrderModuleService data: OrderTypes.DeclineOrderChangeDTO[], sharedContext?: Context ) - @InjectTransactionManager() + + @InjectManager() + @EmitEvents() async declineOrderChange( orderChangeIdOrData: | string | string[] | OrderTypes.DeclineOrderChangeDTO | OrderTypes.DeclineOrderChangeDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.declineOrderChange_(orderChangeIdOrData, sharedContext) + } + + @InjectTransactionManager() + protected async declineOrderChange_( + orderChangeIdOrData: + | string + | string[] + | OrderTypes.DeclineOrderChangeDTO + | OrderTypes.DeclineOrderChangeDTO[], + @MedusaContext() sharedContext: Context = {} ): Promise { const data = Array.isArray(orderChangeIdOrData) ? orderChangeIdOrData @@ -2599,12 +2832,29 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() async registerOrderChange( data: | OrderTypes.RegisterOrderChangeDTO | OrderTypes.RegisterOrderChangeDTO[], - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.registerOrderChange_(data, sharedContext) + + return await this.baseRepository_.serialize< + OrderTypes.OrderChangeDTO | OrderTypes.OrderChangeDTO[] + >(result) + } + + @InjectTransactionManager() + protected async registerOrderChange_( + data: + | OrderTypes.RegisterOrderChangeDTO + | OrderTypes.RegisterOrderChangeDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise< + InferEntityType | InferEntityType[] + > { const inputData = Array.isArray(data) ? data : [data] const orders = await this.orderService_.list( @@ -2615,7 +2865,7 @@ export default class OrderModuleService const orderVersionsMap = new Map(orders.map((o) => [o.id, o.version])) - const changes = (await this.orderChangeService_.create( + const changes = await this.orderChangeService_.create( inputData.map((d) => ({ order_id: d.order_id, change_type: d.change_type, @@ -2637,15 +2887,28 @@ export default class OrderModuleService ], })), sharedContext - )) as unknown as OrderTypes.OrderChangeDTO[] + ) - return Array.isArray(data) ? changes : changes[0] + return changes } @InjectManager() + @EmitEvents() async applyPendingOrderActions( orderId: string | string[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise { + const result = await this.applyPendingOrderActions_(orderId, sharedContext) + + return await this.baseRepository_.serialize( + result + ) + } + + @InjectTransactionManager() + protected async applyPendingOrderActions_( + orderId: string | string[], + @MedusaContext() sharedContext: Context = {} ): Promise { const orderIds = Array.isArray(orderId) ? orderId : [orderId] @@ -2695,9 +2958,10 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() async revertLastVersion( orderId: string, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ) { const order = await super.retrieveOrder( orderId, @@ -2718,10 +2982,11 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() async undoLastChange( orderId: string, lastOrderChange?: Partial, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ) { const order = await super.retrieveOrder( orderId, @@ -2745,7 +3010,7 @@ export default class OrderModuleService protected async undoLastChange_( order: OrderDTO, lastOrderChange?: Partial, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const currentVersion = order.version @@ -2861,7 +3126,7 @@ export default class OrderModuleService @InjectTransactionManager() protected async revertLastChange_( order: OrderDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const currentVersion = order.version @@ -3097,15 +3362,32 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async addOrderAction( data: | OrderTypes.CreateOrderChangeActionDTO | OrderTypes.CreateOrderChangeActionDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< OrderTypes.OrderChangeActionDTO | OrderTypes.OrderChangeActionDTO[] > { + const actions = await this.addOrderAction_(data, sharedContext) + + const serializedActions = await this.baseRepository_.serialize< + OrderTypes.OrderChangeActionDTO | OrderTypes.OrderChangeActionDTO[] + >(actions) + + return Array.isArray(data) ? serializedActions : serializedActions[0] + } + + @InjectTransactionManager() + protected async addOrderAction_( + data: + | OrderTypes.CreateOrderChangeActionDTO + | OrderTypes.CreateOrderChangeActionDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let dataArr = Array.isArray(data) ? data : [data] const orderChangeMap = {} @@ -3135,18 +3417,18 @@ export default class OrderModuleService } } - const actions = (await this.orderChangeActionService_.create( + const actions = await this.orderChangeActionService_.create( dataArr, sharedContext - )) as unknown as OrderTypes.OrderChangeActionDTO[] + ) - return Array.isArray(data) ? actions : actions[0] + return actions } @InjectTransactionManager() private async applyOrderChanges_( changeActions: ApplyOrderChangeDTO[], - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const actionsMap: Record = {} const ordersIds: string[] = [] @@ -3258,14 +3540,36 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() async addOrderTransactions( transactionData: | OrderTypes.CreateOrderTransactionDTO | OrderTypes.CreateOrderTransactionDTO[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise< OrderTypes.OrderTransactionDTO | OrderTypes.OrderTransactionDTO[] > { + const created = await this.addOrderTransactions_( + transactionData, + sharedContext + ) + + const serializedTransactions = await this.baseRepository_.serialize< + OrderTypes.OrderTransactionDTO | OrderTypes.OrderTransactionDTO[] + >(created) + + return Array.isArray(transactionData) + ? serializedTransactions + : (serializedTransactions[0] as OrderTypes.OrderTransactionDTO) + } + + @InjectTransactionManager() + private async addOrderTransactions_( + transactionData: + | OrderTypes.CreateOrderTransactionDTO + | OrderTypes.CreateOrderTransactionDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const orders = await this.orderService_.list( { id: Array.isArray(transactionData) @@ -3296,19 +3600,15 @@ export default class OrderModuleService await this.updateOrderPaidRefundableAmount_(created, false, sharedContext) - return await this.baseRepository_.serialize( - !Array.isArray(transactionData) ? created[0] : created, - { - populate: true, - } - ) + return created } - @InjectManager() + @InjectTransactionManager() + @EmitEvents() // @ts-ignore async deleteOrderTransactions( transactionIds: string | object | string[] | object[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const data = Array.isArray(transactionIds) ? transactionIds @@ -3334,11 +3634,29 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() // @ts-ignore async softDeleteOrderTransactions( transactionIds: string | object | string[] | object[], config?: SoftDeleteReturn, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise | void> { + const returned = await this.softDeleteOrderTransactions_( + transactionIds, + config, + sharedContext + ) + + return returned + } + + @InjectTransactionManager() + private async softDeleteOrderTransactions_< + TReturnableLinkableKeys extends string + >( + transactionIds: string | object | string[] | object[], + config?: SoftDeleteReturn, + @MedusaContext() sharedContext: Context = {} ): Promise | void> { const transactions = await super.listOrderTransactions( { @@ -3366,11 +3684,29 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() // @ts-ignore async restoreOrderTransactions( transactionIds: string | object | string[] | object[], config?: RestoreReturn, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise | void> { + const returned = await this.restoreOrderTransactions_( + transactionIds, + config, + sharedContext + ) + + return returned + } + + @InjectTransactionManager() + private async restoreOrderTransactions_< + TReturnableLinkableKeys extends string + >( + transactionIds: string | object | string[] | object[], + config?: RestoreReturn, + @MedusaContext() sharedContext: Context = {} ): Promise | void> { const transactions = await super.listOrderTransactions( { @@ -3406,7 +3742,7 @@ export default class OrderModuleService amount: BigNumber | number | BigNumberInput }[], isRemoved: boolean, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ) { const summaries: any = await super.listOrderSummaries( { @@ -3471,11 +3807,24 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async archive( orderId: string | string[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { + const orders = await this.archive_(orderId, sharedContext) + + return await this.baseRepository_.serialize< + OrderTypes.OrderDTO | OrderTypes.OrderDTO[] + >(Array.isArray(orderId) ? orders : orders[0]) + } + + @InjectTransactionManager() + private async archive_( + orderId: string | string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { const orderIds = Array.isArray(orderId) ? orderId : [orderId] const orders = await this.listOrders( { @@ -3519,7 +3868,7 @@ export default class OrderModuleService sharedContext ) - return Array.isArray(orderId) ? orders : orders[0] + return orders } async completeOrder( @@ -3531,11 +3880,24 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async completeOrder( orderId: string | string[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { + const orders = await this.completeOrder_(orderId, sharedContext) + + return await this.baseRepository_.serialize< + OrderTypes.OrderDTO | OrderTypes.OrderDTO[] + >(Array.isArray(orderId) ? orders : orders[0]) + } + + @InjectTransactionManager() + private async completeOrder_( + orderId: string | string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { const orderIds = Array.isArray(orderId) ? orderId : [orderId] const orders = await this.listOrders( { @@ -3571,7 +3933,7 @@ export default class OrderModuleService sharedContext ) - return Array.isArray(orderId) ? orders : orders[0] + return orders } async cancel( @@ -3583,11 +3945,24 @@ export default class OrderModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async cancel( orderId: string | string[], - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { + const orders = await this.cancel_(orderId, sharedContext) + + return await this.baseRepository_.serialize< + OrderTypes.OrderDTO | OrderTypes.OrderDTO[] + >(Array.isArray(orderId) ? orders : orders[0]) + } + + @InjectTransactionManager() + private async cancel_( + orderId: string | string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { const orderIds = Array.isArray(orderId) ? orderId : [orderId] const orders = await this.listOrders( { @@ -3614,15 +3989,26 @@ export default class OrderModuleService sharedContext ) - return Array.isArray(orderId) ? orders : orders[0] + return orders } // ------------------- Bundled Order Actions - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async createReturn( data: OrderTypes.CreateOrderReturnDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} + ): Promise { + const ret = await this.createReturn_(data, sharedContext) + + return await this.baseRepository_.serialize(ret) + } + + @InjectTransactionManager() + private async createReturn_( + data: OrderTypes.CreateOrderReturnDTO, + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await BundledActions.createReturn.bind(this)( data, @@ -3644,13 +4030,14 @@ export default class OrderModuleService } @InjectManager() + @EmitEvents() async receiveReturn( data: OrderTypes.ReceiveOrderReturnDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.receiveReturn_(data, sharedContext) - return await this.retrieveReturn(ret[0].id, { + const returned = await this.retrieveReturn(ret[0].id, { relations: [ "items", "items.item", @@ -3659,24 +4046,27 @@ export default class OrderModuleService "shipping_methods.adjustments", ], }) + + return await this.baseRepository_.serialize(returned) } @InjectTransactionManager() private async receiveReturn_( data: OrderTypes.ReceiveOrderReturnDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.receiveReturn.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() async cancelReturn( data: OrderTypes.CancelOrderReturnDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.cancelReturn_(data, sharedContext) - return await this.retrieveReturn(ret.id, { + const returned = await this.retrieveReturn(ret.id, { relations: [ "items", "shipping_methods", @@ -3684,20 +4074,23 @@ export default class OrderModuleService "shipping_methods.adjustments", ], }) + + return await this.baseRepository_.serialize(returned) } @InjectTransactionManager() private async cancelReturn_( data: OrderTypes.CancelOrderReturnDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.cancelReturn.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() async createClaim( data: OrderTypes.CreateOrderClaimDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.createClaim_(data, sharedContext) @@ -3720,46 +4113,45 @@ export default class OrderModuleService sharedContext ) - return await this.baseRepository_.serialize( - claim, - { - populate: true, - } - ) + return await this.baseRepository_.serialize(claim) } @InjectTransactionManager() async createClaim_( data: OrderTypes.CreateOrderClaimDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.createClaim.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() async cancelClaim( data: OrderTypes.CancelOrderClaimDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.cancelClaim_(data, sharedContext) - return await this.retrieveOrderClaim(ret.id, { + const claim = await this.retrieveOrderClaim(ret.id, { relations: ["additional_items", "claim_items", "return", "return.items"], }) + + return await this.baseRepository_.serialize(claim) } @InjectTransactionManager() private async cancelClaim_( data: OrderTypes.CancelOrderClaimDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.cancelClaim.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() async createExchange( data: OrderTypes.CreateOrderExchangeDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.createExchange_(data, sharedContext) @@ -3781,10 +4173,7 @@ export default class OrderModuleService ) return await this.baseRepository_.serialize( - exchange, - { - populate: true, - } + exchange ) } @@ -3802,12 +4191,30 @@ export default class OrderModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateReturnReasons( idOrSelector: string | FilterableOrderReturnReasonProps, data: UpdateOrderReturnReasonDTO, @MedusaContext() sharedContext: Context = {} ): Promise { + const reasons = await this.updateReturnReasons_( + idOrSelector, + data, + sharedContext + ) + + return await this.baseRepository_.serialize< + OrderReturnReasonDTO[] | OrderReturnReasonDTO + >(isString(idOrSelector) ? reasons[0] : reasons) + } + + @InjectTransactionManager() + private async updateReturnReasons_( + idOrSelector: string | FilterableOrderReturnReasonProps, + data: UpdateOrderReturnReasonDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let normalizedInput: UpdateReturnReasonDTO[] = [] if (isString(idOrSelector)) { // Check if the return reason exists in the first place @@ -3831,82 +4238,80 @@ export default class OrderModuleService sharedContext ) - const updatedReturnReasons = await this.baseRepository_.serialize< - OrderReturnReasonDTO[] - >(reasons) - - return isString(idOrSelector) - ? updatedReturnReasons[0] - : updatedReturnReasons + return reasons } @InjectTransactionManager() async createExchange_( data: OrderTypes.CreateOrderExchangeDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.createExchange.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() async cancelExchange( data: OrderTypes.CancelOrderExchangeDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { const ret = await this.cancelExchange_(data, sharedContext) - return await this.retrieveOrderExchange(ret.id, { + const exchange = await this.retrieveOrderExchange(ret.id, { relations: ["additional_items", "return", "return.items"], }) + + return await this.baseRepository_.serialize( + exchange + ) } @InjectTransactionManager() private async cancelExchange_( data: OrderTypes.CancelOrderExchangeDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { return await BundledActions.cancelExchange.bind(this)(data, sharedContext) } @InjectTransactionManager() + @EmitEvents() async registerFulfillment( data: OrderTypes.RegisterOrderFulfillmentDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { - return await BundledActions.registerFulfillment.bind(this)( - data, - sharedContext - ) + await BundledActions.registerFulfillment.bind(this)(data, sharedContext) } @InjectTransactionManager() + @EmitEvents() async cancelFulfillment( data: OrderTypes.CancelOrderFulfillmentDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { - return await BundledActions.cancelFulfillment.bind(this)( - data, - sharedContext - ) + await BundledActions.cancelFulfillment.bind(this)(data, sharedContext) } @InjectTransactionManager() + @EmitEvents() async registerShipment( data: OrderTypes.RegisterOrderShipmentDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { - return await BundledActions.registerShipment.bind(this)(data, sharedContext) + await BundledActions.registerShipment.bind(this)(data, sharedContext) } @InjectTransactionManager() + @EmitEvents() async registerDelivery( data: OrderTypes.RegisterOrderDeliveryDTO, - @MedusaContext() sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise { - return await BundledActions.registerDelivery.bind(this)(data, sharedContext) + await BundledActions.registerDelivery.bind(this)(data, sharedContext) } @InjectManager() + @EmitEvents() // @ts-expect-error async createReturnItems( data: OrderTypes.CreateOrderReturnItemDTO, diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index 28d3fd8c72..daae87bce6 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -41,6 +41,7 @@ import { } from "@medusajs/framework/types" import { BigNumber, + EmitEvents, InjectManager, InjectTransactionManager, isPresent, @@ -182,8 +183,9 @@ export default class PaymentModuleService data: CreatePaymentCollectionDTO[], sharedContext?: Context ): Promise - @InjectManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createPaymentCollections( data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[], @@ -197,10 +199,7 @@ export default class PaymentModuleService ) return await this.baseRepository_.serialize( - Array.isArray(data) ? collections : collections[0], - { - populate: true, - } + Array.isArray(data) ? collections : collections[0] ) } @@ -226,6 +225,7 @@ export default class PaymentModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updatePaymentCollections( idOrSelector: string | FilterablePaymentCollectionProps, @@ -260,10 +260,7 @@ export default class PaymentModuleService ) return await this.baseRepository_.serialize( - Array.isArray(data) ? result : result[0], - { - populate: true, - } + Array.isArray(data) ? result : result[0] ) } @@ -285,10 +282,23 @@ export default class PaymentModuleService ): Promise @InjectManager() + @EmitEvents() async upsertPaymentCollections( data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[], @MedusaContext() sharedContext?: Context ): Promise { + const result = await this.upsertPaymentCollections_(data, sharedContext) + + return await this.baseRepository_.serialize< + PaymentCollectionDTO[] | PaymentCollectionDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager() + protected async upsertPaymentCollections_( + data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[], + @MedusaContext() sharedContext?: Context + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (collection): collection is UpdatePaymentCollectionDTO => !!collection.id @@ -309,9 +319,7 @@ export default class PaymentModuleService const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize< - PaymentCollectionDTO[] | PaymentCollectionDTO - >(Array.isArray(data) ? result : result[0]) + return result } completePaymentCollections( @@ -325,6 +333,7 @@ export default class PaymentModuleService // Should we remove this and use `updatePaymentCollections` instead? @InjectManager() + @EmitEvents() async completePaymentCollections( paymentCollectionId: string | string[], @MedusaContext() sharedContext?: Context @@ -344,12 +353,12 @@ export default class PaymentModuleService ) return await this.baseRepository_.serialize( - Array.isArray(paymentCollectionId) ? updated : updated[0], - { populate: true } + Array.isArray(paymentCollectionId) ? updated : updated[0] ) } @InjectManager() + @EmitEvents() async createPaymentSession( paymentCollectionId: string, input: CreatePaymentSessionDTO, @@ -429,6 +438,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async updatePaymentSession( data: UpdatePaymentSessionDTO, @MedusaContext() sharedContext?: Context @@ -462,10 +472,11 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize(updated, { populate: true }) + return await this.baseRepository_.serialize(updated) } @InjectManager() + @EmitEvents() async deletePaymentSession( id: string, @MedusaContext() sharedContext?: Context @@ -484,6 +495,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async authorizePaymentSession( id: string, context: Record, @@ -509,9 +521,7 @@ export default class PaymentModuleService // this method needs to be idempotent if (session.payment && session.authorized_at) { - return await this.baseRepository_.serialize(session.payment, { - populate: true, - }) + return await this.baseRepository_.serialize(session.payment) } let { data, status } = await this.paymentProviderService_.authorizePayment( @@ -565,13 +575,11 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize(payment, { - populate: true, - }) + return await this.baseRepository_.serialize(payment) } @InjectTransactionManager() - async authorizePaymentSession_( + protected async authorizePaymentSession_( session: InferEntityType, data: Record | undefined, status: PaymentSessionStatus, @@ -620,6 +628,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async updatePayment( data: UpdatePaymentDTO, @MedusaContext() sharedContext?: Context @@ -632,6 +641,7 @@ export default class PaymentModuleService // TODO: This method should return a capture, not a payment @InjectManager() + @EmitEvents() async capturePayment( data: CreateCaptureDTO, @MedusaContext() sharedContext: Context = {} @@ -680,13 +690,11 @@ export default class PaymentModuleService sharedContext ) - return await this.baseRepository_.serialize(payment, { - populate: true, - }) + return await this.baseRepository_.serialize(payment) } @InjectTransactionManager() - private async capturePayment_( + protected async capturePayment_( data: CreateCaptureDTO, payment: InferEntityType, @MedusaContext() sharedContext: Context = {} @@ -750,8 +758,9 @@ export default class PaymentModuleService return { isFullyCaptured, capture } } + @InjectManager() - private async capturePaymentFromProvider_( + protected async capturePaymentFromProvider_( payment: InferEntityType, capture: InferEntityType | undefined, isFullyCaptured: boolean, @@ -780,6 +789,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async refundPayment( data: CreateRefundDTO, @MedusaContext() sharedContext: Context = {} @@ -862,7 +872,7 @@ export default class PaymentModuleService } @InjectManager() - private async refundPaymentFromProvider_( + protected async refundPaymentFromProvider_( payment: InferEntityType, refund: InferEntityType, @MedusaContext() sharedContext: Context = {} @@ -887,6 +897,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async cancelPayment( paymentId: string, @MedusaContext() sharedContext?: Context @@ -913,7 +924,7 @@ export default class PaymentModuleService } @InjectManager() - private async maybeUpdatePaymentCollection_( + protected async maybeUpdatePaymentCollection_( paymentCollectionId: string, sharedContext?: Context ) { @@ -1050,6 +1061,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async createAccountHolder( input: CreateAccountHolderDTO, @MedusaContext() sharedContext?: Context @@ -1089,6 +1101,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async updateAccountHolder( input: UpdateAccountHolderDTO, @MedusaContext() sharedContext?: Context @@ -1128,6 +1141,7 @@ export default class PaymentModuleService } @InjectManager() + @EmitEvents() async deleteAccountHolder( id: string, @MedusaContext() sharedContext?: Context @@ -1187,7 +1201,6 @@ export default class PaymentModuleService return [normalizedResponse, paymentMethods.length] } - // @ts-ignore createPaymentMethods( data: CreatePaymentMethodDTO, sharedContext?: Context @@ -1197,7 +1210,9 @@ export default class PaymentModuleService data: CreatePaymentMethodDTO[], sharedContext?: Context ): Promise + @InjectManager() + @EmitEvents() async createPaymentMethods( data: CreatePaymentMethodDTO | CreatePaymentMethodDTO[], @MedusaContext() sharedContext?: Context 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 9b4c70efc4..360bdf2564 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 @@ -461,35 +461,41 @@ moduleIntegrationTestRunner({ ) const events = eventBusEmitSpy.mock.calls[0][0] - expect(events).toHaveLength(3) - expect(events[0]).toEqual( - composeMessage(PricingEvents.PRICE_LIST_CREATED, { - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_list", - data: { id: priceList.id }, - }) - ) - expect(events[1]).toEqual( - composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, { - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_list_rule", - data: { - id: [ - priceList.price_list_rules?.[0].id, - priceList.price_list_rules?.[1].id, - ], - }, - }) - ) - expect(events[2]).toEqual( - composeMessage(PricingEvents.PRICE_CREATED, { - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price", - data: { id: priceList.prices![0].id }, - }) + + // 4 events: 1 price list created, 2 price list rules created, 1 price created + expect(events).toHaveLength(4) + + expect(events).toEqual( + expect.arrayContaining([ + composeMessage(PricingEvents.PRICE_LIST_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list", + data: { id: priceList.id }, + }), + composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list_rule", + data: { + id: priceList.price_list_rules?.[1].id, + }, + }), + composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_list_rule", + data: { + id: priceList.price_list_rules?.[0].id, + }, + }), + composeMessage(PricingEvents.PRICE_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price", + data: { id: priceList.prices![0].id }, + }), + ]) ) }) @@ -789,7 +795,7 @@ moduleIntegrationTestRunner({ it("should update a price to a priceList successfully", async () => { const [priceSet] = await service.createPriceSets([{}]) - await service.addPriceListPrices([ + const [priceBeforeUpdate] = await service.addPriceListPrices([ { price_list_id: "price-list-1", prices: [ @@ -806,7 +812,9 @@ moduleIntegrationTestRunner({ }, ]) - await service.updatePriceListPrices([ + eventBusEmitSpy.mockClear() + + const [price] = await service.updatePriceListPrices([ { price_list_id: "price-list-1", prices: [ @@ -822,28 +830,37 @@ moduleIntegrationTestRunner({ }, ]) - const events = eventBusEmitSpy.mock.calls[2][0] + const events = eventBusEmitSpy.mock.calls[0][0] + + // 4 events: 2 price rules created, 1 price updated, 1 price rule updated, 1 price rule deleted + expect(events).toHaveLength(4) expect(events).toEqual( expect.arrayContaining([ - expect.objectContaining({ - name: "pricing.price-rule.created", - }), - expect.objectContaining({ - name: "pricing.price-rule.created", - }), - expect.objectContaining({ - name: "pricing.price.updated", - metadata: { - source: "pricing", + expect.objectContaining( + composeMessage(PricingEvents.PRICE_UPDATED, { + source: Modules.PRICING, + action: CommonEvents.UPDATED, object: "price", - action: "updated", - }, - data: { - id: "test-price-id", - }, + data: { id: price.id }, + }) + ), + composeMessage(PricingEvents.PRICE_RULE_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + data: { id: price.price_rules![0].id }, }), - expect.objectContaining({ - name: "pricing.price-rule.deleted", + composeMessage(PricingEvents.PRICE_RULE_CREATED, { + source: Modules.PRICING, + action: CommonEvents.CREATED, + object: "price_rule", + data: { id: price.price_rules![1].id }, + }), + composeMessage(PricingEvents.PRICE_RULE_DELETED, { + source: Modules.PRICING, + action: CommonEvents.DELETED, + object: "price_rule", + data: { id: priceBeforeUpdate.price_rules![0].id }, }), ]) ) diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 2d59ae2866..53fd304089 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -41,7 +41,6 @@ import { promiseAll, removeNullish, simpleHash, - upperCaseFirst, } from "@medusajs/framework/utils" import { @@ -55,7 +54,7 @@ import { import { Collection } from "@mikro-orm/core" import { ServiceTypes } from "@types" -import { eventBuilders, validatePriceListDates } from "@utils" +import { validatePriceListDates } from "@utils" import { joinerConfig } from "../joiner-config" type InjectedDependencies = { @@ -169,6 +168,8 @@ export default class PricingModuleService return pricingContext } + @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async createPriceRules( ...args: Parameters @@ -180,6 +181,8 @@ export default class PricingModuleService } } + @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async updatePriceRules( ...args: Parameters @@ -191,6 +194,8 @@ export default class PricingModuleService } } + @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async createPriceListRules( ...args: any[] @@ -203,6 +208,8 @@ export default class PricingModuleService } } + @InjectTransactionManager() + @EmitEvents() // @ts-expect-error async updatePriceListRules( ...args: any[] @@ -269,7 +276,7 @@ export default class PricingModuleService priceSet.calculated_price = calculatedPrice ?? null } - return priceSets + return await this.baseRepository_.serialize(priceSets) } @InjectManager() @@ -291,7 +298,10 @@ export default class PricingModuleService sharedContext ) if (!pricingContext || !priceSets.length) { - return [priceSets, count] + const serializedPriceSets = await this.baseRepository_.serialize< + PriceSetDTO[] + >(priceSets) + return [serializedPriceSets, count] } const calculatedPrices = await this.calculatePrices( @@ -310,7 +320,11 @@ export default class PricingModuleService priceSet.calculated_price = calculatedPrice ?? null } - return [priceSets, count] + const serializedPriceSets = await this.baseRepository_.serialize< + PriceSetDTO[] + >(priceSets) + + return [serializedPriceSets, count] } @InjectManager() @@ -405,73 +419,73 @@ export default class PricingModuleService sharedContext ) - const calculatedPrices: PricingTypes.CalculatedPriceSet[] = - pricingFilters.id - .map((priceSetId: string): PricingTypes.CalculatedPriceSet | null => { - const prices = pricesSetPricesMap.get(priceSetId) - if (!prices) { - return null - } - const { - calculatedPrice, - originalPrice, - }: { - calculatedPrice: PricingTypes.CalculatedPriceSetDTO - originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined - } = prices + const calculatedPrices: PricingTypes.CalculatedPriceSet[] = [] - return { - id: priceSetId, - is_calculated_price_price_list: !!calculatedPrice?.price_list_id, - is_calculated_price_tax_inclusive: isTaxInclusive( - priceRulesPriceMap.get(calculatedPrice.id), + for (const priceSetId of pricingFilters.id) { + const prices = pricesSetPricesMap.get(priceSetId) + if (!prices) { + continue + } + + const { + calculatedPrice, + originalPrice, + }: { + calculatedPrice: PricingTypes.CalculatedPriceSetDTO + originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined + } = prices + + const calculatedPrice_: PricingTypes.CalculatedPriceSet = { + id: priceSetId, + is_calculated_price_price_list: !!calculatedPrice?.price_list_id, + is_calculated_price_tax_inclusive: isTaxInclusive( + priceRulesPriceMap.get(calculatedPrice.id), + pricingPreferences, + calculatedPrice.currency_code!, + pricingContext.context?.region_id as string + ), + calculated_amount: isPresent(calculatedPrice?.amount) + ? parseFloat(calculatedPrice?.amount as string) + : null, + raw_calculated_amount: calculatedPrice?.raw_amount || null, + + is_original_price_price_list: !!originalPrice?.price_list_id, + is_original_price_tax_inclusive: originalPrice?.id + ? isTaxInclusive( + priceRulesPriceMap.get(originalPrice.id), pricingPreferences, - calculatedPrice.currency_code!, + originalPrice.currency_code || calculatedPrice.currency_code!, pricingContext.context?.region_id as string - ), - calculated_amount: isPresent(calculatedPrice?.amount) - ? parseFloat(calculatedPrice?.amount as string) - : null, - raw_calculated_amount: calculatedPrice?.raw_amount || null, + ) + : false, + original_amount: isPresent(originalPrice?.amount) + ? parseFloat(originalPrice?.amount as string) + : null, + raw_original_amount: originalPrice?.raw_amount || null, - is_original_price_price_list: !!originalPrice?.price_list_id, - is_original_price_tax_inclusive: originalPrice?.id - ? isTaxInclusive( - priceRulesPriceMap.get(originalPrice.id), - pricingPreferences, - originalPrice.currency_code || calculatedPrice.currency_code!, - pricingContext.context?.region_id as string - ) - : false, - original_amount: isPresent(originalPrice?.amount) - ? parseFloat(originalPrice?.amount as string) - : null, - raw_original_amount: originalPrice?.raw_amount || null, + currency_code: calculatedPrice?.currency_code || null, - currency_code: calculatedPrice?.currency_code || null, + calculated_price: { + id: calculatedPrice?.id || null, + price_list_id: calculatedPrice?.price_list_id || null, + price_list_type: calculatedPrice?.price_list_type || null, + min_quantity: parseInt(calculatedPrice?.min_quantity || "") || null, + max_quantity: parseInt(calculatedPrice?.max_quantity || "") || null, + }, - calculated_price: { - id: calculatedPrice?.id || null, - price_list_id: calculatedPrice?.price_list_id || null, - price_list_type: calculatedPrice?.price_list_type || null, - min_quantity: - parseInt(calculatedPrice?.min_quantity || "") || null, - max_quantity: - parseInt(calculatedPrice?.max_quantity || "") || null, - }, + original_price: { + id: originalPrice?.id || null, + price_list_id: originalPrice?.price_list_id || null, + price_list_type: originalPrice?.price_list_type || null, + min_quantity: parseInt(originalPrice?.min_quantity || "") || null, + max_quantity: parseInt(originalPrice?.max_quantity || "") || null, + }, + } - original_price: { - id: originalPrice?.id || null, - price_list_id: originalPrice?.price_list_id || null, - price_list_type: originalPrice?.price_list_type || null, - min_quantity: parseInt(originalPrice?.min_quantity || "") || null, - max_quantity: parseInt(originalPrice?.max_quantity || "") || null, - }, - } - }) - .filter(Boolean) as PricingTypes.CalculatedPriceSet[] + calculatedPrices.push(calculatedPrice_) + } - return JSON.parse(JSON.stringify(calculatedPrices)) + return calculatedPrices } // @ts-expect-error @@ -534,6 +548,22 @@ export default class PricingModuleService data: UpsertPriceSetDTO | UpsertPriceSetDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.upsertPriceSets_(data, sharedContext) + + try { + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0] + ) + } finally { + this.pricingRepository_.clearAvailableAttributes?.() + } + } + + @InjectTransactionManager() + protected async upsertPriceSets_( + data: UpsertPriceSetDTO | UpsertPriceSetDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( @@ -554,13 +584,7 @@ export default class PricingModuleService const result = (await promiseAll(operations)).flat() - try { - return await this.baseRepository_.serialize( - Array.isArray(data) ? result : result[0] - ) - } finally { - this.pricingRepository_.clearAvailableAttributes?.() - } + return result } // @ts-expect-error @@ -606,12 +630,13 @@ export default class PricingModuleService normalizedInput, sharedContext ) - const priceSets = await this.baseRepository_.serialize< + + const serializedUpdateResult = await this.baseRepository_.serialize< PriceSetDTO[] | PriceSetDTO - >(updateResult) + >(isString(idOrSelector) ? updateResult[0] : updateResult) try { - return isString(idOrSelector) ? priceSets[0] : priceSets + return serializedUpdateResult } finally { this.pricingRepository_.clearAvailableAttributes?.() } @@ -707,10 +732,6 @@ export default class PricingModuleService })), sharedContext ) - eventBuilders.createdPriceRule({ - data: createdPriceRules.map((r) => ({ id: r.id })), - sharedContext, - }) } if (priceRulesToUpdate.length > 0) { @@ -718,10 +739,6 @@ export default class PricingModuleService priceRulesToUpdate, sharedContext ) - eventBuilders.updatedPriceRule({ - data: updatedPriceRules.map((r) => ({ id: r.id })), - sharedContext, - }) } if (priceRulesToDelete.length > 0) { @@ -729,10 +746,6 @@ export default class PricingModuleService priceRulesToDelete, sharedContext ) - eventBuilders.deletedPriceRule({ - data: priceRulesToDelete.map((id) => ({ id })), - sharedContext, - }) } const upsertedPriceRules = [ @@ -755,10 +768,6 @@ export default class PricingModuleService priceRuleToDelete, sharedContext ) - eventBuilders.deletedPriceRule({ - data: priceRuleToDelete.map((id) => ({ id })), - sharedContext, - }) } ;(priceToUpdate as InferEntityType).rules_count = 0 @@ -780,27 +789,6 @@ export default class PricingModuleService await this.priceService_.delete(pricesToDelete, sharedContext) } - if (createdPrices.length > 0) { - eventBuilders.createdPrice({ - data: createdPrices.map((p) => ({ id: p.id })), - sharedContext, - }) - } - - if (updatedPrices.length > 0) { - eventBuilders.updatedPrice({ - data: updatedPrices.map((p) => ({ id: p.id })), - sharedContext, - }) - } - - if (pricesToDelete.length > 0) { - eventBuilders.deletedPrice({ - data: pricesToDelete.map((id) => ({ id })), - sharedContext, - }) - } - const priceSets = await this.priceSetService_.list( { id: normalizedData.map(({ id }) => id) }, { @@ -825,11 +813,6 @@ export default class PricingModuleService ps.prices = upsertedPricesMap.get(ps.id) || [] }) - eventBuilders.updatedPriceSet({ - data: priceSets.map((ps) => ({ id: ps.id })), - sharedContext, - }) - return priceSets } @@ -952,8 +935,12 @@ export default class PricingModuleService return dbPrices.find((p) => p.id === inputItem.priceSetId)! }) + const serializedOrderedPriceSets = await this.baseRepository_.serialize< + PricingTypes.PriceSetDTO[] + >(Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0]) + try { - return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0] + return serializedOrderedPriceSets } finally { this.pricingRepository_.clearAvailableAttributes?.() } @@ -977,7 +964,7 @@ export default class PricingModuleService } } - @InjectTransactionManager() + @InjectManager() @EmitEvents() // @ts-ignore async updatePriceLists( @@ -1117,6 +1104,18 @@ export default class PricingModuleService data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.upsertPricePreferences_(data, sharedContext) + + return await this.baseRepository_.serialize< + PricePreferenceDTO[] | PricePreferenceDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager() + protected async upsertPricePreferences_( + data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( ( @@ -1139,9 +1138,8 @@ export default class PricingModuleService } const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize< - PricePreferenceDTO[] | PricePreferenceDTO - >(Array.isArray(data) ? result : result[0]) + + return result } // @ts-expect-error @@ -1158,6 +1156,7 @@ export default class PricingModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updatePricePreferences( idOrSelector: string | PricingTypes.FilterablePricePreferenceProps, @@ -1248,49 +1247,6 @@ export default class PricingModuleService sharedContext ) - const eventsData = priceSets.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, - }) - return priceSets } @@ -1336,18 +1292,11 @@ export default class PricingModuleService } }) - const { entities, performedActions } = - await this.priceService_.upsertWithReplace( - pricesToUpsert, - { relations: ["price_rules"] }, - sharedContext - ) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + const { entities } = await this.priceService_.upsertWithReplace( + pricesToUpsert, + { relations: ["price_rules"] }, + sharedContext + ) return entities } @@ -1401,67 +1350,6 @@ export default class PricingModuleService 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 } @@ -1519,16 +1407,12 @@ export default class PricingModuleService } ) - const { entities, performedActions } = - await this.priceListService_.upsertWithReplace(normalizedData, { + const { entities } = await this.priceListService_.upsertWithReplace( + normalizedData, + { relations: ["price_list_rules"], - }) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + } + ) return entities } @@ -1572,18 +1456,11 @@ export default class PricingModuleService } } - const { entities, performedActions } = - await this.priceService_.upsertWithReplace( - pricesToUpsert, - { relations: ["price_rules"] }, - sharedContext - ) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + const { entities } = await this.priceService_.upsertWithReplace( + pricesToUpsert, + { relations: ["price_rules"] }, + sharedContext + ) return entities } @@ -1634,18 +1511,11 @@ export default class PricingModuleService } }) - const { entities, performedActions } = - await this.priceService_.upsertWithReplace( - pricesToUpsert, - { relations: ["price_rules"] }, - sharedContext - ) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + const { entities } = await this.priceService_.upsertWithReplace( + pricesToUpsert, + { relations: ["price_rules"] }, + sharedContext + ) return entities } @@ -1705,18 +1575,11 @@ export default class PricingModuleService }) .filter(Boolean) - const { entities, performedActions } = - await this.priceListService_.upsertWithReplace( - priceListsUpsert, - { relations: ["price_list_rules"] }, - sharedContext - ) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + const { entities } = await this.priceListService_.upsertWithReplace( + priceListsUpsert, + { relations: ["price_list_rules"] }, + sharedContext + ) return entities } @@ -1779,18 +1642,11 @@ export default class PricingModuleService }) .filter(Boolean) - const { entities, performedActions } = - await this.priceListService_.upsertWithReplace( - priceListsUpsert, - { relations: ["price_list_rules"] }, - sharedContext - ) - - composeAllEvents({ - eventBuilders, - performedActions, - sharedContext, - }) + const { entities } = await this.priceListService_.upsertWithReplace( + priceListsUpsert, + { relations: ["price_list_rules"] }, + sharedContext + ) return entities } @@ -1829,26 +1685,6 @@ export default class PricingModuleService } } -const composeAllEvents = ({ - eventBuilders, - performedActions, - sharedContext, -}) => { - for (const action of Object.keys(performedActions)) { - for (const entity of Object.keys(performedActions[action])) { - const eventName = action + upperCaseFirst(entity) - if (!eventBuilders[eventName]) { - continue - } - - eventBuilders[eventName]({ - data: performedActions[action][entity] ?? [], - sharedContext, - }) - } - } -} - const isTaxInclusive = ( priceRules: InferEntityType[], preferences: InferEntityType[], diff --git a/packages/modules/pricing/src/utils/events.ts b/packages/modules/pricing/src/utils/events.ts deleted file mode 100644 index 3d9a8b398d..0000000000 --- a/packages/modules/pricing/src/utils/events.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - CommonEvents, - moduleEventBuilderFactory, - Modules, - PricingEvents, -} from "@medusajs/framework/utils" - -export const eventBuilders = { - createdPriceSet: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_set", - eventName: PricingEvents.PRICE_SET_CREATED, - }), - createdPrice: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price", - eventName: PricingEvents.PRICE_CREATED, - }), - createdPriceRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_rule", - eventName: PricingEvents.PRICE_RULE_CREATED, - }), - createdPriceList: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_list", - eventName: PricingEvents.PRICE_LIST_CREATED, - }), - createdPriceListRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.CREATED, - object: "price_list_rule", - eventName: PricingEvents.PRICE_LIST_RULE_CREATED, - }), - updatedPriceSet: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.UPDATED, - object: "price_set", - eventName: PricingEvents.PRICE_SET_UPDATED, - }), - updatedPrice: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.UPDATED, - object: "price", - eventName: PricingEvents.PRICE_UPDATED, - }), - updatedPriceList: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.UPDATED, - object: "price_list", - eventName: PricingEvents.PRICE_LIST_UPDATED, - }), - updatedPriceListRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.UPDATED, - object: "price_list_rule", - eventName: PricingEvents.PRICE_LIST_RULE_UPDATED, - }), - updatedPriceRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.UPDATED, - object: "price_rule", - eventName: PricingEvents.PRICE_RULE_UPDATED, - }), - deletedPriceSet: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.DELETED, - object: "price_set", - eventName: PricingEvents.PRICE_SET_DELETED, - }), - deletedPrice: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.DELETED, - object: "price", - eventName: PricingEvents.PRICE_DELETED, - }), - deletedPriceList: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.DELETED, - object: "price_list", - eventName: PricingEvents.PRICE_LIST_DELETED, - }), - deletedPriceListRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.DELETED, - object: "price_list_rule", - eventName: PricingEvents.PRICE_LIST_RULE_DELETED, - }), - deletedPriceRule: moduleEventBuilderFactory({ - source: Modules.PRICING, - action: CommonEvents.DELETED, - object: "price_rule", - eventName: PricingEvents.PRICE_RULE_DELETED, - }), -} diff --git a/packages/modules/pricing/src/utils/index.ts b/packages/modules/pricing/src/utils/index.ts index 0bdeeb988b..5c60adb105 100644 --- a/packages/modules/pricing/src/utils/index.ts +++ b/packages/modules/pricing/src/utils/index.ts @@ -1,2 +1 @@ export * from "./validate-price-list-dates" -export * from "./events" diff --git a/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts b/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts index 7dc77b1908..8fb174a1f4 100644 --- a/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts +++ b/packages/modules/product/integration-tests/__fixtures__/product/data/create-product.ts @@ -79,7 +79,10 @@ export const buildProductAndRelationsData = ({ title: faker.commerce.productName(), sku: faker.commerce.productName(), options: options - ? { [options[0].title]: options[0].values[0] } + ? options.reduce((acc, option) => { + acc[option.title] = option.values[0] + return acc + }, {} as Record) : { [defaultOptionTitle]: defaultOptionValue, }, diff --git a/packages/modules/product/integration-tests/__tests__/product-category.ts b/packages/modules/product/integration-tests/__tests__/product-category.spec.ts similarity index 100% rename from packages/modules/product/integration-tests/__tests__/product-category.ts rename to packages/modules/product/integration-tests/__tests__/product-category.spec.ts diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/events.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/events.spec.ts new file mode 100644 index 0000000000..9c01a2cfc2 --- /dev/null +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/events.spec.ts @@ -0,0 +1,1150 @@ +import { + InferEntityType, + IProductModuleService, +} from "@medusajs/framework/types" +import { + CommonEvents, + composeMessage, + Modules, + ProductEvents, +} from "@medusajs/framework/utils" +import { + MockEventBusService, + moduleIntegrationTestRunner, +} from "@medusajs/test-utils" +import { ProductOption } from "../../../src/models" +import { buildProductAndRelationsData } from "../../__fixtures__/product" + +jest.setTimeout(300000) + +moduleIntegrationTestRunner({ + moduleName: Modules.PRODUCT, + injectedDependencies: { + [Modules.EVENT_BUS]: new MockEventBusService(), + }, + testSuite: ({ MikroOrmWrapper, service }) => { + let eventBusSpy: jest.SpyInstance + + beforeEach(() => { + eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("ProductModuleService Events", () => { + describe("Product Creation", () => { + it("should emit all related events when creating a product with full relations", async () => { + const productData = buildProductAndRelationsData({ + title: "Test Product", + images: [{ url: "image-1.jpg" }, { url: "image-2.jpg" }], + thumbnail: "image-1.jpg", + options: [ + { + title: "size", + values: ["small", "medium", "large"], + }, + { + title: "color", + values: ["red", "blue"], + }, + ], + variants: [ + { + title: "Small Red", + sku: "small-red", + options: { size: "small", color: "red" }, + }, + { + title: "Medium Blue", + sku: "medium-blue", + options: { size: "medium", color: "blue" }, + }, + ], + }) + + const products = await service.createProducts([productData]) + const createdProduct = products[0] + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Should emit events for: + // 1. Product created + // 2. Product options created (2 options) + // 3. Product option values created (5 values total: 3 sizes + 2 colors) + // 4. Product variants created (2 variants) + // 5. Product images created (2 images) + + const expectedEventsCount = 1 + 2 + 5 + 2 + 2 // 12 total events + expect(emittedEvents).toHaveLength(expectedEventsCount) + + // Verify product created event + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_CREATED, { + data: { id: createdProduct.id }, + object: "product", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + + // Verify product option created events + createdProduct.options.forEach((option) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_CREATED, { + data: { id: option.id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + + // Verify option value created events for each option + option.values.forEach((value) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_CREATED, { + data: { id: value.id }, + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + // Verify product variant created events + createdProduct.variants.forEach((variant) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_VARIANT_CREATED, { + data: { id: variant.id }, + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + + // Verify product image created events + createdProduct.images.forEach((image) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_IMAGE_CREATED, { + data: { id: image.id }, + object: "product_image", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + }) + + describe("Product Update", () => { + let existingProduct: any + + beforeEach(async () => { + const productData = buildProductAndRelationsData({ + title: "Original Product", + images: [{ url: "original-image.jpg" }], + options: [ + { + title: "existing-option", + values: ["value-1"], + }, + { + title: "existing-option-2", + values: ["value-1"], + }, + ], + variants: [ + { + title: "existing-variant", + options: { + "existing-option": "value-1", + "existing-option-2": "value-1", + }, + }, + ], + }) + const products = await service.createProducts([productData]) + existingProduct = products[0] + eventBusSpy.mockClear() + }) + + it("should emit cascade events when updating product with relations", async () => { + const existingOption = existingProduct.options.find( + (option: any) => option.title === "existing-option" + )! as InferEntityType + const existingVariant = existingProduct.variants[0] + const expectedDeletedOption = existingProduct.options.find( + (option: any) => option.title === "existing-option-2" + )! + const expectedDeletedImage = existingProduct.images[0] + + const updateData = { + id: existingProduct.id, + title: "Updated Product", + images: [{ url: "new-image-1.jpg" }, { url: "new-image-2.jpg" }], + options: [ + { + title: "new-size-option", + values: ["small", "large"], + }, + { + id: existingOption.id, + title: "updated-existing-option", + values: ["value-1"], + }, + ], + variants: [ + { + id: existingVariant.id, + title: "updated-existing-variant", + options: { + "new-size-option": "small", + "updated-existing-option": "value-1", + }, + }, + { + title: "New Variant", + options: { + "new-size-option": "large", + "updated-existing-option": "value-1", + }, + }, + ], + } + + await service.updateProducts(existingProduct.id, updateData) + const updatedProduct = await service.retrieveProduct( + existingProduct.id, + { + relations: [ + "options", + "options.values", + "variants", + "images", + "tags", + ], + } + ) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Total count should include: 1 product update + 1 option created + 2 option values created + 1 option update + 1 option deleted + 1 option value deleted + 1 variant created + 1 variant updated + 2 images created + 1 image deleted = 12 events + expect(emittedEvents).toHaveLength(12) + + // Should emit product update event + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_UPDATED, { + data: { id: existingProduct.id }, + object: "product", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ]) + ) + + // Should emit option created event for new option + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_CREATED, { + data: expect.objectContaining({ id: expect.any(String) }), + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + + // Should emit option value created events for new option values + const newOptionValues = updatedProduct.options.find( + (option) => option.title === "new-size-option" + )!.values + + newOptionValues.forEach((value) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_CREATED, { + data: expect.objectContaining({ id: value.id }), + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + + // should emit option updated event for updated option + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_UPDATED, { + data: expect.objectContaining({ id: existingOption.id }), + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ]) + ) + + // Should emit option deleted event for deleted option + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_DELETED, { + data: expect.objectContaining({ id: expectedDeletedOption.id }), + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + + // Should emit option value event for deleted option value + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_DELETED, { + data: expect.objectContaining({ + id: expectedDeletedOption.values[0].id, + }), + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + + // Should emit variant created event for new variant + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_VARIANT_CREATED, { + data: expect.objectContaining({ id: expect.any(String) }), + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + + // Should emit variant updated event for updated variant + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_VARIANT_UPDATED, { + data: expect.objectContaining({ id: existingVariant.id }), + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ]) + ) + + // Should emit image created events for new images + updatedProduct.images.forEach((image) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_IMAGE_CREATED, { + data: expect.objectContaining({ id: expect.any(String) }), + object: "product_image", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + + // Should emit image deleted events for deleted images + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_IMAGE_DELETED, { + data: expect.objectContaining({ id: expectedDeletedImage.id }), + object: "product_image", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + }) + }) + + describe("Product Deletion", () => { + it("should emit all cascade delete events when soft deleting a product", async () => { + const productData = buildProductAndRelationsData({ + title: "Product to Delete", + images: [ + { url: "delete-image-1.jpg" }, + { url: "delete-image-2.jpg" }, + ], + options: [ + { + title: "delete-option", + values: ["delete-value-1", "delete-value-2"], + }, + ], + variants: [ + { + title: "Delete Variant", + options: { "delete-option": "delete-value-1" }, + }, + ], + }) + + const products = await service.createProducts([productData]) + const createdProduct = products[0] + eventBusSpy.mockClear() + + await service.softDeleteProducts([createdProduct.id]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Total count should include: 1 product deleted + 1 variant deleted + 1 option deleted + 2 option values deleted + 2 images deleted = 7 events + expect(emittedEvents).toHaveLength(7) + + // Should emit delete events for product and all its relations + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_DELETED, { + data: { id: createdProduct.id }, + object: "product", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + + // Should emit delete events for variants + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_VARIANT_DELETED, { + data: { id: createdProduct.variants[0].id }, + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + + // Should emit delete events for options + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_DELETED, { + data: { id: createdProduct.options[0].id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + + // Should emit delete events for option values + createdProduct.options[0].values.forEach((value) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_DELETED, { + data: { + id: value.id, + }, + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + }) + + // Should emit delete events for images + createdProduct.images.forEach((image) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_IMAGE_DELETED, { + data: { + id: image.id, + }, + object: "product_image", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + }) + }) + }) + + describe("Product Variant Operations", () => { + let productWithOptions: any + + beforeEach(async () => { + const productData = buildProductAndRelationsData({ + options: [ + { + title: "size", + values: ["small", "medium", "large"], + }, + { + title: "color", + values: ["red", "blue", "green"], + }, + ], + }) + const products = await service.createProducts([productData]) + productWithOptions = products[0] + eventBusSpy.mockClear() + }) + + it("should emit PRODUCT_VARIANT_CREATED event only when creating standalone variant", async () => { + const variantData = { + title: "New Standalone Variant", + product_id: productWithOptions.id, + options: { size: "large", color: "green" }, + } + + const variants = await service.createProductVariants([variantData]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_VARIANT_CREATED, { + data: { id: variants[0].id }, + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit PRODUCT_VARIANT_UPDATED event only when updating variant", async () => { + const variant = productWithOptions.variants[0] + + await service.updateProductVariants(variant.id, { + title: "Updated Variant Title", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_VARIANT_UPDATED, { + data: { id: variant.id }, + object: "product_variant", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + }) + + describe("Product Tag Operations", () => { + it("should emit PRODUCT_TAG_CREATED event on createProductTags", async () => { + const tagData = { value: "New Tag" } + + const tags = await service.createProductTags([tagData]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_TAG_CREATED, { + data: { id: tags[0].id }, + object: "product_tag", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit PRODUCT_TAG_UPDATED event on updateProductTags", async () => { + const tags = await service.createProductTags([ + { value: "Original Tag" }, + ]) + eventBusSpy.mockClear() + + await service.updateProductTags(tags[0].id, { value: "Updated Tag" }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_TAG_UPDATED, { + data: { id: tags[0].id }, + object: "product_tag", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit appropriate events on upsertProductTags", async () => { + const existingTag = await service.createProductTags([ + { value: "Existing Tag" }, + ]) + eventBusSpy.mockClear() + + const tags = await service.upsertProductTags([ + { id: existingTag[0].id, value: "Updated Existing Tag" }, + { value: "New Tag" }, + ]) + + const updatedTag = tags.find((tag) => tag.id === existingTag[0].id)! + const createdTag = tags.find((tag) => tag.id !== updatedTag.id)! + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Total count should include: 1 tag updated + 1 tag created = 2 events + expect(emittedEvents).toHaveLength(2) + + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_TAG_UPDATED, { + data: { id: updatedTag.id }, + object: "product_tag", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + composeMessage(ProductEvents.PRODUCT_TAG_CREATED, { + data: { id: createdTag.id }, + object: "product_tag", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + describe("Product Type Operations", () => { + it("should emit PRODUCT_TYPE_CREATED event on createProductTypes", async () => { + const typeData = { value: "New Type" } + + const types = await service.createProductTypes([typeData]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_TYPE_CREATED, { + data: { id: types[0].id }, + object: "product_type", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit PRODUCT_TYPE_UPDATED event on updateProductTypes", async () => { + const types = await service.createProductTypes([ + { value: "Original Type" }, + ]) + eventBusSpy.mockClear() + + await service.updateProductTypes(types[0].id, { + value: "Updated Type", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_TYPE_UPDATED, { + data: { id: types[0].id }, + object: "product_type", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit appropriate events on upsertProductTypes", async () => { + const existingType = await service.createProductTypes([ + { value: "Existing Type" }, + ]) + eventBusSpy.mockClear() + + const types = await service.upsertProductTypes([ + { id: existingType[0].id, value: "Updated Existing Type" }, + { value: "New Type" }, + ]) + + const updatedType = types.find( + (type) => type.id === existingType[0].id + )! + const createdType = types.find((type) => type.id !== updatedType.id)! + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Total count should include: 1 type updated + 1 type created = 2 events + expect(emittedEvents).toHaveLength(2) + + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_TYPE_UPDATED, { + data: { id: updatedType.id }, + object: "product_type", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + composeMessage(ProductEvents.PRODUCT_TYPE_CREATED, { + data: { id: createdType.id }, + object: "product_type", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + describe("Product Option Operations", () => { + let productWithOptions: any + + beforeEach(async () => { + const productData = buildProductAndRelationsData({ + title: "Product with Options", + options: [ + { title: "Size", values: ["small", "medium", "large"] }, + { title: "Color", values: ["red", "blue", "green"] }, + ], + }) + const products = await service.createProducts([productData]) + productWithOptions = products[0] + eventBusSpy.mockClear() + }) + + it("should emit PRODUCT_OPTION_CREATED event on createProductOptions", async () => { + const optionData = { + title: "New Option", + product_id: productWithOptions.id, + values: ["value1", "value2", "value3"], + } + + const options = await service.createProductOptions([optionData]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + // Total count should include: 1 option created + 3 option values created = 4 events + expect(emittedEvents).toHaveLength(4) + + // Should emit 1 option created event + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_CREATED, { + data: { id: options[0].id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + + // Should emit 3 option values created events + options[0].values.forEach((value) => { + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_CREATED, { + data: { id: value.id }, + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + it("should emit PRODUCT_OPTION_UPDATED event on updateProductOptions", async () => { + const option = productWithOptions.options[0] + + await service.updateProductOptions(option.id, { + title: "Updated Option", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_OPTION_UPDATED, { + data: { id: option.id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit appropriate events on upsertProductOptions", async () => { + const existingOption = productWithOptions.options[0] + const newOptionData = { + title: "New Option", + product_id: productWithOptions.id, + values: ["new1", "new2"], + } + + const options = await service.upsertProductOptions([ + { id: existingOption.id, title: "Updated Option" }, + newOptionData, + ]) + + const updatedOption = options.find( + (option) => option.id === existingOption.id + )! + const createdOption = options.find( + (option) => option.id !== updatedOption.id + )! + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_OPTION_UPDATED, { + data: { id: updatedOption.id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + composeMessage(ProductEvents.PRODUCT_OPTION_CREATED, { + data: { id: createdOption.id }, + object: "product_option", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + describe("Product Option Value Operations", () => { + let productWithOptions: any + + beforeEach(async () => { + const productData = buildProductAndRelationsData({ + options: [ + { title: "Size", values: ["small", "medium", "large"] }, + { title: "Color", values: ["red", "blue", "green"] }, + ], + }) + const products = await service.createProducts([productData]) + productWithOptions = products[0] + eventBusSpy.mockClear() + }) + + it("should emit PRODUCT_OPTION_VALUE_UPDATED event on updateProductOptionValues", async () => { + const optionValue = productWithOptions.options[0].values[0] + + await service.updateProductOptionValues(optionValue.id, { + value: "Updated Value", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_UPDATED, { + data: { id: optionValue.id }, + object: "product_option_value", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + }) + + describe("Product Collection Operations", () => { + it("should emit PRODUCT_COLLECTION_CREATED event on createProductCollections", async () => { + const collectionData = { title: "New Collection" } + + const collections = await service.createProductCollections([ + collectionData, + ]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_COLLECTION_CREATED, { + data: { id: collections[0].id }, + object: "product_collection", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit PRODUCT_COLLECTION_UPDATED event on updateProductCollections", async () => { + const collections = await service.createProductCollections([ + { title: "Original Collection" }, + ]) + eventBusSpy.mockClear() + + await service.updateProductCollections(collections[0].id, { + title: "Updated Collection", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_COLLECTION_UPDATED, { + data: { id: collections[0].id }, + object: "product_collection", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit appropriate events on upsertProductCollections", async () => { + const existingCollection = await service.createProductCollections([ + { title: "Existing Collection" }, + ]) + eventBusSpy.mockClear() + + const collections = await service.upsertProductCollections([ + { + id: existingCollection[0].id, + title: "Updated Existing Collection", + }, + { title: "New Collection" }, + ]) + + const updatedCollection = collections.find( + (collection) => collection.id === existingCollection[0].id + )! + const createdCollection = collections.find( + (collection) => collection.id !== updatedCollection.id + )! + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_COLLECTION_UPDATED, { + data: { id: updatedCollection.id }, + object: "product_collection", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + composeMessage(ProductEvents.PRODUCT_COLLECTION_CREATED, { + data: { id: createdCollection.id }, + object: "product_collection", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + describe("Product Category Operations", () => { + it("should emit PRODUCT_CATEGORY_CREATED event on createProductCategories", async () => { + const categoryData = { name: "New Category" } + + const categories = await service.createProductCategories([ + categoryData, + ]) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_CATEGORY_CREATED, { + data: { id: categories[0].id }, + object: "product_category", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit PRODUCT_CATEGORY_UPDATED event on updateProductCategories", async () => { + const categories = await service.createProductCategories([ + { name: "Original Category" }, + ]) + eventBusSpy.mockClear() + + await service.updateProductCategories(categories[0].id, { + name: "Updated Category", + }) + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + expect(eventBusSpy).toHaveBeenCalledWith( + [ + composeMessage(ProductEvents.PRODUCT_CATEGORY_UPDATED, { + data: { id: categories[0].id }, + object: "product_category", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + ], + { + internal: true, + } + ) + }) + + it("should emit appropriate events on upsertProductCategories", async () => { + const existingCategory = await service.createProductCategories([ + { name: "Existing Category" }, + ]) + eventBusSpy.mockClear() + + const categories = await service.upsertProductCategories([ + { id: existingCategory[0].id, name: "Updated Existing Category" }, + { name: "New Category" }, + ]) + + const updatedCategory = categories.find( + (category) => category.id === existingCategory[0].id + )! + const createdCategory = categories.find( + (category) => category.id !== updatedCategory.id + )! + + expect(eventBusSpy).toHaveBeenCalledTimes(1) + const emittedEvents = eventBusSpy.mock.calls[0][0] + + expect(emittedEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_CATEGORY_UPDATED, { + data: { id: updatedCategory.id }, + object: "product_category", + source: Modules.PRODUCT, + action: CommonEvents.UPDATED, + }), + composeMessage(ProductEvents.PRODUCT_CATEGORY_CREATED, { + data: { id: createdCategory.id }, + object: "product_category", + source: Modules.PRODUCT, + action: CommonEvents.CREATED, + }), + ]) + ) + }) + }) + + describe("Delete Operations - Base Service Automatic Events", () => { + it("should emit delete events for all entity types via base service", async () => { + // Create entities + const products = await service.createProducts([ + buildProductAndRelationsData({ + options: [ + { title: "Size", values: ["small", "medium", "large"] }, + { title: "Color", values: ["red", "blue", "green"] }, + ], + }), + ]) + const tags = await service.createProductTags([{ value: "Test Tag" }]) + const types = await service.createProductTypes([ + { value: "Test Type" }, + ]) + const categories = await service.createProductCategories([ + { name: "Test Category" }, + ]) + const collections = await service.createProductCollections([ + { title: "Test Collection" }, + ]) + + eventBusSpy.mockClear() + + // Test delete operations - these are handled automatically by base service + await service.deleteProducts([products[0].id]) + await service.deleteProductTags([tags[0].id]) + await service.deleteProductTypes([types[0].id]) + await service.deleteProductCategories([categories[0].id]) + await service.deleteProductCollections([collections[0].id]) + + // Each delete should emit the appropriate delete event + expect(eventBusSpy).toHaveBeenCalledTimes(5) + + // Verify each delete event was emitted + const allCalls = eventBusSpy.mock.calls + const allEvents = allCalls.flat(2) + + expect(allEvents).toEqual( + expect.arrayContaining([ + composeMessage(ProductEvents.PRODUCT_DELETED, { + data: { id: products[0].id }, + object: "product", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + composeMessage(ProductEvents.PRODUCT_TAG_DELETED, { + data: { id: tags[0].id }, + object: "product_tag", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + composeMessage(ProductEvents.PRODUCT_TYPE_DELETED, { + data: { id: types[0].id }, + object: "product_type", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + composeMessage(ProductEvents.PRODUCT_CATEGORY_DELETED, { + data: { id: categories[0].id }, + object: "product_category", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + composeMessage(ProductEvents.PRODUCT_COLLECTION_DELETED, { + data: { id: collections[0].id }, + object: "product_collection", + source: Modules.PRODUCT, + action: CommonEvents.DELETED, + }), + ]) + ) + }) + }) + }) + }, +}) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts index d3016d4734..824097991d 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-categories.spec.ts @@ -1,15 +1,11 @@ import { IProductModuleService } from "@medusajs/framework/types" import { - CommonEvents, - composeMessage, Modules, - ProductEvents, ProductStatus, toMikroORMEntity, } from "@medusajs/framework/utils" import { Product, ProductCategory } from "@models" import { - MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" import { productCategoriesRankData } from "../../__fixtures__/product-category/data" @@ -18,9 +14,6 @@ jest.setTimeout(30000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, - injectedDependencies: { - [Modules.EVENT_BUS]: new MockEventBusService(), - }, testSuite: ({ MikroOrmWrapper, service }) => { describe("ProductModuleService product categories", () => { let productOne: Product @@ -404,29 +397,6 @@ moduleIntegrationTestRunner({ ) }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - - const category = await service.createProductCategories({ - name: "New Category", - parent_category_id: productCategoryOne.id, - }) - - expect(eventBusSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_CATEGORY_CREATED, { - data: { id: category.id }, - object: "product_category", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) - }) it("should append rank from an existing category depending on parent", async () => { await service.createProductCategories({ @@ -504,29 +474,6 @@ moduleIntegrationTestRunner({ productCategoryZeroTwo = categories[5] }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - eventBusSpy.mockClear() - - await service.updateProductCategories(productCategoryZero.id, { - name: "New Category", - }) - - expect(eventBusSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_CATEGORY_UPDATED, { - data: { id: productCategoryZero.id }, - object: "product_category", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) - }) it("should update the name of the category successfully", async () => { await service.updateProductCategories(productCategoryZero.id, { @@ -683,30 +630,6 @@ moduleIntegrationTestRunner({ productCategoryTwo = categories[2] }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - eventBusSpy.mockClear() - - await service.deleteProductCategories([productCategoryOne.id]) - - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - expect.objectContaining({ - data: { id: productCategoryOne.id }, - name: "product.product-category.deleted", - metadata: { - action: CommonEvents.DELETED, - object: "product_category", - source: Modules.PRODUCT, - }, - }), - ], - { - internal: true, - } - ) - }) it("should throw an error when an id does not exist", async () => { let error diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts index b27fa6adf4..b02c5bba05 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-collections.spec.ts @@ -1,14 +1,10 @@ import { IProductModuleService } from "@medusajs/framework/types" import { - CommonEvents, - composeMessage, Modules, - ProductEvents, ProductStatus, toMikroORMEntity, } from "@medusajs/framework/utils" import { - MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" import { Product, ProductCollection } from "@models" @@ -18,9 +14,6 @@ jest.setTimeout(30000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, - injectedDependencies: { - [Modules.EVENT_BUS]: new MockEventBusService(), - }, testSuite: ({ MikroOrmWrapper, service }) => { describe("ProductModuleService product collections", () => { let productOne: Product @@ -284,59 +277,11 @@ moduleIntegrationTestRunner({ expect(collections).toHaveLength(0) }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - await service.deleteProductCollections([collectionId]) - - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - { - name: "product.product-collection.deleted", - data: { id: collectionId }, - metadata: { - action: CommonEvents.DELETED, - object: "product_collection", - source: Modules.PRODUCT, - }, - }, - ], - { - internal: true, - } - ) - }) }) describe("updateCollections", () => { const collectionId = "test-1" - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - - await service.upsertProductCollections([ - { - id: collectionId, - title: "New Collection", - product_ids: ["product_id"], - }, - ]) - - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_COLLECTION_UPDATED, { - data: { id: collectionId }, - object: "product_collection", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) - }) it("should update the value of the collection successfully", async () => { await service.upsertProductCollections([ @@ -534,28 +479,6 @@ moduleIntegrationTestRunner({ ) }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - - const collections = await service.createProductCollections([ - { title: "New Collection" }, - ]) - - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_COLLECTION_CREATED, { - data: { id: collections[0].id }, - object: "product_collection", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) - }) }) }) }, diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts index 56b046c336..5f479cbe3c 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-tags.spec.ts @@ -1,15 +1,11 @@ import { IProductModuleService } from "@medusajs/framework/types" import { - CommonEvents, - composeMessage, Modules, - ProductEvents, ProductStatus, toMikroORMEntity, } from "@medusajs/framework/utils" import { Product, ProductTag } from "@models" import { - MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" @@ -18,15 +14,6 @@ jest.setTimeout(30000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, testSuite: ({ MikroOrmWrapper, service }) => { - let eventBusEmitSpy - - beforeEach(() => { - eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") - }) - - afterEach(() => { - jest.clearAllMocks() - }) describe("ProductModuleService product tags", () => { let tagOne: ProductTag @@ -303,20 +290,6 @@ moduleIntegrationTestRunner({ expect(productTag.value).toEqual("UK") - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_TAG_UPDATED, { - data: { id: productTag.id }, - object: "product_tag", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) }) it("should throw an error when an id does not exist", async () => { @@ -350,20 +323,6 @@ moduleIntegrationTestRunner({ expect(productTag[0]?.value).toEqual("UK") - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_TAG_CREATED, { - data: { id: productTag[0].id }, - object: "product_tag", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) }) }) @@ -409,26 +368,6 @@ moduleIntegrationTestRunner({ const newTag = productTags.find((t) => t.value === "new")! const updatedTag = productTags.find((t) => t.value === "updated")! - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_TAG_CREATED, { - data: { id: newTag.id }, - object: "product_tag", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - composeMessage(ProductEvents.PRODUCT_TAG_UPDATED, { - data: { id: updatedTag.id }, - object: "product_tag", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) }) }) }) diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/product-variants.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/product-variants.spec.ts index 5929b8f607..900f825cab 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/product-variants.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/product-variants.spec.ts @@ -7,15 +7,11 @@ import { UpdateProductVariantDTO, } from "@medusajs/framework/types" import { - CommonEvents, - composeMessage, Modules, - ProductEvents, ProductStatus, } from "@medusajs/framework/utils" import { - MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" @@ -24,15 +20,6 @@ jest.setTimeout(30000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, testSuite: ({ service }) => { - let eventBusEmitSpy - - beforeEach(() => { - eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") - }) - - afterEach(() => { - jest.clearAllMocks() - }) describe("ProductModuleService product variants", () => { let variantOne: ProductVariantDTO @@ -212,20 +199,6 @@ moduleIntegrationTestRunner({ ) expect(productVariant.title).toEqual("new test") - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_VARIANT_UPDATED, { - data: { id: variantOne.id }, - object: "product_variant", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) }) it("should do a partial update on the options of a variant successfully", async () => { @@ -294,20 +267,6 @@ moduleIntegrationTestRunner({ }) ) - expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) - expect(eventBusEmitSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_VARIANT_CREATED, { - data: { id: variant.id }, - object: "product_variant", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) }) it("should correctly associate variants with own product options", async () => { diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts index ed05dd058d..1ba6506bdb 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts @@ -4,11 +4,8 @@ import { ProductTagDTO, } from "@medusajs/framework/types" import { - CommonEvents, - composeMessage, kebabCase, Modules, - ProductEvents, ProductStatus, } from "@medusajs/framework/utils" import { @@ -20,7 +17,6 @@ import { } from "@models" import { - MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" import { UpdateProductInput } from "@types" @@ -34,9 +30,6 @@ jest.setTimeout(300000) moduleIntegrationTestRunner({ moduleName: Modules.PRODUCT, - injectedDependencies: { - [Modules.EVENT_BUS]: new MockEventBusService(), - }, testSuite: ({ MikroOrmWrapper, service }) => { describe("ProductModuleService products", function () { let productCollectionOne: ProductCollection @@ -573,37 +566,6 @@ moduleIntegrationTestRunner({ ) }) - it("should emit events through event bus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - const data = buildProductAndRelationsData({ - images, - thumbnail: images[0].url, - }) - - const updateData = { - ...data, - options: data.options, - id: productOne.id, - title: "updated title", - } - - await service.upsertProducts([updateData]) - - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_UPDATED, { - data: { id: productOne.id }, - object: "product", - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - }), - ], - { - internal: true, - } - ) - }) it("should add relationships to a product", async () => { const updateData = { @@ -1086,29 +1048,6 @@ moduleIntegrationTestRunner({ ) }) - it("should emit events through eventBus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - const data = buildProductAndRelationsData({ - images, - thumbnail: images[0].url, - }) - - const products = await service.createProducts([data]) - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ - composeMessage(ProductEvents.PRODUCT_CREATED, { - data: { id: products[0].id }, - object: "product", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) - }) it("should throw because variant doesn't have all options set", async () => { const error = await service @@ -1289,75 +1228,6 @@ moduleIntegrationTestRunner({ expect(softDeleted).toHaveLength(1) }) - it("should emit events through eventBus", async () => { - const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") - const data = buildProductAndRelationsData({ - images, - thumbnail: images[0].url, - }) - - const products = await service.createProducts([data]) - - await service.softDeleteProducts([products[0].id]) - - expect(eventBusSpy).toHaveBeenNthCalledWith( - 1, - [ - composeMessage(ProductEvents.PRODUCT_CREATED, { - data: { id: products[0].id }, - object: "product", - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - }), - ], - { - internal: true, - } - ) - - expect(eventBusSpy).toHaveBeenNthCalledWith( - 2, - [ - composeMessage(ProductEvents.PRODUCT_DELETED, { - data: { id: [products[0].id] }, - object: "product", - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - }), - composeMessage(ProductEvents.PRODUCT_VARIANT_DELETED, { - data: { id: [products[0].variants[0].id] }, - object: "product_variant", - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - }), - composeMessage(ProductEvents.PRODUCT_OPTION_DELETED, { - data: { id: [products[0].options[0].id] }, - object: "product_option", - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - }), - composeMessage(ProductEvents.PRODUCT_IMAGE_DELETED, { - data: { - id: [products[0].images[0].id], - }, - object: "product_image", - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - }), - composeMessage(ProductEvents.PRODUCT_OPTION_VALUE_DELETED, { - data: { - id: [products[0].options[0].values[0].id], - }, - object: "product_option_value", - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - }), - ], - { - internal: true, - } - ) - }) }) describe("restore", function () { diff --git a/packages/modules/product/integration-tests/__tests__/product.ts b/packages/modules/product/integration-tests/__tests__/product.spec.ts similarity index 100% rename from packages/modules/product/integration-tests/__tests__/product.ts rename to packages/modules/product/integration-tests/__tests__/product.spec.ts diff --git a/packages/modules/product/package.json b/packages/modules/product/package.json index f1b351e223..8c086f3987 100644 --- a/packages/modules/product/package.json +++ b/packages/modules/product/package.json @@ -29,7 +29,7 @@ "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", "build": "rimraf dist && tsc --build && npm run resolve:aliases", "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", - "test:integration": "jest --bail --forceExit -- integration-tests/__tests__/**/*.ts", + "test:integration": "jest --bail --forceExit -- integration-tests/__tests__/**/*.spec.ts", "migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial", "migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create", "migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up", diff --git a/packages/modules/product/src/repositories/product-category.ts b/packages/modules/product/src/repositories/product-category.ts index b3ccfae6f0..48ce3b6163 100644 --- a/packages/modules/product/src/repositories/product-category.ts +++ b/packages/modules/product/src/repositories/product-category.ts @@ -452,9 +452,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito } let productCategory = await manager.findOne< InferEntityType - >(ProductCategory.name, { - id: categoryData.id, - }) + >(ProductCategory.name, categoryData.id!) if (!productCategory) { throw new MedusaError( diff --git a/packages/modules/product/src/repositories/product.ts b/packages/modules/product/src/repositories/product.ts index 3001ef1271..186a3b600a 100644 --- a/packages/modules/product/src/repositories/product.ts +++ b/packages/modules/product/src/repositories/product.ts @@ -9,6 +9,7 @@ import { isPresent, mergeMetadata, isDefined, + deepCopy, } from "@medusajs/framework/utils" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql" @@ -80,21 +81,22 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( ) => void, context: Context = {} ): Promise[]> { + const productsToUpdate_ = deepCopy(productsToUpdate) const productIdsToUpdate: string[] = [] - productsToUpdate.forEach((productToUpdate) => { + productsToUpdate_.forEach((productToUpdate) => { ProductRepository.#correctUpdateDTOTypes(productToUpdate) productIdsToUpdate.push(productToUpdate.id) }) const relationsToLoad = - ProductRepository.#getProductDeepUpdateRelationsToLoad(productsToUpdate) + ProductRepository.#getProductDeepUpdateRelationsToLoad(productsToUpdate_) const findOptions = buildQuery( { id: productIdsToUpdate }, { relations: relationsToLoad, - take: productsToUpdate.length, + take: productsToUpdate_.length, } ) @@ -111,7 +113,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( ) } - for (const productToUpdate of productsToUpdate) { + for (const productToUpdate of productsToUpdate_) { const product = productsMap.get(productToUpdate.id)! const wrappedProduct = wrap(product) @@ -173,7 +175,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( // Doing this to ensure updates are returned in the same order they were provided, // since some core flows rely on this. // This is a high level of coupling though. - return productsToUpdate.map( + return productsToUpdate_.map( (productToUpdate) => productsMap.get(productToUpdate.id)! ) } diff --git a/packages/modules/product/src/services/product-category.ts b/packages/modules/product/src/services/product-category.ts index 3a2eef0109..1fc7b19bc0 100644 --- a/packages/modules/product/src/services/product-category.ts +++ b/packages/modules/product/src/services/product-category.ts @@ -6,30 +6,45 @@ import { ProductTypes, } from "@medusajs/framework/types" import { + createMedusaMikroOrmEventSubscriber, FreeTextSearchFilterKeyPrefix, InjectManager, InjectTransactionManager, isDefined, MedusaContext, MedusaError, + MedusaInternalService, + MedusaService, ModulesSdkUtils, + registerInternalServiceEventSubscriber, } from "@medusajs/framework/utils" +import { EntityManager, EventType } from "@mikro-orm/core" import { ProductCategory } from "@models" import { ProductCategoryRepository } from "@repositories" import { UpdateCategoryInput } from "@types" type InjectedDependencies = { productCategoryRepository: DAL.TreeRepositoryService + productModuleService: ReturnType } -export default class ProductCategoryService { - protected readonly productCategoryRepository_: DAL.TreeRepositoryService - constructor({ productCategoryRepository }: InjectedDependencies) { - this.productCategoryRepository_ = productCategoryRepository +export default class ProductCategoryService extends MedusaInternalService< + InjectedDependencies, + typeof ProductCategory +>(ProductCategory) { + protected readonly productCategoryRepository_: DAL.TreeRepositoryService + protected readonly container: InjectedDependencies + + constructor(container: InjectedDependencies) { + // @ts-expect-error + super(...arguments) + this.container = container + this.productCategoryRepository_ = container.productCategoryRepository } // TODO: Add support for object filter @InjectManager("productCategoryRepository_") + // @ts-expect-error async retrieve( productCategoryId: string, config: FindConfig = {}, @@ -150,6 +165,7 @@ export default class ProductCategoryService { } @InjectTransactionManager("productCategoryRepository_") + // @ts-expect-error async update( data: UpdateCategoryInput[], @MedusaContext() sharedContext: Context = {} @@ -160,14 +176,45 @@ export default class ProductCategoryService { } @InjectTransactionManager("productCategoryRepository_") + // @ts-expect-error async delete( ids: string[], @MedusaContext() sharedContext: Context = {} ): Promise { - return await this.productCategoryRepository_.delete(ids, sharedContext) + const subscriber = createMedusaMikroOrmEventSubscriber( + [ProductCategory.name], + this.container["productModuleService"] + ) + + registerInternalServiceEventSubscriber(sharedContext, subscriber) + + const deletedIds = await this.productCategoryRepository_.delete( + ids, + sharedContext + ) + + // Delete are handled a bit differently since we are going to the DB directly, therefore + // just like upsert with replace, we need to dispatch the events manually. + if (deletedIds.length) { + const manager = (sharedContext.transactionManager ?? + sharedContext.manager) as EntityManager + const eventManager = manager.getEventManager() + + deletedIds.forEach((id) => { + eventManager.dispatchEvent(EventType.afterDelete, { + entity: { id }, + meta: { + className: ProductCategory.name, + } as Parameters[2], + }) + }) + } + + return deletedIds } @InjectTransactionManager("productCategoryRepository_") + // @ts-expect-error async softDelete( ids: string[], @MedusaContext() sharedContext?: Context @@ -178,6 +225,7 @@ export default class ProductCategoryService { } @InjectTransactionManager("productCategoryRepository_") + // @ts-expect-error async restore( ids: string[], @MedusaContext() sharedContext?: Context diff --git a/packages/modules/product/src/services/product-module-service.ts b/packages/modules/product/src/services/product-module-service.ts index 2af892b68a..70e334bf08 100644 --- a/packages/modules/product/src/services/product-module-service.ts +++ b/packages/modules/product/src/services/product-module-service.ts @@ -25,6 +25,7 @@ import { ProductCategoryService } from "@services" import { arrayDifference, + createMedusaMikroOrmEventSubscriber, EmitEvents, generateEntityId, InjectManager, @@ -37,11 +38,13 @@ import { MedusaContext, MedusaError, MedusaService, + MessageAggregator, Modules, ProductStatus, removeUndefined, toHandle, } from "@medusajs/framework/utils" +import { EntityManager } from "@mikro-orm/core" import { ProductRepository } from "../repositories" import { UpdateCategoryInput, @@ -52,8 +55,8 @@ import { UpdateTagInput, UpdateTypeInput, } from "../types" -import { eventBuilders } from "../utils" import { joinerConfig } from "./../joiner-config" +import { eventBuilders } from "../utils/events" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -331,11 +334,6 @@ export default class ProductModuleService sharedContext ) - eventBuilders.createdProductVariant({ - data: createdVariants, - sharedContext, - }) - return createdVariants } @@ -366,11 +364,11 @@ export default class ProductModuleService (variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id ) - let created: InferEntityType[] = [] + let created: ProductTypes.ProductVariantDTO[] = [] let updated: InferEntityType[] = [] if (forCreate.length) { - created = await this.createVariants_(forCreate, sharedContext) + created = await this.createProductVariants(forCreate, sharedContext) } if (forUpdate.length) { updated = await this.updateVariants_(forUpdate, sharedContext) @@ -493,7 +491,7 @@ export default class ProductModuleService ) } - const { entities: productVariants, performedActions } = + const { entities: productVariants } = await this.productVariantService_.upsertWithReplace( productVariantsWithOptions, { @@ -502,19 +500,6 @@ export default class ProductModuleService sharedContext ) - eventBuilders.createdProductVariant({ - data: performedActions.created[ProductVariant.name] ?? [], - sharedContext, - }) - eventBuilders.updatedProductVariant({ - data: performedActions.updated[ProductVariant.name] ?? [], - sharedContext, - }) - eventBuilders.deletedProductVariant({ - data: performedActions.deleted[ProductVariant.name] ?? [], - sharedContext, - }) - return productVariants } @@ -544,11 +529,6 @@ export default class ProductModuleService ProductTypes.ProductTagDTO[] >(tags) - eventBuilders.createdProductTag({ - data: createdTags, - sharedContext, - }) - return Array.isArray(data) ? createdTags : createdTags[0] } @@ -561,12 +541,26 @@ export default class ProductModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() @EmitEvents() async upsertProductTags( data: ProductTypes.UpsertProductTagDTO[] | ProductTypes.UpsertProductTagDTO, @MedusaContext() sharedContext: Context = {} ): Promise { + const tags = await this.upsertProductTags_(data, sharedContext) + + const allTags = await this.baseRepository_.serialize< + ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO + >(Array.isArray(data) ? tags : tags[0]) + + return allTags + } + + @InjectTransactionManager() + protected async upsertProductTags_( + data: ProductTypes.UpsertProductTagDTO[] | ProductTypes.UpsertProductTagDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter((tag): tag is UpdateTagInput => !!tag.id) const forCreate = input.filter( @@ -578,25 +572,12 @@ export default class ProductModuleService if (forCreate.length) { created = await this.productTagService_.create(forCreate, sharedContext) - eventBuilders.createdProductTag({ - data: created, - sharedContext, - }) } if (forUpdate.length) { updated = await this.productTagService_.update(forUpdate, sharedContext) - eventBuilders.updatedProductTag({ - data: updated, - sharedContext, - }) } - const result = [...created, ...updated] - const allTags = await this.baseRepository_.serialize< - ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO - >(result) - - return Array.isArray(data) ? allTags : allTags[0] + return [...created, ...updated] } // @ts-expect-error @@ -647,11 +628,6 @@ export default class ProductModuleService ProductTypes.ProductTagDTO[] >(tags) - eventBuilders.updatedProductTag({ - data: updatedTags, - sharedContext, - }) - return isString(idOrSelector) ? updatedTags[0] : updatedTags } @@ -667,6 +643,7 @@ export default class ProductModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createProductTypes( data: @@ -694,13 +671,30 @@ export default class ProductModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertProductTypes( data: | ProductTypes.UpsertProductTypeDTO[] | ProductTypes.UpsertProductTypeDTO, @MedusaContext() sharedContext: Context = {} ): Promise { + const types = await this.upsertProductTypes_(data, sharedContext) + + const result = await this.baseRepository_.serialize< + ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO + >(types) + + return Array.isArray(data) ? result : result[0] + } + + @InjectTransactionManager() + protected async upsertProductTypes_( + data: + | ProductTypes.UpsertProductTypeDTO + | ProductTypes.UpsertProductTypeDTO[], + sharedContext?: Context + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter((type): type is UpdateTypeInput => !!type.id) const forCreate = input.filter( @@ -717,12 +711,7 @@ export default class ProductModuleService updated = await this.productTypeService_.update(forUpdate, sharedContext) } - const result = [...created, ...updated] - const allTypes = await this.baseRepository_.serialize< - ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO - >(result) - - return Array.isArray(data) ? allTypes : allTypes[0] + return [...created, ...updated] } // @ts-expect-error @@ -739,6 +728,7 @@ export default class ProductModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateProductTypes( idOrSelector: string | ProductTypes.FilterableProductTypeProps, @@ -787,6 +777,7 @@ export default class ProductModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createProductOptions( data: @@ -842,6 +833,7 @@ export default class ProductModuleService ): Promise @InjectTransactionManager() + @EmitEvents() async upsertProductOptions( data: | ProductTypes.UpsertProductOptionDTO[] @@ -888,6 +880,7 @@ export default class ProductModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateProductOptions( idOrSelector: string | ProductTypes.FilterableProductOptionProps, @@ -1023,11 +1016,6 @@ export default class ProductModuleService ProductTypes.ProductCollectionDTO[] >(collections) - eventBuilders.createdProductCollection({ - data: collections, - sharedContext, - }) - return Array.isArray(data) ? createdCollections : createdCollections[0] } @@ -1061,7 +1049,7 @@ export default class ProductModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() @EmitEvents() async upsertProductCollections( data: @@ -1071,6 +1059,24 @@ export default class ProductModuleService ): Promise< ProductTypes.ProductCollectionDTO[] | ProductTypes.ProductCollectionDTO > { + const collections = await this.upsertCollections_(data, sharedContext) + + const serializedCollections = await this.baseRepository_.serialize< + ProductTypes.ProductCollectionDTO[] + >(collections) + + return Array.isArray(data) + ? serializedCollections + : serializedCollections[0] + } + + @InjectTransactionManager() + protected async upsertCollections_( + data: + | ProductTypes.UpsertProductCollectionDTO[] + | ProductTypes.UpsertProductCollectionDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (collection): collection is UpdateCollectionInput => !!collection.id @@ -1091,26 +1097,7 @@ export default class ProductModuleService updated = await this.updateCollections_(forUpdate, sharedContext) } - const result = [...created, ...updated] - const allCollections = await this.baseRepository_.serialize< - ProductTypes.ProductCollectionDTO[] | ProductTypes.ProductCollectionDTO - >(result) - - if (created.length) { - eventBuilders.createdProductCollection({ - data: created, - sharedContext, - }) - } - - if (updated.length) { - eventBuilders.updatedProductCollection({ - data: updated, - sharedContext, - }) - } - - return Array.isArray(data) ? allCollections : allCollections[0] + return [...created, ...updated] } // @ts-expect-error @@ -1166,11 +1153,6 @@ export default class ProductModuleService ProductTypes.ProductCollectionDTO[] >(collections) - eventBuilders.updatedProductCollection({ - data: updatedCollections, - sharedContext, - }) - return isString(idOrSelector) ? updatedCollections[0] : updatedCollections } @@ -1283,6 +1265,7 @@ export default class ProductModuleService ProductTypes.ProductCategoryDTO[] >(categories) + // TODO: Same as the update categories, for some reason I cant get the tree repository update eventBuilders.createdProductCategory({ data: createdCategories, sharedContext, @@ -1300,7 +1283,7 @@ export default class ProductModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() @EmitEvents() async upsertProductCategories( data: @@ -1310,11 +1293,27 @@ export default class ProductModuleService ): Promise< ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO > { + const categories = await this.upsertProductCategories_(data, sharedContext) + + const serializedCategories = await this.baseRepository_.serialize< + ProductTypes.ProductCategoryDTO[] + >(categories) + + return Array.isArray(data) ? serializedCategories : serializedCategories[0] + } + + @InjectTransactionManager() + protected async upsertProductCategories_( + data: + | ProductTypes.UpsertProductCategoryDTO[] + | ProductTypes.UpsertProductCategoryDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (category): category is UpdateCategoryInput => !!category.id ) - const forCreate = input.filter( + let forCreate = input.filter( (category): category is ProductTypes.CreateProductCategoryDTO => !category.id ) @@ -1323,6 +1322,11 @@ export default class ProductModuleService let updated: InferEntityType[] = [] if (forCreate.length) { + forCreate = forCreate.map((productCategory) => { + productCategory.handle ??= kebabCase(productCategory.name) + return productCategory + }) + created = await this.productCategoryService_.create( forCreate, sharedContext @@ -1335,25 +1339,22 @@ export default class ProductModuleService ) } - const createdCategories = await this.baseRepository_.serialize< - ProductTypes.ProductCategoryDTO[] - >(created) - const updatedCategories = await this.baseRepository_.serialize< - ProductTypes.ProductCategoryDTO[] - >(updated) + // TODO: Same as the update categories, for some reason I cant get the tree repository update + // event. I ll need to investigate this + if (created.length) { + eventBuilders.createdProductCategory({ + data: created, + sharedContext, + }) + } + if (updated.length) { + eventBuilders.updatedProductCategory({ + data: updated, + sharedContext, + }) + } - eventBuilders.createdProductCategory({ - data: createdCategories, - sharedContext, - }) - - eventBuilders.updatedProductCategory({ - data: updatedCategories, - sharedContext, - }) - - const result = [...createdCategories, ...updatedCategories] - return Array.isArray(data) ? result : result[0] + return [...created, ...updated] } // @ts-expect-error @@ -1377,8 +1378,36 @@ export default class ProductModuleService data: ProductTypes.UpdateProductCategoryDTO, @MedusaContext() sharedContext: Context = {} ): Promise< - ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO + ProductTypes.ProductCategoryDTO | ProductTypes.ProductCategoryDTO[] > { + const categories = await this.updateProductCategories_( + idOrSelector, + data, + sharedContext + ) + + const serializedCategories = await this.baseRepository_.serialize< + ProductTypes.ProductCategoryDTO[] + >(categories) + + // TODO: for some reason I cant get the tree repository update + // event. I ll need to investigate this + eventBuilders.updatedProductCategory({ + data: serializedCategories, + sharedContext, + }) + + return isString(idOrSelector) + ? serializedCategories[0] + : serializedCategories + } + + @InjectTransactionManager() + protected async updateProductCategories_( + idOrSelector: string | ProductTypes.FilterableProductTypeProps, + data: ProductTypes.UpdateProductCategoryDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let normalizedInput: UpdateCategoryInput[] = [] if (isString(idOrSelector)) { // Check if the type exists in the first place @@ -1406,16 +1435,7 @@ export default class ProductModuleService sharedContext ) - const updatedCategories = await this.baseRepository_.serialize< - ProductTypes.ProductCategoryDTO[] - >(categories) - - eventBuilders.updatedProductCategory({ - data: updatedCategories, - sharedContext, - }) - - return isString(idOrSelector) ? updatedCategories[0] : updatedCategories + return categories } //@ts-expect-error @@ -1443,11 +1463,6 @@ export default class ProductModuleService ProductTypes.ProductDTO[] >(products) - eventBuilders.createdProduct({ - data: createdProducts, - sharedContext, - }) - return Array.isArray(data) ? createdProducts : createdProducts[0] } @@ -1474,11 +1489,11 @@ export default class ProductModuleService (product): product is ProductTypes.CreateProductDTO => !product.id ) - let created: InferEntityType[] = [] + let created: ProductTypes.ProductDTO[] = [] let updated: InferEntityType[] = [] if (forCreate.length) { - created = await this.createProducts_(forCreate, sharedContext) + created = await this.createProducts(forCreate, sharedContext) } if (forUpdate.length) { updated = await this.updateProducts_(forUpdate, sharedContext) @@ -1489,20 +1504,6 @@ export default class ProductModuleService ProductTypes.ProductDTO[] | ProductTypes.ProductDTO >(result) - if (created.length) { - eventBuilders.createdProduct({ - data: created, - sharedContext, - }) - } - - if (updated.length) { - eventBuilders.updatedProduct({ - data: updated, - sharedContext, - }) - } - return Array.isArray(data) ? allProducts : allProducts[0] } @@ -1552,11 +1553,6 @@ export default class ProductModuleService ProductTypes.ProductDTO[] >(products) - eventBuilders.updatedProduct({ - data: updatedProducts, - sharedContext, - }) - return isString(idOrSelector) ? updatedProducts[0] : updatedProducts } @@ -1657,20 +1653,47 @@ export default class ProductModuleService data: UpdateProductInput[], @MedusaContext() sharedContext: Context = {} ): Promise[]> { + // We have to do that manually because this method is bypassing the product service and goes + // directly to the custom product repository + const manager = (sharedContext.transactionManager ?? + sharedContext.manager) as EntityManager + const subscriber = createMedusaMikroOrmEventSubscriber( + ["updateProducts_"], + this as unknown as ReturnType> + ) + + if (manager && subscriber) { + manager + .getEventManager() + .registerSubscriber(new subscriber(sharedContext)) + } + + const originalProducts = await this.productService_.list( + { + id: data.map((d) => d.id), + }, + { + relations: ["options", "options.values", "variants", "images", "tags"], + }, + sharedContext + ) + const normalizedProducts = await this.normalizeUpdateProductInput( data, - sharedContext + originalProducts ) for (const product of normalizedProducts) { this.validateProductUpdatePayload(product) } - return this.productRepository_.deepUpdate( + const updatedProducts = await this.productRepository_.deepUpdate( normalizedProducts, ProductModuleService.validateVariantOptions, sharedContext ) + + return updatedProducts } // @ts-expect-error @@ -1685,6 +1708,7 @@ export default class ProductModuleService data: ProductTypes.UpdateProductOptionValueDTO, sharedContext?: Context ): Promise + // @ts-expect-error async updateProductOptionValues( idOrSelector: string | FilterableProductOptionValueProps, @@ -1693,6 +1717,11 @@ export default class ProductModuleService ): Promise< ProductTypes.ProductOptionValueDTO | ProductTypes.ProductOptionValueDTO[] > { + // TODO: There is a missmatch in the API which lead to function with different number of + // arguments. Therefore, applying the MedusaContext() decorator to the function will not work + // because the context arg index will differ from method to method. + sharedContext.messageAggregator ??= new MessageAggregator() + let normalizedInput: ({ id: string } & ProductTypes.UpdateProductOptionValueDTO)[] = [] @@ -1718,7 +1747,7 @@ export default class ProductModuleService })) } - const productOptionValues = await super.updateProductOptionValues( + const productOptionValues = await this.updateProductOptionValues_( normalizedInput, sharedContext ) @@ -1727,16 +1756,45 @@ export default class ProductModuleService ProductTypes.ProductOptionValueDTO[] >(productOptionValues) - eventBuilders.updatedProductOptionValue({ - data: updatedProductOptionValues, - sharedContext: sharedContext, - }) + // TODO: Because of the wrong method override, we have to compensate to prevent breaking + // changes right now + const groupedEvents = sharedContext.messageAggregator!.getMessages() + if ( + Object.values(groupedEvents).flat().length > 0 && + this.eventBusModuleService_ + ) { + const promises: Promise[] = [] + for (const group of Object.keys(groupedEvents)) { + promises.push( + this.eventBusModuleService_!.emit(groupedEvents[group], { + internal: true, + }) + ) + } + + await Promise.all(promises) + + sharedContext.messageAggregator.clearMessages() + } return isString(idOrSelector) ? updatedProductOptionValues[0] : updatedProductOptionValues } + @InjectTransactionManager() + protected async updateProductOptionValues_( + normalizedInput: ({ + id: string + } & ProductTypes.UpdateProductOptionValueDTO)[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { + return await this.productOptionValueService_.update( + normalizedInput, + sharedContext + ) + } + /** * Validates the manually provided handle value of the product * to be URL-safe @@ -1805,8 +1863,7 @@ export default class ProductModuleService const products_ = Array.isArray(products) ? products : [products] const normalizedProducts = (await this.normalizeUpdateProductInput( - products_ as UpdateProductInput[], - sharedContext + products_ as UpdateProductInput[] )) as ProductTypes.CreateProductDTO[] for (const productData of normalizedProducts) { @@ -1839,6 +1896,12 @@ export default class ProductModuleService ) as TOutput } + /** + * Normalizes the input for the update product input + * @param products - The products to normalize + * @param originalProducts - The original products to use for the normalization (must include options and option values relations) + * @returns The normalized products + */ protected async normalizeUpdateProductInput< T extends UpdateProductInput | UpdateProductInput[], TOutput = T extends UpdateProductInput[] @@ -1846,7 +1909,7 @@ export default class ProductModuleService : UpdateProductInput >( products: T, - @MedusaContext() sharedContext: Context = {} + originalProducts?: InferEntityType[] ): Promise { const products_ = Array.isArray(products) ? products : [products] const productsIds = products_.map((p) => p.id).filter(Boolean) @@ -1854,11 +1917,14 @@ export default class ProductModuleService let dbOptions: InferEntityType[] = [] if (productsIds.length) { - dbOptions = await this.productOptionService_.list( - { product_id: productsIds }, - { relations: ["values"] }, - sharedContext - ) + // Re map options to handle non serialized data as well + dbOptions = + originalProducts + ?.map((originalProduct) => + originalProduct.options.map((option) => option) + ) + .flat() + .filter(Boolean) ?? [] } const normalizedProducts: UpdateProductInput[] = [] @@ -1872,7 +1938,9 @@ export default class ProductModuleService if (productData.options?.length) { ;(productData as any).options = productData.options?.map((option) => { const dbOption = dbOptions.find( - (o) => o.title === option.title && o.product_id === productData.id + (o) => + (o.title === option.title || o.id === option.id) && + o.product_id === productData.id ) return { title: option.title, diff --git a/packages/modules/product/src/utils/events.ts b/packages/modules/product/src/utils/events.ts index dc91d7c417..13059728f4 100644 --- a/packages/modules/product/src/utils/events.ts +++ b/packages/modules/product/src/utils/events.ts @@ -6,96 +6,6 @@ import { } from "@medusajs/framework/utils" export const eventBuilders = { - createdProduct: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product", - eventName: ProductEvents.PRODUCT_CREATED, - }), - updatedProduct: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product", - eventName: ProductEvents.PRODUCT_UPDATED, - }), - deletedProduct: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product", - eventName: ProductEvents.PRODUCT_DELETED, - }), - createdProductVariant: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_variant", - eventName: ProductEvents.PRODUCT_VARIANT_CREATED, - }), - updatedProductVariant: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_variant", - eventName: ProductEvents.PRODUCT_VARIANT_UPDATED, - }), - deletedProductVariant: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_variant", - eventName: ProductEvents.PRODUCT_VARIANT_DELETED, - }), - createdProductOption: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_option", - eventName: ProductEvents.PRODUCT_OPTION_CREATED, - }), - updatedProductOption: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_option", - eventName: ProductEvents.PRODUCT_OPTION_UPDATED, - }), - deletedProductOption: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_option", - eventName: ProductEvents.PRODUCT_OPTION_DELETED, - }), - createdProductType: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_type", - eventName: ProductEvents.PRODUCT_TYPE_CREATED, - }), - updatedProductType: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_type", - eventName: ProductEvents.PRODUCT_TYPE_UPDATED, - }), - deletedProductType: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_type", - eventName: ProductEvents.PRODUCT_TYPE_DELETED, - }), - createdProductTag: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_tag", - eventName: ProductEvents.PRODUCT_TAG_CREATED, - }), - updatedProductTag: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_tag", - eventName: ProductEvents.PRODUCT_TAG_UPDATED, - }), - deletedProductTag: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_tag", - eventName: ProductEvents.PRODUCT_TAG_DELETED, - }), createdProductCategory: moduleEventBuilderFactory({ source: Modules.PRODUCT, action: CommonEvents.CREATED, @@ -114,40 +24,4 @@ export const eventBuilders = { object: "product_category", eventName: ProductEvents.PRODUCT_CATEGORY_DELETED, }), - createdProductCollection: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_collection", - eventName: ProductEvents.PRODUCT_COLLECTION_CREATED, - }), - updatedProductCollection: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_collection", - eventName: ProductEvents.PRODUCT_COLLECTION_UPDATED, - }), - deletedProductCollection: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_collection", - eventName: ProductEvents.PRODUCT_COLLECTION_DELETED, - }), - createdProductOptionValue: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.CREATED, - object: "product_option_value", - eventName: ProductEvents.PRODUCT_OPTION_VALUE_CREATED, - }), - updatedProductOptionValue: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.UPDATED, - object: "product_option_value", - eventName: ProductEvents.PRODUCT_OPTION_VALUE_UPDATED, - }), - deletedProductOptionValue: moduleEventBuilderFactory({ - source: Modules.PRODUCT, - action: CommonEvents.DELETED, - object: "product_option_value", - eventName: ProductEvents.PRODUCT_OPTION_VALUE_DELETED, - }), } diff --git a/packages/modules/promotion/src/services/promotion-module.ts b/packages/modules/promotion/src/services/promotion-module.ts index 97d6c02688..bafd86223b 100644 --- a/packages/modules/promotion/src/services/promotion-module.ts +++ b/packages/modules/promotion/src/services/promotion-module.ts @@ -18,6 +18,7 @@ import { CampaignBudgetType, ComputedActions, deduplicate, + EmitEvents, InjectManager, InjectTransactionManager, isDefined, @@ -193,6 +194,7 @@ export default class PromotionModuleService } @InjectTransactionManager() + @EmitEvents() async registerUsage( computedActions: PromotionTypes.UsageComputedActions[], @MedusaContext() sharedContext: Context = {} @@ -306,6 +308,7 @@ export default class PromotionModuleService } @InjectTransactionManager() + @EmitEvents() async revertUsage( computedActions: PromotionTypes.UsageComputedActions[], @MedusaContext() sharedContext: Context = {} @@ -625,6 +628,7 @@ export default class PromotionModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createPromotions( data: @@ -1041,6 +1045,7 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() // @ts-ignore async updatePromotionRules( data: PromotionTypes.UpdatePromotionRuleDTO[], @@ -1149,6 +1154,7 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async addPromotionTargetRules( promotionId: string, rulesData: PromotionTypes.CreatePromotionRuleDTO[], @@ -1182,6 +1188,7 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async addPromotionBuyRules( promotionId: string, rulesData: PromotionTypes.CreatePromotionRuleDTO[], @@ -1278,6 +1285,7 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async removePromotionRules( promotionId: string, ruleIds: string[], @@ -1305,6 +1313,7 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async removePromotionTargetRules( promotionId: string, ruleIds: string[], @@ -1379,6 +1388,7 @@ export default class PromotionModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createCampaigns( data: PromotionTypes.CreateCampaignDTO | PromotionTypes.CreateCampaignDTO[], @@ -1490,6 +1500,7 @@ export default class PromotionModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateCampaigns( data: PromotionTypes.UpdateCampaignDTO | PromotionTypes.UpdateCampaignDTO[], @@ -1583,9 +1594,10 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async addPromotionsToCampaign( data: PromotionTypes.AddPromotionsToCampaignDTO, - sharedContext?: Context + @MedusaContext() sharedContext?: Context ): Promise<{ ids: string[] }> { const ids = await this.addPromotionsToCampaign_(data, sharedContext) @@ -1650,9 +1662,10 @@ export default class PromotionModuleService } @InjectManager() + @EmitEvents() async removePromotionsFromCampaign( data: PromotionTypes.AddPromotionsToCampaignDTO, - sharedContext?: Context + @MedusaContext() sharedContext: Context = {} ): Promise<{ ids: string[] }> { const ids = await this.removePromotionsFromCampaign_(data, sharedContext) diff --git a/packages/modules/region/src/services/region-module.ts b/packages/modules/region/src/services/region-module.ts index 2ec4a5eb3f..21007fdeed 100644 --- a/packages/modules/region/src/services/region-module.ts +++ b/packages/modules/region/src/services/region-module.ts @@ -15,6 +15,7 @@ import { } from "@medusajs/framework/types" import { arrayDifference, + EmitEvents, getDuplicates, InjectManager, InjectTransactionManager, @@ -78,6 +79,7 @@ export default class RegionModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createRegions( data: CreateRegionDTO | CreateRegionDTO[], @@ -132,6 +134,7 @@ export default class RegionModuleService } @InjectManager() + @EmitEvents() // @ts-expect-error async softDeleteRegions( ids: string | object | string[] | object[], @@ -160,11 +163,24 @@ export default class RegionModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertRegions( data: UpsertRegionDTO | UpsertRegionDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.upsertRegions_(data, sharedContext) + + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0] + ) + } + + @InjectTransactionManager() + protected async upsertRegions_( + data: UpsertRegionDTO | UpsertRegionDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (region): region is UpdateRegionInput => !!region.id @@ -183,9 +199,8 @@ export default class RegionModuleService } const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize( - Array.isArray(data) ? result : result[0] - ) + + return result } // @ts-expect-error @@ -202,6 +217,7 @@ export default class RegionModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateRegions( idOrSelector: string | FilterableRegionProps, diff --git a/packages/modules/sales-channel/src/services/sales-channel-module.ts b/packages/modules/sales-channel/src/services/sales-channel-module.ts index 6d60d1b88e..61e98e6bf1 100644 --- a/packages/modules/sales-channel/src/services/sales-channel-module.ts +++ b/packages/modules/sales-channel/src/services/sales-channel-module.ts @@ -13,6 +13,7 @@ import { UpsertSalesChannelDTO, } from "@medusajs/framework/types" import { + EmitEvents, InjectManager, InjectTransactionManager, isString, @@ -67,6 +68,7 @@ export default class SalesChannelModuleService ): Promise @InjectManager() + // @ts-expect-error async createSalesChannels( data: CreateSalesChannelDTO | CreateSalesChannelDTO[], @@ -77,10 +79,7 @@ export default class SalesChannelModuleService const result = await this.createSalesChannels_(input, sharedContext) return await this.baseRepository_.serialize( - Array.isArray(data) ? result : result[0], - { - populate: true, - } + Array.isArray(data) ? result : result[0] ) } @@ -106,6 +105,7 @@ export default class SalesChannelModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateSalesChannels( idOrSelector: string | FilterableSalesChannelProps, @@ -134,10 +134,7 @@ export default class SalesChannelModuleService ) return await this.baseRepository_.serialize( - Array.isArray(data) ? result : result[0], - { - populate: true, - } + Array.isArray(data) ? result : result[0] ) } @@ -157,11 +154,25 @@ export default class SalesChannelModuleService data: UpsertSalesChannelDTO, sharedContext?: Context ): Promise + @InjectManager() + @EmitEvents() @InjectTransactionManager() async upsertSalesChannels( data: UpsertSalesChannelDTO | UpsertSalesChannelDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.upsertSalesChannels_(data, sharedContext) + + return await this.baseRepository_.serialize< + SalesChannelDTO[] | SalesChannelDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager() + protected async upsertSalesChannels_( + data: UpsertSalesChannelDTO | UpsertSalesChannelDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (channel): channel is UpdateSalesChannelDTO => !!channel.id @@ -180,8 +191,7 @@ export default class SalesChannelModuleService } const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize< - SalesChannelDTO[] | SalesChannelDTO - >(Array.isArray(data) ? result : result[0]) + + return result } } diff --git a/packages/modules/settings/src/services/settings-module-service.ts b/packages/modules/settings/src/services/settings-module-service.ts index 6d11c68da0..282b3b5366 100644 --- a/packages/modules/settings/src/services/settings-module-service.ts +++ b/packages/modules/settings/src/services/settings-module-service.ts @@ -7,6 +7,7 @@ import { SettingsTypes, } from "@medusajs/framework/types" import { + EmitEvents, InjectManager, InjectTransactionManager, MedusaContext, @@ -50,7 +51,8 @@ export default class SettingsModuleService this.userPreferenceService_ = userPreferenceService } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async createViewConfigurations( data: @@ -97,10 +99,14 @@ export default class SettingsModuleService dataArray, sharedContext ) - return isArrayInput ? result : result[0] + + return await this.baseRepository_.serialize< + SettingsTypes.ViewConfigurationDTO[] | SettingsTypes.ViewConfigurationDTO + >(isArrayInput ? result : result[0]) } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() // @ts-expect-error async updateViewConfigurations( idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps, @@ -109,6 +115,25 @@ export default class SettingsModuleService ): Promise< SettingsTypes.ViewConfigurationDTO | SettingsTypes.ViewConfigurationDTO[] > { + const updated = await this.updateViewConfigurations_( + idOrSelector, + data, + sharedContext + ) + + const serialized = await this.baseRepository_.serialize< + SettingsTypes.ViewConfigurationDTO[] | SettingsTypes.ViewConfigurationDTO + >(updated) + + return typeof idOrSelector === "string" ? serialized[0] : serialized + } + + @InjectTransactionManager() + protected async updateViewConfigurations_( + idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps, + data: SettingsTypes.UpdateViewConfigurationDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { let selector: SettingsTypes.FilterableViewConfigurationProps = {} if (typeof idOrSelector === "string") { @@ -164,11 +189,7 @@ export default class SettingsModuleService sharedContext ) - const serialized = await this.baseRepository_.serialize< - SettingsTypes.ViewConfigurationDTO[] - >(updatedEntities, { populate: true }) - - return typeof idOrSelector === "string" ? serialized[0] : serialized + return updatedEntities } // For non-configuration updates, use the standard update method @@ -177,11 +198,7 @@ export default class SettingsModuleService sharedContext ) - const serialized = await this.baseRepository_.serialize< - SettingsTypes.ViewConfigurationDTO[] - >(updated, { populate: true }) - - return typeof idOrSelector === "string" ? serialized[0] : serialized + return updated as unknown as InferEntityType[] } @InjectManager() @@ -201,12 +218,12 @@ export default class SettingsModuleService } return await this.baseRepository_.serialize( - prefs[0], - { populate: true } + prefs[0] ) } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async setUserPreference( userId: string, key: string, @@ -236,8 +253,7 @@ export default class SettingsModuleService } return await this.baseRepository_.serialize( - result, - { populate: true } + result ) } @@ -297,7 +313,8 @@ export default class SettingsModuleService return systemDefaults.length > 0 ? systemDefaults[0] : null } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async setActiveViewConfiguration( entity: string, userId: string, @@ -347,7 +364,8 @@ export default class SettingsModuleService return systemDefaults.length > 0 ? systemDefaults[0] : null } - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async clearActiveViewConfiguration( entity: string, userId: string, diff --git a/packages/modules/stock-location/src/services/stock-location-module.ts b/packages/modules/stock-location/src/services/stock-location-module.ts index 8e9487ad9e..70bc70522c 100644 --- a/packages/modules/stock-location/src/services/stock-location-module.ts +++ b/packages/modules/stock-location/src/services/stock-location-module.ts @@ -17,6 +17,7 @@ import { UpsertStockLocationInput, } from "@medusajs/framework/types" import { + EmitEvents, InjectManager, InjectTransactionManager, isString, @@ -86,7 +87,9 @@ export default class StockLocationModuleService data: CreateStockLocationInput[], context: Context ): Promise + @InjectManager() + @EmitEvents() // @ts-expect-error async createStockLocations( data: CreateStockLocationInput | CreateStockLocationInput[], @@ -101,7 +104,7 @@ export default class StockLocationModuleService const serialized = await this.baseRepository_.serialize< | StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO[] - >(created, { populate: true }) + >(created) return Array.isArray(data) ? serialized : serialized[0] } @@ -124,6 +127,7 @@ export default class StockLocationModuleService ): Promise @InjectManager() + @EmitEvents() async upsertStockLocations( data: UpsertStockLocationInput | UpsertStockLocationInput[], @MedusaContext() context: Context = {} @@ -179,6 +183,7 @@ export default class StockLocationModuleService input: UpdateStockLocationInput, context?: Context ): Promise + /** * Updates an existing stock location. * @param stockLocationId - The ID of the stock location to update. @@ -187,6 +192,7 @@ export default class StockLocationModuleService * @returns The updated stock location. */ @InjectManager() + @EmitEvents() // @ts-expect-error async updateStockLocations( idOrSelector: string | FilterableStockLocationProps, @@ -208,7 +214,7 @@ export default class StockLocationModuleService const serialized = await this.baseRepository_.serialize< | StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO[] - >(updated, { populate: true }) + >(updated) return Array.isArray(data) ? serialized : serialized[0] } @@ -239,6 +245,7 @@ export default class StockLocationModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateStockLocationAddresses( data: @@ -253,7 +260,7 @@ export default class StockLocationModuleService const serialized = await this.baseRepository_.serialize< | StockLocationTypes.StockLocationAddressDTO | StockLocationTypes.StockLocationAddressDTO[] - >(updated, { populate: true }) + >(updated) return Array.isArray(data) ? serialized : serialized[0] } @@ -276,6 +283,7 @@ export default class StockLocationModuleService ): Promise @InjectManager() + @EmitEvents() async upsertStockLocationAddresses( data: UpsertStockLocationAddressInput | UpsertStockLocationAddressInput[], @MedusaContext() context: Context = {} diff --git a/packages/modules/store/src/services/store-module-service.ts b/packages/modules/store/src/services/store-module-service.ts index 5a6221a0d3..cf3e63d693 100644 --- a/packages/modules/store/src/services/store-module-service.ts +++ b/packages/modules/store/src/services/store-module-service.ts @@ -8,6 +8,7 @@ import { StoreTypes, } from "@medusajs/framework/types" import { + EmitEvents, getDuplicates, InjectManager, InjectTransactionManager, @@ -59,7 +60,9 @@ export default class StoreModuleService data: StoreTypes.CreateStoreDTO, sharedContext?: Context ): Promise + @InjectManager() + @EmitEvents() // @ts-expect-error async createStores( data: StoreTypes.CreateStoreDTO | StoreTypes.CreateStoreDTO[], @@ -99,11 +102,25 @@ export default class StoreModuleService data: StoreTypes.UpsertStoreDTO, sharedContext?: Context ): Promise - @InjectTransactionManager() + + @InjectManager() + @EmitEvents() async upsertStores( data: StoreTypes.UpsertStoreDTO | StoreTypes.UpsertStoreDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { + const result = await this.upsertStores_(data, sharedContext) + + return await this.baseRepository_.serialize< + StoreTypes.StoreDTO[] | StoreTypes.StoreDTO + >(Array.isArray(data) ? result : result[0]) + } + + @InjectTransactionManager() + protected async upsertStores_( + data: StoreTypes.UpsertStoreDTO | StoreTypes.UpsertStoreDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise[]> { const input = Array.isArray(data) ? data : [data] const forUpdate = input.filter( (store): store is UpdateStoreInput => !!store.id @@ -122,9 +139,8 @@ export default class StoreModuleService } const result = (await promiseAll(operations)).flat() - return await this.baseRepository_.serialize< - StoreTypes.StoreDTO[] | StoreTypes.StoreDTO - >(Array.isArray(data) ? result : result[0]) + + return result } // @ts-expect-error @@ -139,7 +155,9 @@ export default class StoreModuleService data: StoreTypes.UpdateStoreDTO, sharedContext?: Context ): Promise + @InjectManager() + @EmitEvents() // @ts-expect-error async updateStores( idOrSelector: string | StoreTypes.FilterableStoreProps, diff --git a/packages/modules/tax/src/services/tax-module-service.ts b/packages/modules/tax/src/services/tax-module-service.ts index 6971e9dc75..597089e5dd 100644 --- a/packages/modules/tax/src/services/tax-module-service.ts +++ b/packages/modules/tax/src/services/tax-module-service.ts @@ -10,6 +10,7 @@ import { TaxTypes, } from "@medusajs/framework/types" import { + EmitEvents, InjectManager, InjectTransactionManager, isDefined, @@ -17,7 +18,6 @@ import { MedusaContext, MedusaError, ModulesSdkUtils, - promiseAll, } from "@medusajs/framework/utils" import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models" import { TaxProviderService } from "@services" @@ -94,6 +94,7 @@ export default class TaxModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createTaxRates( data: TaxTypes.CreateTaxRateDTO[] | TaxTypes.CreateTaxRateDTO, @@ -101,7 +102,12 @@ export default class TaxModuleService ): Promise { const input = Array.isArray(data) ? data : [data] const rates = await this.createTaxRates_(input, sharedContext) - return Array.isArray(data) ? rates : rates[0] + + const serialized = await this.baseRepository_.serialize< + TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO + >(rates) + + return Array.isArray(data) ? serialized : serialized[0] } @InjectTransactionManager() @@ -145,9 +151,7 @@ export default class TaxModuleService await this.taxRateRuleService_.create(rulesToCreate, sharedContext) } - return await this.baseRepository_.serialize(rates, { - populate: true, - }) + return rates } // @ts-expect-error @@ -170,6 +174,7 @@ export default class TaxModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async updateTaxRates( selector: string | string[] | TaxTypes.FilterableTaxRateProps, @@ -177,9 +182,11 @@ export default class TaxModuleService @MedusaContext() sharedContext: Context = {} ): Promise { const rates = await this.updateTaxRates_(selector, data, sharedContext) + const serialized = await this.baseRepository_.serialize< TaxTypes.TaxRateDTO[] - >(rates, { populate: true }) + >(rates) + return isString(selector) ? serialized[0] : serialized } @@ -282,15 +289,18 @@ export default class TaxModuleService sharedContext?: Context ): Promise - @InjectTransactionManager() + @InjectManager() + @EmitEvents() async upsertTaxRates( data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { const result = await this.taxRateService_.upsert(data, sharedContext) + const serialized = await this.baseRepository_.serialize< TaxTypes.TaxRateDTO[] - >(result, { populate: true }) + >(result) + return Array.isArray(data) ? serialized : serialized[0] } @@ -307,6 +317,7 @@ export default class TaxModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createTaxRegions( data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[], @@ -314,9 +325,15 @@ export default class TaxModuleService ) { const input = Array.isArray(data) ? data : [data] const result = await this.createTaxRegions_(input, sharedContext) - return Array.isArray(data) ? result : result[0] + + const serialized = await this.baseRepository_.serialize< + TaxTypes.TaxRegionDTO[] | TaxTypes.TaxRegionDTO + >(result) + + return Array.isArray(data) ? serialized : serialized[0] } + @InjectTransactionManager() async createTaxRegions_( data: TaxTypes.CreateTaxRegionDTO[], sharedContext: Context = {} @@ -347,10 +364,7 @@ export default class TaxModuleService await this.createTaxRates(rates, sharedContext) } - return await this.baseRepository_.serialize( - regions, - { populate: true } - ) + return regions } // @ts-expect-error @@ -365,6 +379,7 @@ export default class TaxModuleService ): Promise @InjectManager() + @EmitEvents() // @ts-expect-error async createTaxRateRules( data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[], @@ -372,7 +387,12 @@ export default class TaxModuleService ) { const input = Array.isArray(data) ? data : [data] const result = await this.createTaxRateRules_(input, sharedContext) - return Array.isArray(data) ? result : result[0] + + const serialized = await this.baseRepository_.serialize< + TaxTypes.TaxRateRuleDTO[] | TaxTypes.TaxRateRuleDTO + >(result) + + return Array.isArray(data) ? serialized : serialized[0] } @InjectTransactionManager() @@ -381,12 +401,7 @@ export default class TaxModuleService @MedusaContext() sharedContext: Context = {} ) { const rules = await this.taxRateRuleService_.create(data, sharedContext) - return await this.baseRepository_.serialize( - rules, - { - populate: true, - } - ) + return rules } @InjectManager() @@ -419,30 +434,71 @@ export default class TaxModuleService return [] } - const toReturn = await promiseAll( - items.map(async (item) => { - const regionIds = regions.map((r) => r.id) - const rateQuery = this.getTaxRateQueryForItem(item, regionIds) - const candidateRates = await this.taxRateService_.list( - rateQuery, - { - relations: ["tax_region", "rules"], - }, - sharedContext - ) + const regionIds = regions.map((r) => r.id) - const applicableRates = await this.getTaxRatesForItem( - item, - candidateRates - ) + // Collect all unique reference IDs for batch query + const productIds = new Set() + const productTypeIds = new Set() + const shippingOptionIds = new Set() - return { - rates: applicableRates, - item, + items.forEach((item) => { + if ("shipping_option_id" in item) { + shippingOptionIds.add(item.shipping_option_id) + } else { + productIds.add(item.product_id) + if (item.product_type_id) { + productTypeIds.add(item.product_type_id) } - }) + } + }) + + // Build comprehensive query for all items + const ruleQueries = [ + ...Array.from(productIds).map((id) => ({ + reference: "product", + reference_id: id, + })), + ...Array.from(productTypeIds).map((id) => ({ + reference: "product_type", + reference_id: id, + })), + ...Array.from(shippingOptionIds).map((id) => ({ + reference: "shipping_option", + reference_id: id, + })), + ] + + const allCandidateRates = await this.taxRateService_.list( + { + $and: [ + { tax_region_id: regionIds }, + { + $or: [ + { is_default: true }, + ...(ruleQueries.length ? [{ rules: { $or: ruleQueries } }] : []), + ], + }, + ], + }, + { + relations: ["tax_region", "rules"], + }, + sharedContext ) + const toReturn = items.map((item) => { + const rateQuery = this.getTaxRateQueryForItem(item, regionIds) + const candidateRates = allCandidateRates.filter((rate) => + this.rateMatchesQuery(rate, rateQuery) + ) + const applicableRates = this.getTaxRatesForItem(item, candidateRates) + + return { + rates: applicableRates, + item, + } + }) + const taxLines = await this.getTaxLinesFromProvider( parentRegion.provider_id as string, toReturn, @@ -574,10 +630,10 @@ export default class TaxModuleService } } - private async getTaxRatesForItem( + private getTaxRatesForItem( item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, rates: InferEntityType[] - ): Promise[]> { + ): InferEntityType[] { if (!rates.length) { return [] } @@ -609,7 +665,17 @@ export default class TaxModuleService private getTaxRateQueryForItem( item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, regionIds: string[] - ) { + ): { + $and: { + tax_region_id?: string[] + $or?: { + is_default?: boolean + rules?: { + $or: { reference: string; reference_id: string | undefined }[] + } + }[] + }[] + } { const isShipping = "shipping_option_id" in item let ruleQuery = isShipping ? [ @@ -637,6 +703,58 @@ export default class TaxModuleService } } + private rateMatchesQuery( + rate: InferEntityType, + query: { + $and: { + tax_region_id?: string[] + $or?: { + is_default?: boolean + rules?: { + $or: { reference: string; reference_id: string | undefined }[] + } + }[] + }[] + } + ): boolean { + const { $and } = query + const [regionCheck, ruleCheck] = $and + + // Check region match + if (!regionCheck.tax_region_id?.includes(rate.tax_region_id)) { + return false + } + + // Check rule match + const { $or } = ruleCheck + if (rate.is_default) { + return true + } + + // Check if any rule matches + for (const ruleCondition of $or ?? []) { + if (ruleCondition.is_default && rate.is_default) { + return true + } + if (ruleCondition.rules) { + const { $or: ruleQueries } = ruleCondition.rules + for (const ruleQuery of ruleQueries) { + if ( + [...(rate.rules ?? [])]?.some( + (rule: InferEntityType) => + rule.reference === ruleQuery.reference && + rule.reference_id === ruleQuery.reference_id + ) + ) { + return true + } + } + } + } + + return false + } + private checkRuleMatches( rate: InferEntityType, item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO diff --git a/packages/modules/user/integration-tests/__tests__/invite.spec.ts b/packages/modules/user/integration-tests/__tests__/invite.spec.ts index 5e238035ad..250d6b3547 100644 --- a/packages/modules/user/integration-tests/__tests__/invite.spec.ts +++ b/packages/modules/user/integration-tests/__tests__/invite.spec.ts @@ -172,17 +172,20 @@ moduleIntegrationTestRunner({ }, ]) - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ + // 2 events: 1 invite updated, 1 invite token generated + const events = eventBusSpy.mock.calls[0][0] + expect(events).toHaveLength(2) + expect(events).toEqual( + expect.arrayContaining([ expect.objectContaining({ data: { id: "1" }, name: UserEvents.INVITE_UPDATED, }), - ], - { - internal: true, - } + expect.objectContaining({ + data: { id: "1" }, + name: UserEvents.INVITE_TOKEN_GENERATED, + }), + ]) ) }) }) @@ -190,21 +193,25 @@ moduleIntegrationTestRunner({ describe("resendInvite", () => { it("should emit token generated event for invites", async () => { await service.createInvites(defaultInviteData) + const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") + eventBusSpy.mockClear() await service.refreshInviteTokens(["1"]) - expect(eventBusSpy).toHaveBeenCalledTimes(2) - expect(eventBusSpy).toHaveBeenCalledWith( - [ + const events = eventBusSpy.mock.calls[0][0] + expect(events).toHaveLength(2) + expect(events).toEqual( + expect.arrayContaining([ expect.objectContaining({ + name: UserEvents.INVITE_UPDATED, data: { id: "1" }, - name: UserEvents.INVITE_TOKEN_GENERATED, }), - ], - { - internal: true, - } + expect.objectContaining({ + name: UserEvents.INVITE_TOKEN_GENERATED, + data: { id: "1" }, + }), + ]) ) }) }) @@ -251,21 +258,25 @@ moduleIntegrationTestRunner({ const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") await service.createInvites(defaultInviteData) - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ + // 4 events: 2 invites created, 2 invite token generated + const events = eventBusSpy.mock.calls[0][0] + expect(events).toHaveLength(3) + + expect(events).toEqual( + expect.arrayContaining([ expect.objectContaining({ - data: { id: ["1", "2"] }, + data: { id: "1" }, + name: UserEvents.INVITE_CREATED, + }), + expect.objectContaining({ + data: { id: "2" }, name: UserEvents.INVITE_CREATED, }), expect.objectContaining({ data: { id: ["1", "2"] }, name: UserEvents.INVITE_TOKEN_GENERATED, }), - ], - { - internal: true, - } + ]) ) }) }) diff --git a/packages/modules/user/integration-tests/__tests__/user.spec.ts b/packages/modules/user/integration-tests/__tests__/user.spec.ts index 00da40fadb..8b07f019d9 100644 --- a/packages/modules/user/integration-tests/__tests__/user.spec.ts +++ b/packages/modules/user/integration-tests/__tests__/user.spec.ts @@ -253,17 +253,22 @@ moduleIntegrationTestRunner({ const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") await service.createUsers(defaultUserData) - expect(eventBusSpy).toHaveBeenCalledTimes(1) - expect(eventBusSpy).toHaveBeenCalledWith( - [ + // 2 events: 2 user created + expect(eventBusSpy.mock.calls[0][0]).toHaveLength(2) + + const events = eventBusSpy.mock.calls[0][0] + expect(events).toHaveLength(2) + expect(events).toEqual( + expect.arrayContaining([ expect.objectContaining({ - data: { id: ["1", "2"] }, + data: { id: "1" }, name: UserEvents.USER_CREATED, }), - ], - { - internal: true, - } + expect.objectContaining({ + data: { id: "2" }, + name: UserEvents.USER_CREATED, + }), + ]) ) }) }) diff --git a/packages/modules/user/src/services/user-module.ts b/packages/modules/user/src/services/user-module.ts index 2f07ef5e9d..deb52446a3 100644 --- a/packages/modules/user/src/services/user-module.ts +++ b/packages/modules/user/src/services/user-module.ts @@ -95,7 +95,7 @@ export default class UserModuleService } } - @InjectTransactionManager() + @InjectManager() async validateInviteToken( token: string, @MedusaContext() sharedContext: Context = {} @@ -129,9 +129,7 @@ export default class UserModuleService ) } - return await this.baseRepository_.serialize(invite, { - populate: true, - }) + return await this.baseRepository_.serialize(invite) } @InjectManager() @@ -142,22 +140,21 @@ export default class UserModuleService ): Promise { const invites = await this.refreshInviteTokens_(inviteIds, sharedContext) + const serializedInvites = await this.baseRepository_.serialize< + UserTypes.InviteDTO[] + >(invites) + moduleEventBuilderFactory({ eventName: UserEvents.INVITE_TOKEN_GENERATED, source: Modules.USER, - action: "token_generated", + action: CommonEvents.CREATED, object: "invite", })({ - data: invites, + data: serializedInvites, sharedContext, }) - return await this.baseRepository_.serialize( - invites, - { - populate: true, - } - ) + return serializedInvites } @InjectTransactionManager() @@ -221,19 +218,7 @@ export default class UserModuleService const serializedUsers = await this.baseRepository_.serialize< UserTypes.UserDTO[] | UserTypes.UserDTO - >(users, { - populate: true, - }) - - moduleEventBuilderFactory({ - eventName: UserEvents.USER_CREATED, - source: Modules.USER, - action: CommonEvents.CREATED, - object: "user", - })({ - data: serializedUsers, - sharedContext, - }) + >(users) return Array.isArray(data) ? serializedUsers : serializedUsers[0] } @@ -262,19 +247,7 @@ export default class UserModuleService const serializedUsers = await this.baseRepository_.serialize< UserTypes.UserDTO[] - >(updatedUsers, { - populate: true, - }) - - moduleEventBuilderFactory({ - eventName: UserEvents.USER_UPDATED, - source: Modules.USER, - action: CommonEvents.UPDATED, - object: "user", - })({ - data: serializedUsers, - sharedContext, - }) + >(updatedUsers) return Array.isArray(data) ? serializedUsers : serializedUsers[0] } @@ -303,19 +276,7 @@ export default class UserModuleService const serializedInvites = await this.baseRepository_.serialize< UserTypes.InviteDTO[] | UserTypes.InviteDTO - >(invites, { - populate: true, - }) - - moduleEventBuilderFactory({ - eventName: UserEvents.INVITE_CREATED, - source: Modules.USER, - action: CommonEvents.CREATED, - object: "invite", - })({ - data: serializedInvites, - sharedContext, - }) + >(invites) moduleEventBuilderFactory({ eventName: UserEvents.INVITE_TOKEN_GENERATED, @@ -390,14 +351,12 @@ export default class UserModuleService const serializedInvites = await this.baseRepository_.serialize< UserTypes.InviteDTO[] - >(updatedInvites, { - populate: true, - }) + >(updatedInvites) moduleEventBuilderFactory({ - eventName: UserEvents.INVITE_UPDATED, + eventName: UserEvents.INVITE_TOKEN_GENERATED, source: Modules.USER, - action: CommonEvents.UPDATED, + action: "token_generated", object: "invite", })({ data: serializedInvites,