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>
This commit is contained in:
Adrien de PerettiandOli Juhl authored and GitHub committed 2025-09-10 14:37:38 +02:00
1 parent afe21741c4
commit e8822f3e69
55 files changed
+3614 -2353

No files matched your search

+15
View File
@@ -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
@@ -9,12 +9,13 @@ import {
import { composeMessage } from "./build-event-messages" import { composeMessage } from "./build-event-messages"
export class MessageAggregator implements IMessageAggregator { export class MessageAggregator implements IMessageAggregator {
private messages: Message[] = [] #messagesHash: Set<string> = new Set()
#messages: Message[] = []
constructor() {} constructor() {}
count(): number { count(): number {
return this.messages.length return this.#messages.length
} }
save(msg: Message | Message[]): void { save(msg: Message | Message[]): void {
@@ -23,7 +24,19 @@ export class MessageAggregator implements IMessageAggregator {
return 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<T>( saveRawMessageData<T>(
@@ -55,15 +68,15 @@ export class MessageAggregator implements IMessageAggregator {
const { groupBy, sortBy } = format ?? {} const { groupBy, sortBy } = format ?? {}
if (sortBy) { 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[] } = { let messages: { [group: string]: Message[] } = {
default: [...this.messages], default: [...this.#messages],
} }
if (groupBy) { if (groupBy) {
messages = this.messages.reduce<{ messages = this.#messages.reduce<{
[key: string]: Message[] [key: string]: Message[]
}>((acc, msg) => { }>((acc, msg) => {
const key = groupBy const key = groupBy
@@ -91,7 +104,8 @@ export class MessageAggregator implements IMessageAggregator {
clearMessages(): void { clearMessages(): void {
// Ensure no references are left over in case something rely on messages // 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 { private getValueFromPath(obj: any, path: string): any {
@@ -20,7 +20,7 @@ describe("Internal Module Service Factory", () => {
findAndCount: jest.fn(), findAndCount: jest.fn(),
create: jest.fn(), create: jest.fn(),
update: jest.fn(), update: jest.fn(),
delete: jest.fn(), delete: jest.fn().mockImplementation((ids) => Promise.resolve(ids)),
softDelete: jest.fn(), softDelete: jest.fn(),
restore: jest.fn(), restore: jest.fn(),
upsert: jest.fn(), upsert: jest.fn(),
@@ -32,7 +32,7 @@ describe("Internal Module Service Factory", () => {
findAndCount: jest.fn(), findAndCount: jest.fn(),
create: jest.fn(), create: jest.fn(),
update: jest.fn(), update: jest.fn(),
delete: jest.fn(), delete: jest.fn().mockImplementation((ids) => Promise.resolve(ids)),
softDelete: jest.fn(), softDelete: jest.fn(),
restore: jest.fn(), restore: jest.fn(),
upsert: jest.fn(), upsert: jest.fn(),
@@ -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<any>,
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<T>(args: EventArgs<T>): Promise<void> {
this.#service.interceptEntityMutationEvents(
"afterCreate",
args,
this.#context
)
}
async afterUpdate<T>(args: EventArgs<T>): Promise<void> {
this.#service.interceptEntityMutationEvents(
"afterUpdate",
args,
this.#context
)
}
async afterUpsert<T>(args: EventArgs<T>): Promise<void> {
this.#service.interceptEntityMutationEvents(
"afterUpsert",
args,
this.#context
)
}
async afterDelete<T>(args: EventArgs<T>): Promise<void> {
this.#service.interceptEntityMutationEvents(
"afterDelete",
args,
this.#context
)
}
}
Object.defineProperty(klass, "name", {
value: keys.join(","),
writable: false,
})
return klass
}
@@ -21,3 +21,4 @@ export * from "./types/links-config"
export * from "./types/medusa-service" export * from "./types/medusa-service"
export * from "./module-provider-registration-key" export * from "./module-provider-registration-key"
export * from "./modules-to-container-types" export * from "./modules-to-container-types"
export * from "./create-medusa-mikro-orm-event-subscriber"
@@ -9,7 +9,12 @@ import {
PerformedActions, PerformedActions,
UpsertWithReplaceConfig, UpsertWithReplaceConfig,
} from "@medusajs/types" } from "@medusajs/types"
import type { EntityClass, EntitySchema } from "@mikro-orm/core" import {
EventType,
type EntityClass,
type EntityManager,
type EntitySchema,
} from "@mikro-orm/core"
import { import {
isDefined, isDefined,
isObject, isObject,
@@ -27,23 +32,60 @@ import {
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
} from "./decorators" } from "./decorators"
import { MedusaMikroOrmEventSubscriber } from "./create-medusa-mikro-orm-event-subscriber"
type InternalService = {
new <TContainer extends object = object, TEntity extends object = any>(
container: TContainer
): ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
setEventSubscriber(subscriber: MedusaMikroOrmEventSubscriber): void
}
type SelectorAndData = { type SelectorAndData = {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>> selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
data: any 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< export function MedusaInternalService<
TContainer extends object = object, TContainer extends object = object,
TEntity extends object = any TEntity extends object = any
>( >(rawModel: any): InternalService {
rawModel: any
): {
new (container: TContainer): ModulesSdkTypes.IMedusaInternalService<
TEntity,
TContainer
>
} {
const model = DmlEntity.isDmlEntity(rawModel) const model = DmlEntity.isDmlEntity(rawModel)
? toMikroORMEntity(rawModel) ? toMikroORMEntity(rawModel)
: rawModel : rawModel
@@ -54,6 +96,10 @@ export function MedusaInternalService<
class AbstractService_ class AbstractService_
implements ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer> implements ModulesSdkTypes.IMedusaInternalService<TEntity, TContainer>
{ {
[MedusaInternalServiceSymbol] = true
#eventSubscriber?: MedusaMikroOrmEventSubscriber
readonly __container__: TContainer; readonly __container__: TContainer;
[key: string]: any [key: string]: any
@@ -62,6 +108,10 @@ export function MedusaInternalService<
this[propertyRepositoryName] = container[injectedRepositoryName] this[propertyRepositoryName] = container[injectedRepositoryName]
} }
setEventSubscriber(subscriber: MedusaMikroOrmEventSubscriber) {
this.#eventSubscriber = subscriber
}
static applyFreeTextSearchFilter( static applyFreeTextSearchFilter(
filters: FilterQuery & { q?: string }, filters: FilterQuery & { q?: string },
config: FindConfig<any> config: FindConfig<any>
@@ -223,6 +273,11 @@ export function MedusaInternalService<
| InferEntityType<TEntity>[] | InferEntityType<TEntity>[]
} }
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
const entities = await this[propertyRepositoryName].create( const entities = await this[propertyRepositoryName].create(
data_, data_,
@@ -260,6 +315,11 @@ export function MedusaInternalService<
| InferEntityType<TEntity>[] | InferEntityType<TEntity>[]
} }
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
let shouldReturnArray = false let shouldReturnArray = false
if (Array.isArray(input)) { if (Array.isArray(input)) {
shouldReturnArray = true shouldReturnArray = true
@@ -419,6 +479,11 @@ export function MedusaInternalService<
return [] return []
} }
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
const primaryKeys = AbstractService_.retrievePrimaryKeys(model) const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
if ( if (
@@ -467,10 +532,29 @@ export function MedusaInternalService<
return [] return []
} }
return await this[propertyRepositoryName].delete( const deletedIds = await this[propertyRepositoryName].delete(
deleteCriteria, deleteCriteria,
sharedContext 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<typeof eventManager.dispatchEvent>[2],
})
})
}
return deletedIds
} }
@InjectTransactionManager(propertyRepositoryName) @InjectTransactionManager(propertyRepositoryName)
@@ -489,6 +573,11 @@ export function MedusaInternalService<
return [[], {}] return [[], {}]
} }
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
return await this[propertyRepositoryName].softDelete( return await this[propertyRepositoryName].softDelete(
idsOrFilter, idsOrFilter,
sharedContext sharedContext
@@ -520,6 +609,11 @@ export function MedusaInternalService<
data: any | any[], data: any | any[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<TEntity> | InferEntityType<TEntity>[]> { ): Promise<InferEntityType<TEntity> | InferEntityType<TEntity>[]> {
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
const entities = await this[propertyRepositoryName].upsert( const entities = await this[propertyRepositoryName].upsert(
data_, data_,
@@ -556,10 +650,74 @@ export function MedusaInternalService<
entities: InferEntityType<TEntity> | InferEntityType<TEntity>[] entities: InferEntityType<TEntity> | InferEntityType<TEntity>[]
performedActions: PerformedActions performedActions: PerformedActions
}> { }> {
registerInternalServiceEventSubscriber(
sharedContext,
this.#eventSubscriber
)
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
const { entities, performedActions } = await this[ const { entities, performedActions } = await this[
propertyRepositoryName propertyRepositoryName
].upsertWithReplace(data_, config, sharedContext) ].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<string, any[]>
).forEach(([modelName, entities]) => {
entities.forEach((entity) => {
eventManager.dispatchEvent(EventType.afterCreate, {
entity,
meta: {
className: modelName,
} as Parameters<typeof eventManager.dispatchEvent>[2],
})
})
})
}
if (updatedEntities) {
Object.entries(
performedActions.updated as Record<string, any[]>
).forEach(([modelName, entities]) => {
entities.forEach((entity) => {
eventManager.dispatchEvent(EventType.afterUpdate, {
entity,
meta: {
className: modelName,
} as Parameters<typeof eventManager.dispatchEvent>[2],
})
})
})
}
if (deletedEntities) {
Object.entries(
performedActions.deleted as Record<string, any[]>
).forEach(([modelName, entities]) => {
entities.forEach((entity) => {
eventManager.dispatchEvent(EventType.afterDelete, {
entity,
meta: {
className: modelName,
} as Parameters<typeof eventManager.dispatchEvent>[2],
})
})
})
}
return { return {
entities: Array.isArray(data) ? entities : entities[0], entities: Array.isArray(data) ? entities : entities[0],
performedActions, 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
} }
@@ -10,9 +10,9 @@ import {
RestoreReturn, RestoreReturn,
SoftDeleteReturn, SoftDeleteReturn,
} from "@medusajs/types" } from "@medusajs/types"
import { EventArgs } from "@mikro-orm/core"
import { import {
camelToSnakeCase, camelToSnakeCase,
isString,
lowerCaseFirst, lowerCaseFirst,
mapObjectTo, mapObjectTo,
MapToConfig, MapToConfig,
@@ -21,10 +21,12 @@ import {
} from "../common" } from "../common"
import { DmlEntity } from "../dml" import { DmlEntity } from "../dml"
import { CommonEvents } from "../event-bus" import { CommonEvents } from "../event-bus"
import { createMedusaMikroOrmEventSubscriber } from "./create-medusa-mikro-orm-event-subscriber"
import { EmitEvents, InjectManager, MedusaContext } from "./decorators" import { EmitEvents, InjectManager, MedusaContext } from "./decorators"
import { Modules } from "./definition" import { Modules } from "./definition"
import { moduleEventBuilderFactory } from "./event-builder-factory" import { moduleEventBuilderFactory } from "./event-builder-factory"
import { buildModelsNameToLinkableKeysMap } from "./joiner-config-builder" import { buildModelsNameToLinkableKeysMap } from "./joiner-config-builder"
import { isMedusaInternalService } from "./medusa-internal-service"
import { import {
BaseMethods, BaseMethods,
ExtractKeysFromConfig, ExtractKeysFromConfig,
@@ -137,37 +139,6 @@ export function MedusaService<
? ModelConfigurationsToConfigTemplate<TModels> ? ModelConfigurationsToConfigTemplate<TModels>
: ModelsConfig : ModelsConfig
> { > {
function emitSoftDeleteRestoreEvents(
this: AbstractModuleService_,
klassPrototype: any,
cascadedModelsMap: Record<string, string[]>,
action: string,
sharedContext: Context
) {
const joinerConfig = (
typeof this.__joinerConfig === "function"
? this.__joinerConfig()
: this.__joinerConfig
) as ModuleJoinerConfig
const emittedEntities = new Set<string>()
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 ( const buildAndAssignMethodImpl = function (
klassPrototype: any, klassPrototype: any,
method: string, method: string,
@@ -220,16 +191,9 @@ export function MedusaService<
sharedContext: Context = {} sharedContext: Context = {}
): Promise<T | T[]> { ): Promise<T | T[]> {
const service = this.__container__[serviceRegistrationName] const service = this.__container__[serviceRegistrationName]
const models = await service.create(data, sharedContext) const models_ = await service.create(data, sharedContext)
klassPrototype.aggregatedEvents.bind(this)({ return await this.baseRepository_.serialize<T | T[]>(models_)
action: CommonEvents.CREATED,
object: camelToSnakeCase(modelName).toLowerCase(),
data: models,
context: sharedContext,
})
return await this.baseRepository_.serialize<T | T[]>(models)
} }
applyMethod(methodImplementation, 1) applyMethod(methodImplementation, 1)
@@ -244,13 +208,6 @@ export function MedusaService<
const service = this.__container__[serviceRegistrationName] const service = this.__container__[serviceRegistrationName]
const response = await service.update(data, sharedContext) 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<T | T[]>(response) return await this.baseRepository_.serialize<T | T[]>(response)
} }
@@ -300,19 +257,10 @@ export function MedusaService<
? primaryKeyValues ? primaryKeyValues
: [primaryKeyValues] : [primaryKeyValues]
const ids = await this.__container__[serviceRegistrationName].delete( await this.__container__[serviceRegistrationName].delete(
primaryKeyValues_, primaryKeyValues_,
sharedContext 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) 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 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 return mappedCascadedModelsMap ? mappedCascadedModelsMap : void 0
} }
@@ -424,6 +354,41 @@ export function MedusaService<
this.__container__ = container this.__container__ = container
this.baseRepository_ = container.baseRepository 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( const hasEventBusModuleService = Object.keys(this.__container__).find(
(key) => key === Modules.EVENT_BUS (key) => key === Modules.EVENT_BUS
) )
@@ -433,9 +398,72 @@ export function MedusaService<
: undefined : undefined
this[MedusaServiceModelNameToLinkableKeysMapSymbol] = this[MedusaServiceModelNameToLinkableKeysMapSymbol] =
buildModelsNameToLinkableKeysMap( buildModelsNameToLinkableKeysMap(joinerConfig?.linkableKeys ?? {})
this.__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<any>, 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<any>,
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 }[] data: { id: any } | { id: any }[]
context: Context context: Context
}) { }) {
if (!context.messageAggregator) {
return
}
const __joinerConfig = ( const __joinerConfig = (
typeof this.__joinerConfig === "function" typeof this.__joinerConfig === "function"
? this.__joinerConfig() ? this.__joinerConfig()
@@ -3,13 +3,14 @@ import {
Context, Context,
FindConfig, FindConfig,
IDmlEntity, IDmlEntity,
InferEntityForModuleService,
InferEntityType,
Pluralize, Pluralize,
Prettify, Prettify,
RestoreReturn, RestoreReturn,
SoftDeleteReturn, SoftDeleteReturn,
InferEntityType,
InferEntityForModuleService,
} from "@medusajs/types" } from "@medusajs/types"
import { EventArgs } from "@mikro-orm/core"
import { DmlEntity } from "../../dml" import { DmlEntity } from "../../dml"
export type BaseMethods = export type BaseMethods =
@@ -318,4 +319,22 @@ export type MedusaServiceReturnType<ModelsConfig extends Record<string, any>> =
data: { id: any } | { id: any }[] data: { id: any } | { id: any }[]
context: Context context: Context
}): void }): 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<any>,
context: Context
): void
} }
@@ -12,6 +12,7 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
ApiKeyType, ApiKeyType,
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isObject, isObject,
@@ -65,11 +66,20 @@ export class ApiKeyModuleService
return joinerConfig return joinerConfig
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async deleteApiKeys( async deleteApiKeys(
ids: string | string[], ids: string | string[],
@MedusaContext() sharedContext: Context = {} @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] const apiKeyIds = Array.isArray(ids) ? ids : [ids]
@@ -111,6 +121,7 @@ export class ApiKeyModuleService
): Promise<ApiKeyTypes.ApiKeyDTO> ): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager() @InjectManager()
@EmitEvents()
//@ts-expect-error //@ts-expect-error
async createApiKeys( async createApiKeys(
data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[], data: ApiKeyTypes.CreateApiKeyDTO | ApiKeyTypes.CreateApiKeyDTO[],
@@ -183,9 +194,20 @@ export class ApiKeyModuleService
): Promise<ApiKeyTypes.ApiKeyDTO> ): Promise<ApiKeyTypes.ApiKeyDTO>
@InjectManager() @InjectManager()
@EmitEvents()
async upsertApiKeys( async upsertApiKeys(
data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[], data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const result = await this.upsertApiKeys_(data, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(result)
}
@InjectTransactionManager()
protected async upsertApiKeys_(
data: ApiKeyTypes.UpsertApiKeyDTO | ApiKeyTypes.UpsertApiKeyDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> { ): Promise<ApiKeyTypes.ApiKeyDTO | ApiKeyTypes.ApiKeyDTO[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
@@ -205,9 +227,7 @@ export class ApiKeyModuleService
) )
const serializedResponse = await this.baseRepository_.serialize< const serializedResponse = await this.baseRepository_.serialize<
ApiKeyTypes.ApiKeyDTO[] ApiKeyTypes.ApiKeyDTO[]
>(createdApiKeys, { >(createdApiKeys)
populate: true,
})
return serializedResponse.map( return serializedResponse.map(
(key) => (key) =>
@@ -226,10 +246,7 @@ export class ApiKeyModuleService
if (forUpdate.length) { if (forUpdate.length) {
const op = async () => { const op = async () => {
const updateResp = await this.updateApiKeys_(forUpdate, sharedContext) return await this.updateApiKeys_(forUpdate, sharedContext)
return await this.baseRepository_.serialize<ApiKeyTypes.ApiKeyDTO[]>(
updateResp
)
} }
operations.push(op()) operations.push(op())
@@ -253,6 +270,7 @@ export class ApiKeyModuleService
): Promise<ApiKeyTypes.ApiKeyDTO[]> ): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
//@ts-expect-error //@ts-expect-error
async updateApiKeys( async updateApiKeys(
idOrSelector: string | FilterableApiKeyProps, idOrSelector: string | FilterableApiKeyProps,
@@ -368,7 +386,9 @@ export class ApiKeyModuleService
data: ApiKeyTypes.RevokeApiKeyDTO, data: ApiKeyTypes.RevokeApiKeyDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ApiKeyTypes.ApiKeyDTO[]> ): Promise<ApiKeyTypes.ApiKeyDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async revoke( async revoke(
idOrSelector: string | FilterableApiKeyProps, idOrSelector: string | FilterableApiKeyProps,
data: ApiKeyTypes.RevokeApiKeyDTO, data: ApiKeyTypes.RevokeApiKeyDTO,
@@ -13,6 +13,7 @@ import {
createRawPropertiesFromBigNumber, createRawPropertiesFromBigNumber,
decorateCartTotals, decorateCartTotals,
deduplicate, deduplicate,
EmitEvents,
generateEntityId, generateEntityId,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
@@ -269,6 +270,7 @@ export default class CartModuleService
): Promise<CartTypes.CartDTO> ): Promise<CartTypes.CartDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createCarts( async createCarts(
data: CartTypes.CreateCartDTO[] | CartTypes.CreateCartDTO, data: CartTypes.CreateCartDTO[] | CartTypes.CreateCartDTO,
@@ -340,6 +342,7 @@ export default class CartModuleService
): Promise<CartTypes.CartDTO[]> ): Promise<CartTypes.CartDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateCarts( async updateCarts(
dataOrIdOrSelector: dataOrIdOrSelector:
@@ -425,6 +428,7 @@ export default class CartModuleService
): Promise<CartTypes.CartLineItemDTO[]> ): Promise<CartTypes.CartLineItemDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addLineItems( async addLineItems(
cartIdOrData: cartIdOrData:
| string | string
@@ -504,6 +508,7 @@ export default class CartModuleService
): Promise<CartTypes.CartLineItemDTO> ): Promise<CartTypes.CartLineItemDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateLineItems( async updateLineItems(
lineItemIdOrDataOrSelector: lineItemIdOrDataOrSelector:
@@ -606,6 +611,7 @@ export default class CartModuleService
): Promise<CartTypes.CartAddressDTO[]> ): Promise<CartTypes.CartAddressDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createAddresses( async createAddresses(
data: CartTypes.CreateAddressDTO[] | CartTypes.CreateAddressDTO, data: CartTypes.CreateAddressDTO[] | CartTypes.CreateAddressDTO,
@@ -645,6 +651,7 @@ export default class CartModuleService
): Promise<CartTypes.CartAddressDTO[]> ): Promise<CartTypes.CartAddressDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateAddresses( async updateAddresses(
data: CartTypes.UpdateAddressDTO[] | CartTypes.UpdateAddressDTO, data: CartTypes.UpdateAddressDTO[] | CartTypes.UpdateAddressDTO,
@@ -685,6 +692,7 @@ export default class CartModuleService
): Promise<CartTypes.CartShippingMethodDTO[]> ): Promise<CartTypes.CartShippingMethodDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addShippingMethods( async addShippingMethods(
cartIdOrData: cartIdOrData:
| string | string
@@ -763,6 +771,7 @@ export default class CartModuleService
): Promise<CartTypes.LineItemAdjustmentDTO[]> ): Promise<CartTypes.LineItemAdjustmentDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addLineItemAdjustments( async addLineItemAdjustments(
cartIdOrData: cartIdOrData:
| string | string
@@ -827,6 +836,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async upsertLineItemTaxLines( async upsertLineItemTaxLines(
taxLines: ( taxLines: (
| CartTypes.CreateLineItemTaxLineDTO | CartTypes.CreateLineItemTaxLineDTO
@@ -861,6 +871,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async upsertLineItemAdjustments( async upsertLineItemAdjustments(
adjustments: ( adjustments: (
| CartTypes.CreateLineItemAdjustmentDTO | CartTypes.CreateLineItemAdjustmentDTO
@@ -895,6 +906,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async upsertShippingMethodTaxLines( async upsertShippingMethodTaxLines(
taxLines: ( taxLines: (
| CartTypes.CreateShippingMethodTaxLineDTO | CartTypes.CreateShippingMethodTaxLineDTO
@@ -929,6 +941,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async upsertShippingMethodAdjustments( async upsertShippingMethodAdjustments(
adjustments: ( adjustments: (
| CartTypes.CreateShippingMethodAdjustmentDTO | CartTypes.CreateShippingMethodAdjustmentDTO
@@ -963,6 +976,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async setLineItemAdjustments( async setLineItemAdjustments(
cartId: string, cartId: string,
adjustments: ( adjustments: (
@@ -1036,6 +1050,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async setShippingMethodAdjustments( async setShippingMethodAdjustments(
cartId: string, cartId: string,
adjustments: ( adjustments: (
@@ -1122,6 +1137,7 @@ export default class CartModuleService
): Promise<CartTypes.ShippingMethodAdjustmentDTO[]> ): Promise<CartTypes.ShippingMethodAdjustmentDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addShippingMethodAdjustments( async addShippingMethodAdjustments(
cartIdOrData: cartIdOrData:
| string | string
@@ -1212,6 +1228,7 @@ export default class CartModuleService
): Promise<CartTypes.LineItemTaxLineDTO[]> ): Promise<CartTypes.LineItemTaxLineDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addLineItemTaxLines( async addLineItemTaxLines(
cartIdOrData: cartIdOrData:
| string | string
@@ -1275,6 +1292,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async setLineItemTaxLines( async setLineItemTaxLines(
cartId: string, cartId: string,
taxLines: ( taxLines: (
@@ -1357,6 +1375,7 @@ export default class CartModuleService
): Promise<CartTypes.ShippingMethodTaxLineDTO[]> ): Promise<CartTypes.ShippingMethodTaxLineDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async addShippingMethodTaxLines( async addShippingMethodTaxLines(
cartIdOrData: cartIdOrData:
| string | string
@@ -1422,6 +1441,7 @@ export default class CartModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async setShippingMethodTaxLines( async setShippingMethodTaxLines(
cartId: string, cartId: string,
taxLines: ( taxLines: (
@@ -14,13 +14,13 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isString, isString,
MedusaContext, MedusaContext,
MedusaService, MedusaService,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { EntityManager } from "@mikro-orm/core"
import { import {
Customer, Customer,
CustomerAddress, CustomerAddress,
@@ -101,6 +101,7 @@ export default class CustomerModuleService
): Promise<CustomerTypes.CustomerDTO[]> ): Promise<CustomerTypes.CustomerDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async createCustomers( async createCustomers(
dataOrArray: dataOrArray:
| CustomerTypes.CreateCustomerDTO | CustomerTypes.CreateCustomerDTO
@@ -124,7 +125,7 @@ export default class CustomerModuleService
| CustomerTypes.CreateCustomerDTO | CustomerTypes.CreateCustomerDTO
| CustomerTypes.CreateCustomerDTO[], | CustomerTypes.CreateCustomerDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<CustomerTypes.CustomerDTO[]> { ): Promise<InferEntityType<typeof Customer>[]> {
const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray]
const customerAttributes = data.map(({ addresses, ...rest }) => { const customerAttributes = data.map(({ addresses, ...rest }) => {
return rest return rest
@@ -153,7 +154,7 @@ export default class CustomerModuleService
sharedContext sharedContext
) )
return customers as unknown as CustomerTypes.CustomerDTO[] return customers
} }
// @ts-expect-error // @ts-expect-error
@@ -175,13 +176,33 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<CustomerTypes.CustomerDTO[]> ): Promise<CustomerTypes.CustomerDTO[]>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateCustomers( async updateCustomers(
idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps, idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps,
data: CustomerTypes.CustomerUpdatableFields, data: CustomerTypes.CustomerUpdatableFields,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[]> {
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<typeof Customer>[] | InferEntityType<typeof Customer>
> {
let updateData: let updateData:
| CustomerTypes.UpdateCustomerDTO | CustomerTypes.UpdateCustomerDTO
| CustomerTypes.UpdateCustomerDTO[] | CustomerTypes.UpdateCustomerDTO[]
@@ -212,13 +233,7 @@ export default class CustomerModuleService
sharedContext sharedContext
) )
const serialized = await this.baseRepository_.serialize< return customers
CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[]
>(customers, {
populate: true,
})
return serialized
} }
// @ts-expect-error // @ts-expect-error
@@ -233,24 +248,43 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<CustomerTypes.CustomerGroupDTO[]> ): Promise<CustomerTypes.CustomerGroupDTO[]>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createCustomerGroups( async createCustomerGroups(
dataOrArrayOfData: dataOrArrayOfData:
| CustomerTypes.CreateCustomerGroupDTO | CustomerTypes.CreateCustomerGroupDTO
| CustomerTypes.CreateCustomerGroupDTO[], | CustomerTypes.CreateCustomerGroupDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ): Promise<
const groups = await this.customerGroupService_.create( CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[]
> {
const groups = await this.createCustomerGroups_(
dataOrArrayOfData, dataOrArrayOfData,
sharedContext sharedContext
) )
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[]
>(groups, { >(groups)
populate: true, }
})
@InjectTransactionManager()
protected async createCustomerGroups_(
dataOrArrayOfData:
| CustomerTypes.CreateCustomerGroupDTO
| CustomerTypes.CreateCustomerGroupDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
| InferEntityType<typeof CustomerGroup>[]
| InferEntityType<typeof CustomerGroup>
> {
const groups = await this.customerGroupService_.create(
dataOrArrayOfData,
sharedContext
)
return groups
} }
// @ts-expect-error // @ts-expect-error
@@ -272,7 +306,8 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<CustomerTypes.CustomerGroupDTO[]> ): Promise<CustomerTypes.CustomerGroupDTO[]>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateCustomerGroups( async updateCustomerGroups(
groupIdOrSelector: groupIdOrSelector:
@@ -281,7 +316,32 @@ export default class CustomerModuleService
| CustomerTypes.FilterableCustomerGroupProps, | CustomerTypes.FilterableCustomerGroupProps,
data: CustomerTypes.CustomerGroupUpdatableFields, data: CustomerTypes.CustomerGroupUpdatableFields,
@MedusaContext() sharedContext: Context = {} @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<typeof CustomerGroup>[]
| InferEntityType<typeof CustomerGroup>
> {
let updateData: let updateData:
| CustomerTypes.UpdateCustomerGroupDTO | CustomerTypes.UpdateCustomerGroupDTO
| CustomerTypes.UpdateCustomerGroupDTO[] | CustomerTypes.UpdateCustomerGroupDTO[]
@@ -311,15 +371,10 @@ export default class CustomerModuleService
) )
if (isString(groupIdOrSelector)) { if (isString(groupIdOrSelector)) {
return await this.baseRepository_.serialize<CustomerTypes.CustomerGroupDTO>( return groups[0]
groups[0],
{ populate: true }
)
} }
return await this.baseRepository_.serialize< return groups
CustomerTypes.CustomerGroupDTO[]
>(groups, { populate: true })
} }
async addCustomerToGroup( async addCustomerToGroup(
@@ -332,10 +387,21 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<{ id: string }[]> ): Promise<{ id: string }[]>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async addCustomerToGroup( async addCustomerToGroup(
data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[],
@MedusaContext() sharedContext: Context = {} @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 }[]> { ): Promise<{ id: string } | { id: string }[]> {
const groupCustomers = await this.customerGroupCustomerService_.create( const groupCustomers = await this.customerGroupCustomerService_.create(
data, data,
@@ -365,6 +431,7 @@ export default class CustomerModuleService
): Promise<CustomerTypes.CustomerAddressDTO> ): Promise<CustomerTypes.CustomerAddressDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createCustomerAddresses( async createCustomerAddresses(
data: data:
@@ -419,7 +486,8 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<CustomerTypes.CustomerAddressDTO[]> ): Promise<CustomerTypes.CustomerAddressDTO[]>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateCustomerAddresses( async updateCustomerAddresses(
addressIdOrSelector: addressIdOrSelector:
@@ -428,7 +496,32 @@ export default class CustomerModuleService
| CustomerTypes.FilterableCustomerAddressProps, | CustomerTypes.FilterableCustomerAddressProps,
data: CustomerTypes.UpdateCustomerAddressDTO, data: CustomerTypes.UpdateCustomerAddressDTO,
@MedusaContext() sharedContext: Context = {} @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<typeof CustomerAddress>[]
| InferEntityType<typeof CustomerAddress>
> {
let updateData: let updateData:
| CustomerTypes.UpdateCustomerAddressDTO[] | CustomerTypes.UpdateCustomerAddressDTO[]
| { | {
@@ -459,17 +552,11 @@ export default class CustomerModuleService
sharedContext sharedContext
) )
await this.flush(sharedContext)
const serialized = await this.baseRepository_.serialize<
CustomerTypes.CustomerAddressDTO[]
>(addresses, { populate: true })
if (isString(addressIdOrSelector)) { if (isString(addressIdOrSelector)) {
return serialized[0] return addresses[0]
} }
return serialized return addresses
} }
async removeCustomerFromGroup( async removeCustomerFromGroup(
@@ -481,10 +568,19 @@ export default class CustomerModuleService
sharedContext?: Context sharedContext?: Context
): Promise<void> ): Promise<void>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async removeCustomerFromGroup( async removeCustomerFromGroup(
data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[], data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.removeCustomerFromGroup_(data, sharedContext)
}
@InjectTransactionManager()
protected async removeCustomerFromGroup_(
data: CustomerTypes.GroupCustomerPair | CustomerTypes.GroupCustomerPair[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
const pairs = Array.isArray(data) ? data : [data] const pairs = Array.isArray(data) ? data : [data]
const groupCustomers = await this.customerGroupCustomerService_.list({ const groupCustomers = await this.customerGroupCustomerService_.list({
@@ -495,9 +591,4 @@ export default class CustomerModuleService
sharedContext sharedContext
) )
} }
private async flush(context: Context) {
const em = (context.manager ?? context.transactionManager) as EntityManager
await em.flush()
}
} }
@@ -219,7 +219,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([fulfillmentSets[i].id]), id: fulfillmentSets[i].id,
}, },
}), }),
]), ]),
@@ -338,7 +338,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([fulfillmentSets[i].id]), id: fulfillmentSets[i].id,
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -346,9 +346,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "service_zone", object: "service_zone",
data: { data: {
id: expect.arrayContaining([ id: fulfillmentSets[i].service_zones[0].id,
fulfillmentSets[i].service_zones[0].id,
]),
}, },
}), }),
]), ]),
@@ -514,7 +512,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([fulfillmentSets[i].id]), id: fulfillmentSets[i].id,
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -522,9 +520,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "service_zone", object: "service_zone",
data: { data: {
id: expect.arrayContaining([ id: fulfillmentSets[i].service_zones[0].id,
fulfillmentSets[i].service_zones[0].id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -532,9 +528,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "geo_zone", object: "geo_zone",
data: { data: {
id: expect.arrayContaining([ id: fulfillmentSets[i].service_zones[0].geo_zones[0].id,
fulfillmentSets[i].service_zones[0].geo_zones[0].id,
]),
}, },
}), }),
]), ]),
@@ -724,7 +718,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
expect(updatedFulfillmentSets).toHaveLength(2) 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) { for (const data_ of updateData) {
const currentFullfillmentSet = fullfillmentSets.find( const currentFullfillmentSet = fullfillmentSets.find(
@@ -746,7 +740,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "updated", action: "updated",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([currentFullfillmentSet.id]), id: currentFullfillmentSet.id,
}, },
}), }),
]), ]),
@@ -1075,7 +1069,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
) )
expect(updatedFulfillmentSets).toHaveLength(2) 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) { for (const data_ of updateData) {
const expectedFulfillmentSet = updatedFulfillmentSets.find( const expectedFulfillmentSet = updatedFulfillmentSets.find(
@@ -1116,7 +1110,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "updated", action: "updated",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([expectedFulfillmentSet.id]), id: expectedFulfillmentSet.id,
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1124,9 +1118,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "service_zone", object: "service_zone",
data: { data: {
id: expect.arrayContaining([ id: expectedFulfillmentSet.service_zones[0].id,
expectedFulfillmentSet.service_zones[0].id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1134,9 +1126,8 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "geo_zone", object: "geo_zone",
data: { data: {
id: expect.arrayContaining([ id: expectedFulfillmentSet.service_zones[0].geo_zones[0]
expectedFulfillmentSet.service_zones[0].geo_zones[0].id, .id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1144,9 +1135,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "deleted", action: "deleted",
object: "service_zone", object: "service_zone",
data: { data: {
id: expect.arrayContaining([ id: originalFulfillmentSet.service_zones[0].id,
originalFulfillmentSet.service_zones[0].id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1154,9 +1143,8 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "deleted", action: "deleted",
object: "geo_zone", object: "geo_zone",
data: { data: {
id: expect.arrayContaining([ id: originalFulfillmentSet.service_zones[0].geo_zones[0]
originalFulfillmentSet.service_zones[0].geo_zones[0].id, .id,
]),
}, },
}), }),
]), ]),
@@ -1245,7 +1233,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
) )
expect(updatedFulfillmentSets).toHaveLength(2) 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) { for (const data_ of updateData) {
const expectedFulfillmentSet = updatedFulfillmentSets.find( const expectedFulfillmentSet = updatedFulfillmentSets.find(
@@ -1290,7 +1278,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "updated", action: "updated",
object: "fulfillment_set", object: "fulfillment_set",
data: { data: {
id: expect.arrayContaining([expectedFulfillmentSet.id]), id: expectedFulfillmentSet.id,
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1298,7 +1286,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "service_zone", object: "service_zone",
data: { data: {
id: expect.arrayContaining([createdServiceZone.id]), id: createdServiceZone.id,
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -1306,9 +1294,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "geo_zone", object: "geo_zone",
data: { data: {
id: expect.arrayContaining([ id: createdServiceZone.geo_zones[0].id,
createdServiceZone.geo_zones[0].id,
]),
}, },
}), }),
]), ]),
@@ -152,15 +152,21 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
[ expect.arrayContaining([
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_CREATED, eventName: FulfillmentEvents.FULFILLMENT_CREATED,
action: "created", action: "created",
object: "fulfillment", object: "fulfillment",
data: { id: fulfillment.id }, data: { id: fulfillment.id },
}), }),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_UPDATED,
action: "updated",
object: "fulfillment",
data: { id: fulfillment.id },
}),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED, eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED,
action: "created", action: "created",
@@ -179,7 +185,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "fulfillment_label", object: "fulfillment_label",
data: { id: fulfillment.labels[0].id }, data: { id: fulfillment.labels[0].id },
}), }),
], ]),
{ {
internal: true, internal: true,
} }
@@ -244,15 +250,21 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
[ expect.arrayContaining([
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_CREATED, eventName: FulfillmentEvents.FULFILLMENT_CREATED,
action: "created", action: "created",
object: "fulfillment", object: "fulfillment",
data: { id: fulfillment.id }, data: { id: fulfillment.id },
}), }),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_UPDATED,
action: "updated",
object: "fulfillment",
data: { id: fulfillment.id },
}),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED, eventName: FulfillmentEvents.FULFILLMENT_ADDRESS_CREATED,
action: "created", action: "created",
@@ -271,7 +283,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "fulfillment_label", object: "fulfillment_label",
data: { id: fulfillment.labels[0].id }, data: { id: fulfillment.labels[0].id },
}), }),
], ]),
{ {
internal: true, internal: true,
} }
@@ -383,15 +395,9 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
[ expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_UPDATED,
action: "updated",
object: "fulfillment",
data: { id: updatedFulfillment.id },
}),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.FULFILLMENT_LABEL_DELETED, eventName: FulfillmentEvents.FULFILLMENT_LABEL_DELETED,
action: "deleted", action: "deleted",
@@ -410,7 +416,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "fulfillment_label", object: "fulfillment_label",
data: { id: label4.id }, data: { id: label4.id },
}), }),
], ]),
{ {
internal: true, internal: true,
} }
@@ -154,7 +154,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
const geoZones = await service.createGeoZones(data) const geoZones = await service.createGeoZones(data)
expect(geoZones).toHaveLength(2) expect(geoZones).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
let i = 0 let i = 0
for (const data_ of data) { for (const data_ of data) {
@@ -172,7 +172,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
eventName: FulfillmentEvents.GEO_ZONE_CREATED, eventName: FulfillmentEvents.GEO_ZONE_CREATED,
action: "created", action: "created",
object: "geo_zone", object: "geo_zone",
data: { id: expect.arrayContaining([geoZones[i].id]) }, data: { id: geoZones[i].id },
}), }),
]), ]),
{ {
@@ -331,7 +331,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
const updatedGeoZones = await service.updateGeoZones(updateData) const updatedGeoZones = await service.updateGeoZones(updateData)
expect(updatedGeoZones).toHaveLength(2) 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) { for (const data_ of updateData) {
const expectedGeoZone = updatedGeoZones.find( const expectedGeoZone = updatedGeoZones.find(
@@ -352,7 +352,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
eventName: FulfillmentEvents.GEO_ZONE_UPDATED, eventName: FulfillmentEvents.GEO_ZONE_UPDATED,
action: "updated", action: "updated",
object: "geo_zone", object: "geo_zone",
data: { id: expect.arrayContaining([expectedGeoZone.id]) }, data: { id: expectedGeoZone.id },
}), }),
]), ]),
{ {
@@ -371,7 +371,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
[ expect.arrayContaining([
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.GEO_ZONE_DELETED, eventName: FulfillmentEvents.GEO_ZONE_DELETED,
action: "deleted", action: "deleted",
@@ -396,7 +396,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "geo_zone", object: "geo_zone",
data: { id: usGeoZone.id }, data: { id: usGeoZone.id },
}), }),
], ]),
{ {
internal: true, internal: true,
} }
@@ -510,7 +510,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
expect(updatedServiceZones).toHaveLength(2) 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) { for (const data_ of updateData) {
const expectedServiceZone = updatedServiceZones.find( const expectedServiceZone = updatedServiceZones.find(
@@ -522,7 +522,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
[ expect.arrayContaining([
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.SHIPPING_OPTION_CREATED, eventName: FulfillmentEvents.SHIPPING_OPTION_CREATED,
action: "created", action: "created",
@@ -541,7 +541,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "shipping_option_rule", object: "shipping_option_rule",
data: { id: createdShippingOption.rules[0].id }, data: { id: createdShippingOption.rules[0].id },
}), }),
], ]),
{ {
internal: true, internal: true,
} }
@@ -582,7 +582,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
) )
expect(createdShippingOptions).toHaveLength(2) expect(createdShippingOptions).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(6)
let i = 0 let i = 0
for (const data_ of createData) { for (const data_ of createData) {
@@ -620,9 +620,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "shipping_option", object: "shipping_option",
data: { data: {
id: expect.arrayContaining([ id: createdShippingOptions[i].id,
createdShippingOptions[i].id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -630,9 +628,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "shipping_option_type", object: "shipping_option_type",
data: { data: {
id: expect.arrayContaining([ id: createdShippingOptions[i].type.id,
createdShippingOptions[i].type.id,
]),
}, },
}), }),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -640,9 +636,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "shipping_option_rule", object: "shipping_option_rule",
data: { data: {
id: expect.arrayContaining([ id: createdShippingOptions[i].rules[0].id,
createdShippingOptions[i].rules[0].id,
]),
}, },
}), }),
]), ]),
@@ -823,7 +817,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([ expect.arrayContaining([
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
@@ -832,12 +826,6 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
object: "shipping_option", object: "shipping_option",
data: { id: updatedShippingOption.id }, data: { id: updatedShippingOption.id },
}), }),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.SHIPPING_OPTION_TYPE_DELETED,
action: "deleted",
object: "shipping_option_type",
data: { id: shippingOption.type.id },
}),
buildExpectedEventMessageShape({ buildExpectedEventMessageShape({
eventName: FulfillmentEvents.SHIPPING_OPTION_TYPE_CREATED, eventName: FulfillmentEvents.SHIPPING_OPTION_TYPE_CREATED,
action: "created", action: "created",
@@ -1374,14 +1362,16 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
// Test with full address including postal expression // Test with full address including postal expression
const shippingOptions = await service.listShippingOptionsForContext({ const shippingOptions = await service.listShippingOptionsForContext(
address: { {
country_code: "US", address: {
province_code: "CA", country_code: "US",
city: "Los Angeles", province_code: "CA",
postal_expression: "90210", city: "Los Angeles",
}, postal_expression: "90210",
}) },
}
)
expect(shippingOptions).toHaveLength(1) expect(shippingOptions).toHaveLength(1)
}) })
@@ -1419,13 +1409,15 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
// Test with city but no postal expression // Test with city but no postal expression
const shippingOptions = await service.listShippingOptionsForContext({ const shippingOptions = await service.listShippingOptionsForContext(
address: { {
country_code: "US", address: {
province_code: "CA", country_code: "US",
city: "San Francisco", province_code: "CA",
}, city: "San Francisco",
}) },
}
)
expect(shippingOptions).toHaveLength(1) expect(shippingOptions).toHaveLength(1)
}) })
@@ -1462,12 +1454,14 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
// Test with province but no city // Test with province but no city
const shippingOptions = await service.listShippingOptionsForContext({ const shippingOptions = await service.listShippingOptionsForContext(
address: { {
country_code: "US", address: {
province_code: "NY", country_code: "US",
}, province_code: "NY",
}) },
}
)
expect(shippingOptions).toHaveLength(1) expect(shippingOptions).toHaveLength(1)
}) })
@@ -1503,11 +1497,13 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
// Test with only country // Test with only country
const shippingOptions = await service.listShippingOptionsForContext({ const shippingOptions = await service.listShippingOptionsForContext(
address: { {
country_code: "CA", address: {
}, country_code: "CA",
}) },
}
)
expect(shippingOptions).toHaveLength(1) expect(shippingOptions).toHaveLength(1)
}) })
@@ -1758,14 +1754,16 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
]) ])
// Address with undefined fields should still match if country matches // Address with undefined fields should still match if country matches
const shippingOptions = await service.listShippingOptionsForContext({ const shippingOptions = await service.listShippingOptionsForContext(
address: { {
country_code: "US", address: {
province_code: undefined, country_code: "US",
city: undefined, province_code: undefined,
postal_expression: undefined, city: undefined,
}, postal_expression: undefined,
}) },
}
)
expect(shippingOptions).toHaveLength(1) expect(shippingOptions).toHaveLength(1)
}) })
@@ -1831,26 +1829,27 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
// Create shipping options for each zone // Create shipping options for each zone
const [usOption, europeOption, canadaOption] = await service.createShippingOptions([ const [usOption, europeOption, canadaOption] =
generateCreateShippingOptionsData({ await service.createShippingOptions([
name: "US Shipping", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[0].id, name: "US Shipping",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[0].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
generateCreateShippingOptionsData({ }),
name: "Europe Shipping", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[1].id, name: "Europe Shipping",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[1].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
generateCreateShippingOptionsData({ }),
name: "Canada Shipping", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[2].id, name: "Canada Shipping",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[2].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
]) }),
])
// Test US ZIP code - should only match US zone // Test US ZIP code - should only match US zone
let shippingOptions = await service.listShippingOptionsForContext({ let shippingOptions = await service.listShippingOptionsForContext({
@@ -1970,26 +1969,27 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}) })
// Create shipping options with different prices for each zone // Create shipping options with different prices for each zone
const [broadOption, californiaOption, laOption] = await service.createShippingOptions([ const [broadOption, californiaOption, laOption] =
generateCreateShippingOptionsData({ await service.createShippingOptions([
name: "Standard US Shipping", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[0].id, name: "Standard US Shipping",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[0].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
generateCreateShippingOptionsData({ }),
name: "California Express", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[1].id, name: "California Express",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[1].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
generateCreateShippingOptionsData({ }),
name: "LA Premium", generateCreateShippingOptionsData({
service_zone_id: fulfillmentSet.service_zones[2].id, name: "LA Premium",
shipping_profile_id: shippingProfile.id, service_zone_id: fulfillmentSet.service_zones[2].id,
provider_id: providerId, shipping_profile_id: shippingProfile.id,
}), provider_id: providerId,
]) }),
])
// Test LA ZIP code - should match all three zones // Test LA ZIP code - should match all three zones
let shippingOptions = await service.listShippingOptionsForContext({ let shippingOptions = await service.listShippingOptionsForContext({
@@ -2001,7 +2001,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}, },
}) })
expect(shippingOptions).toHaveLength(3) 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(broadOption.id)
expect(laIds).toContain(californiaOption.id) expect(laIds).toContain(californiaOption.id)
expect(laIds).toContain(laOption.id) expect(laIds).toContain(laOption.id)
@@ -2015,7 +2015,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
}, },
}) })
expect(shippingOptions).toHaveLength(2) 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(broadOption.id)
expect(caIds).toContain(californiaOption.id) expect(caIds).toContain(californiaOption.id)
expect(caIds).not.toContain(laOption.id) expect(caIds).not.toContain(laOption.id)
@@ -100,7 +100,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
await service.createShippingProfiles(createData) await service.createShippingProfiles(createData)
expect(createdShippingProfiles).toHaveLength(2) expect(createdShippingProfiles).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
let i = 0 let i = 0
for (const data_ of createData) { for (const data_ of createData) {
@@ -118,9 +118,7 @@ moduleIntegrationTestRunner<IFulfillmentModuleService>({
action: "created", action: "created",
object: "shipping_profile", object: "shipping_profile",
data: { data: {
id: expect.arrayContaining([ id: createdShippingProfiles[i].id,
createdShippingProfiles[i].id,
]),
}, },
}), }),
]), ]),
@@ -47,18 +47,9 @@ import {
ShippingOptionType, ShippingOptionType,
ShippingProfile, ShippingProfile,
} from "@models" } from "@models"
import { import { isContextValid, Rule, validateAndNormalizeRules } from "@utils"
buildCreatedFulfillmentEvents,
buildCreatedFulfillmentSetEvents,
buildCreatedServiceZoneEvents,
eventBuilders,
isContextValid,
Rule,
validateAndNormalizeRules,
} from "@utils"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
import { UpdateShippingOptionsInput } from "../types/service" import { UpdateShippingOptionsInput } from "../types/service"
import { buildCreatedShippingOptionEvents } from "../utils/events"
import FulfillmentProviderService from "./fulfillment-provider" import FulfillmentProviderService from "./fulfillment-provider"
const generateMethodForModels = { const generateMethodForModels = {
@@ -70,7 +61,7 @@ const generateMethodForModels = {
ShippingOptionRule, ShippingOptionRule,
ShippingOptionType, ShippingOptionType,
FulfillmentProvider, 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 = { type InjectedDependencies = {
@@ -339,11 +330,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
buildCreatedFulfillmentSetEvents({
fulfillmentSets: createdFulfillmentSets,
sharedContext,
})
return createdFulfillmentSets return createdFulfillmentSets
} }
@@ -405,11 +391,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
buildCreatedServiceZoneEvents({
serviceZones: createdServiceZones,
sharedContext,
})
return createdServiceZones return createdServiceZones
} }
@@ -468,11 +449,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
buildCreatedShippingOptionEvents({
shippingOptions: createdSO,
sharedContext,
})
return createdSO return createdSO
} }
@@ -487,7 +463,7 @@ export default class FulfillmentModuleService
sharedContext?: Context sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO> ): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
// @ts-expect-error // @ts-expect-error
async createShippingProfiles( async createShippingProfiles(
@@ -503,11 +479,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
eventBuilders.createdShippingProfile({
data: createdShippingProfiles,
sharedContext,
})
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO
| FulfillmentTypes.ShippingProfileDTO[] | FulfillmentTypes.ShippingProfileDTO[]
@@ -561,11 +532,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
eventBuilders.createdGeoZone({
data: createdGeoZones,
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>( return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
Array.isArray(data) ? createdGeoZones : createdGeoZones[0] Array.isArray(data) ? createdGeoZones : createdGeoZones[0]
) )
@@ -629,11 +595,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
eventBuilders.createdShippingOptionRule({
data: createdSORules.map((sor) => ({ id: sor.id })),
sharedContext,
})
return createdSORules return createdSORules
} }
@@ -679,11 +640,6 @@ export default class FulfillmentModuleService
throw error throw error
} }
buildCreatedFulfillmentEvents({
fulfillments: [fulfillment],
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>( return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment fulfillment
) )
@@ -754,11 +710,6 @@ export default class FulfillmentModuleService
throw error throw error
} }
buildCreatedFulfillmentEvents({
fulfillments: [fulfillment],
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>( return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment fulfillment
) )
@@ -943,15 +894,6 @@ export default class FulfillmentModuleService
}) })
if (serviceZoneIdsToDelete.length) { if (serviceZoneIdsToDelete.length) {
eventBuilders.deletedServiceZone({
data: serviceZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await promiseAll([ await promiseAll([
this.geoZoneService_.delete( this.geoZoneService_.delete(
{ {
@@ -973,32 +915,6 @@ export default class FulfillmentModuleService
sharedContext 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) return Array.isArray(data)
? updatedFulfillmentSets ? updatedFulfillmentSets
: updatedFulfillmentSets[0] : updatedFulfillmentSets[0]
@@ -1187,11 +1103,6 @@ export default class FulfillmentModuleService
}) })
if (geoZoneIdsToDelete.length) { if (geoZoneIdsToDelete.length) {
eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await this.geoZoneService_.delete( await this.geoZoneService_.delete(
{ {
id: geoZoneIdsToDelete, id: geoZoneIdsToDelete,
@@ -1205,26 +1116,6 @@ export default class FulfillmentModuleService
sharedContext 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] return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0]
} }
@@ -1485,11 +1376,6 @@ export default class FulfillmentModuleService
}) })
if (ruleIdsToDelete.length) { if (ruleIdsToDelete.length) {
eventBuilders.deletedShippingOptionRule({
data: ruleIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await this.shippingOptionRuleService_.delete( await this.shippingOptionRuleService_.delete(
ruleIdsToDelete, ruleIdsToDelete,
sharedContext sharedContext
@@ -1501,71 +1387,11 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
this.handleShippingOptionUpdateEvents({
shippingOptionsData: dataArray,
updatedShippingOptions,
optionTypeDeletedIds,
updatedRuleIds,
existingRuleIds,
sharedContext,
})
return Array.isArray(data) return Array.isArray(data)
? updatedShippingOptions ? updatedShippingOptions
: updatedShippingOptions[0] : 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( async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO[], data: FulfillmentTypes.UpsertShippingOptionDTO[],
sharedContext?: Context sharedContext?: Context
@@ -1652,7 +1478,8 @@ export default class FulfillmentModuleService
sharedContext?: Context sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionTypeDTO> ): Promise<FulfillmentTypes.ShippingOptionTypeDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async upsertShippingOptionTypes( async upsertShippingOptionTypes(
data: data:
| FulfillmentTypes.UpsertShippingOptionTypeDTO[] | FulfillmentTypes.UpsertShippingOptionTypeDTO[]
@@ -1662,12 +1489,7 @@ export default class FulfillmentModuleService
| FulfillmentTypes.ShippingOptionTypeDTO[] | FulfillmentTypes.ShippingOptionTypeDTO[]
| FulfillmentTypes.ShippingOptionTypeDTO | FulfillmentTypes.ShippingOptionTypeDTO
> { > {
const input = Array.isArray(data) ? data : [data] const results = await this.updateShippingOptionTypes_(data, sharedContext)
const results = await this.shippingOptionTypeService_.upsert(
input,
sharedContext
)
const allTypes = await this.baseRepository_.serialize< const allTypes = await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionTypeDTO[] | FulfillmentTypes.ShippingOptionTypeDTO[]
@@ -1677,6 +1499,23 @@ export default class FulfillmentModuleService
return Array.isArray(data) ? allTypes : allTypes[0] return Array.isArray(data) ? allTypes : allTypes[0]
} }
@InjectTransactionManager()
protected async updateShippingOptionTypes_(
data:
| FulfillmentTypes.UpsertShippingOptionTypeDTO[]
| FulfillmentTypes.UpsertShippingOptionTypeDTO,
sharedContext: Context
): Promise<InferEntityType<typeof ShippingOptionType>[]> {
const input = Array.isArray(data) ? data : [data]
const results = await this.shippingOptionTypeService_.upsert(
input,
sharedContext
)
return results
}
// @ts-expect-error // @ts-expect-error
updateShippingOptionTypes( updateShippingOptionTypes(
id: string, id: string,
@@ -1691,6 +1530,7 @@ export default class FulfillmentModuleService
): Promise<FulfillmentTypes.ShippingOptionTypeDTO[]> ): Promise<FulfillmentTypes.ShippingOptionTypeDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateShippingOptionTypes( async updateShippingOptionTypes(
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionTypeProps, idOrSelector: string | FulfillmentTypes.FilterableShippingOptionTypeProps,
@@ -1747,7 +1587,8 @@ export default class FulfillmentModuleService
sharedContext?: Context sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO> ): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateShippingProfiles( async updateShippingProfiles(
idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps, idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps,
@@ -1756,6 +1597,25 @@ export default class FulfillmentModuleService
): Promise< ): Promise<
FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[] 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<InferEntityType<typeof ShippingProfile>[]> {
let normalizedInput: ({ let normalizedInput: ({
id: string id: string
} & FulfillmentTypes.UpdateShippingProfileDTO)[] = [] } & FulfillmentTypes.UpdateShippingProfileDTO)[] = []
@@ -1784,11 +1644,7 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
const updatedProfiles = await this.baseRepository_.serialize< return profiles
FulfillmentTypes.ShippingProfileDTO[]
>(profiles)
return isString(idOrSelector) ? updatedProfiles[0] : updatedProfiles
} }
async upsertShippingProfiles( async upsertShippingProfiles(
@@ -1800,7 +1656,8 @@ export default class FulfillmentModuleService
sharedContext?: Context sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO> ): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async upsertShippingProfiles( async upsertShippingProfiles(
data: data:
| FulfillmentTypes.UpsertShippingProfileDTO[] | FulfillmentTypes.UpsertShippingProfileDTO[]
@@ -1808,6 +1665,24 @@ export default class FulfillmentModuleService
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<
FulfillmentTypes.ShippingProfileDTO[] | FulfillmentTypes.ShippingProfileDTO 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<typeof ShippingProfile>[]
| InferEntityType<typeof ShippingProfile>
> { > {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter((prof) => !!prof.id) const forUpdate = input.filter((prof) => !!prof.id)
@@ -1831,13 +1706,7 @@ export default class FulfillmentModuleService
) )
} }
const result = [...created, ...updated] return [...created, ...updated]
const allProfiles = await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingProfileDTO[]
| FulfillmentTypes.ShippingProfileDTO
>(result)
return Array.isArray(data) ? allProfiles : allProfiles[0]
} }
// @ts-expect-error // @ts-expect-error
@@ -1873,11 +1742,6 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
eventBuilders.updatedGeoZone({
data: updatedGeoZones,
sharedContext,
})
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.GeoZoneDTO[] FulfillmentTypes.GeoZoneDTO[]
>(updatedGeoZones) >(updatedGeoZones)
@@ -1940,11 +1804,6 @@ export default class FulfillmentModuleService
const updatedShippingOptionRules = const updatedShippingOptionRules =
await this.shippingOptionRuleService_.update(data_, sharedContext) await this.shippingOptionRuleService_.update(data_, sharedContext)
eventBuilders.updatedShippingOptionRule({
data: updatedShippingOptionRules.map((rule) => ({ id: rule.id })),
sharedContext,
})
return Array.isArray(data) return Array.isArray(data)
? updatedShippingOptionRules ? updatedShippingOptionRules
: updatedShippingOptionRules[0] : updatedShippingOptionRules[0]
@@ -1980,9 +1839,6 @@ export default class FulfillmentModuleService
) )
const updatedLabelIds: string[] = [] const updatedLabelIds: string[] = []
let deletedLabelIds: string[] = []
const existingLabelIds = existingFulfillment.labels.map((label) => label.id)
/** /**
* @note * @note
@@ -1995,12 +1851,6 @@ export default class FulfillmentModuleService
* and we also need to emit the events later on. * and we also need to emit the events later on.
*/ */
if (isDefined(data.labels) && isPresent(data.labels)) { 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) { for (let label of data.labels) {
if (!("id" in label)) { if (!("id" in label)) {
continue continue
@@ -2029,49 +1879,9 @@ export default class FulfillmentModuleService
sharedContext sharedContext
) )
this.handleFulfillmentUpdateEvents(
fulfillment,
existingLabelIds,
updatedLabelIds,
deletedLabelIds,
sharedContext
)
return fulfillment return fulfillment
} }
private handleFulfillmentUpdateEvents(
fulfillment: InferEntityType<typeof Fulfillment>,
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() @InjectManager()
@EmitEvents() @EmitEvents()
async cancelFulfillment( async cancelFulfillment(
@@ -2106,11 +1916,6 @@ export default class FulfillmentModuleService
}, },
sharedContext sharedContext
) )
eventBuilders.updatedFulfillment({
data: [{ id }],
sharedContext,
})
} }
const result = await this.baseRepository_.serialize<FulfillmentDTO>( const result = await this.baseRepository_.serialize<FulfillmentDTO>(
@@ -2211,6 +2016,7 @@ export default class FulfillmentModuleService
} }
@InjectTransactionManager() @InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async deleteShippingProfiles( async deleteShippingProfiles(
ids: string | string[], ids: string | string[],
@@ -2226,6 +2032,7 @@ export default class FulfillmentModuleService
} }
@InjectTransactionManager() @InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async softDeleteShippingProfiles< async softDeleteShippingProfiles<
TReturnableLinkableKeys extends string = string TReturnableLinkableKeys extends string = string
@@ -436,17 +436,32 @@ moduleIntegrationTestRunner<IInventoryService>({
const updated = await service.updateReservationItems(update) const updated = await service.updateReservationItems(update)
const inventoryLevel =
await service.retrieveInventoryLevelByItemAndLocation(
reservationItem.inventory_item_id,
"location-1"
)
expect(updated).toEqual(expect.objectContaining(update)) expect(updated).toEqual(expect.objectContaining(update))
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy.mock.calls[0][0]).toHaveLength(2)
expect(eventBusSpy).toHaveBeenNthCalledWith( expect(eventBusSpy).toHaveBeenNthCalledWith(
1, 1,
[ expect.arrayContaining([
composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, { composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, {
data: { id: reservationItem.id }, data: { id: reservationItem.id },
object: "reservation-item", object: "reservation_item",
source: Modules.INVENTORY, source: Modules.INVENTORY,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
}), }),
], composeMessage(InventoryEvents.INVENTORY_LEVEL_UPDATED, {
data: { id: inventoryLevel.id },
object: "inventory_level",
source: Modules.INVENTORY,
action: CommonEvents.UPDATED,
}),
]),
{ {
internal: true, internal: true,
} }
@@ -457,19 +472,30 @@ moduleIntegrationTestRunner<IInventoryService>({
quantity: 10, quantity: 10,
} }
eventBusSpy.mockClear()
const updated2 = await service.updateReservationItems(update2) const updated2 = await service.updateReservationItems(update2)
expect(updated2).toEqual(expect.objectContaining(update2)) expect(updated2).toEqual(expect.objectContaining(update2))
expect(eventBusSpy).toHaveBeenCalledTimes(1)
expect(eventBusSpy.mock.calls[0][0]).toHaveLength(2)
expect(eventBusSpy).toHaveBeenNthCalledWith( expect(eventBusSpy).toHaveBeenNthCalledWith(
2, 1,
[ expect.arrayContaining([
composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, { composeMessage(InventoryEvents.RESERVATION_ITEM_UPDATED, {
data: { id: reservationItem.id }, data: { id: reservationItem.id },
object: "reservation-item", object: "reservation_item",
source: Modules.INVENTORY, source: Modules.INVENTORY,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
}), }),
], composeMessage(InventoryEvents.INVENTORY_LEVEL_UPDATED, {
data: { id: inventoryLevel.id },
object: "inventory_level",
source: Modules.INVENTORY,
action: CommonEvents.UPDATED,
}),
]),
{ {
internal: true, internal: true,
} }
@@ -15,19 +15,15 @@ import {
import { import {
arrayDifference, arrayDifference,
BigNumber, BigNumber,
CommonEvents,
EmitEvents, EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
InventoryEvents,
isDefined, isDefined,
isString, isString,
MathBN, MathBN,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaService, MedusaService,
moduleEventBuilderFactory,
Modules,
partitionArray, partitionArray,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { InventoryItem, InventoryLevel, ReservationItem } from "@models" import { InventoryItem, InventoryLevel, ReservationItem } from "@models"
@@ -249,21 +245,9 @@ export default class InventoryModuleService
const toCreate = Array.isArray(input) ? input : [input] const toCreate = Array.isArray(input) ? input : [input]
const created = await this.createReservationItems_(toCreate, context) 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< const serializedReservations = await this.baseRepository_.serialize<
InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO InventoryTypes.ReservationItemDTO[] | InventoryTypes.ReservationItemDTO
>(created, { >(created)
populate: true,
})
return Array.isArray(input) return Array.isArray(input)
? serializedReservations ? serializedReservations
@@ -350,21 +334,9 @@ export default class InventoryModuleService
) )
const result = await this.createInventoryItems_(toCreate, context) 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< const serializedItems = await this.baseRepository_.serialize<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[] InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
>(result, { >(result)
populate: true,
})
return Array.isArray(input) ? serializedItems : serializedItems[0] return Array.isArray(input) ? serializedItems : serializedItems[0]
} }
@@ -374,7 +346,7 @@ export default class InventoryModuleService
input: InventoryTypes.CreateInventoryItemInput[], input: InventoryTypes.CreateInventoryItemInput[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<InventoryTypes.InventoryItemDTO[]> { ): Promise<InventoryTypes.InventoryItemDTO[]> {
return await this.inventoryItemService_.create(input) return await this.inventoryItemService_.create(input, context)
} }
// @ts-ignore // @ts-ignore
@@ -405,21 +377,9 @@ export default class InventoryModuleService
const created = await this.createInventoryLevels_(toCreate, context) 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< const serialized = await this.baseRepository_.serialize<
InventoryTypes.InventoryLevelDTO[] | InventoryTypes.InventoryLevelDTO InventoryTypes.InventoryLevelDTO[] | InventoryTypes.InventoryLevelDTO
>(created, { >(created)
populate: true,
})
return Array.isArray(input) ? serialized : serialized[0] return Array.isArray(input) ? serialized : serialized[0]
} }
@@ -460,21 +420,9 @@ export default class InventoryModuleService
const result = await this.updateInventoryItems_(updates, context) 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< const serializedItems = await this.baseRepository_.serialize<
InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[] InventoryTypes.InventoryItemDTO | InventoryTypes.InventoryItemDTO[]
>(result, { >(result)
populate: true,
})
return Array.isArray(input) ? serializedItems : serializedItems[0] return Array.isArray(input) ? serializedItems : serializedItems[0]
} }
@@ -489,7 +437,7 @@ export default class InventoryModuleService
return await this.inventoryItemService_.update(input, context) return await this.inventoryItemService_.update(input, context)
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async deleteInventoryItemLevelByLocationId( async deleteInventoryItemLevelByLocationId(
locationId: string | string[], locationId: string | string[],
@@ -500,16 +448,6 @@ export default class InventoryModuleService
context context
) )
moduleEventBuilderFactory({
action: CommonEvents.DELETED,
object: "inventory-level",
source: Modules.INVENTORY,
eventName: InventoryEvents.INVENTORY_LEVEL_DELETED,
})({
data: result[0],
sharedContext: context,
})
return result return result
} }
@@ -532,16 +470,6 @@ export default class InventoryModuleService
context context
) )
moduleEventBuilderFactory({
action: CommonEvents.DELETED,
object: "inventory-level",
source: Modules.INVENTORY,
eventName: InventoryEvents.INVENTORY_LEVEL_DELETED,
})({
data: { id: inventoryLevel.id },
sharedContext: context,
})
if (!inventoryLevel) { if (!inventoryLevel) {
return return
} }
@@ -577,21 +505,9 @@ export default class InventoryModuleService
const levels = await this.updateInventoryLevels_(input, context) 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< const updatedLevels = await this.baseRepository_.serialize<
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[] InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
>(levels, { >(levels)
populate: true,
})
return Array.isArray(updates) ? updatedLevels : updatedLevels[0] return Array.isArray(updates) ? updatedLevels : updatedLevels[0]
} }
@@ -659,21 +575,9 @@ export default class InventoryModuleService
const update = Array.isArray(input) ? input : [input] const update = Array.isArray(input) ? input : [input]
const result = await this.updateReservationItems_(update, context) 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< const serialized = await this.baseRepository_.serialize<
InventoryTypes.ReservationItemDTO | InventoryTypes.ReservationItemDTO[] InventoryTypes.ReservationItemDTO | InventoryTypes.ReservationItemDTO[]
>(result, { >(result)
populate: true,
})
return Array.isArray(input) ? serialized : serialized[0] return Array.isArray(input) ? serialized : serialized[0]
} }
@@ -807,33 +711,47 @@ export default class InventoryModuleService
return result return result
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async softDeleteReservationItems( async softDeleteReservationItems(
ids: string | string[], ids: string | string[],
config?: SoftDeleteReturn<string>, config?: SoftDeleteReturn<string>,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<void> {
return await this.softDeleteReservationItems_(ids, config, context)
}
@InjectTransactionManager()
protected async softDeleteReservationItems_(
ids: string | string[],
config?: SoftDeleteReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> { ): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] = const reservations: InventoryTypes.ReservationItemDTO[] =
await super.listReservationItems({ id: ids }, {}, context) await super.listReservationItems({ id: ids }, {}, context)
const result = await super.softDeleteReservationItems( await super.softDeleteReservationItems({ id: ids }, config, context)
{ id: ids },
config,
context
)
await this.adjustInventoryLevelsForReservationsDeletion( await this.adjustInventoryLevelsForReservationsDeletion(
reservations, reservations,
context context
) )
}
result @InjectManager()
@EmitEvents()
// @ts-expect-error
async restoreReservationItems(
ids: string | string[],
config?: RestoreReturn<string>,
@MedusaContext() context: Context = {}
): Promise<void> {
return await this.restoreReservationItems_(ids, config, context)
} }
@InjectTransactionManager() @InjectTransactionManager()
// @ts-expect-error protected async restoreReservationItems_(
async restoreReservationItems(
ids: string | string[], ids: string | string[],
config?: RestoreReturn<string>, config?: RestoreReturn<string>,
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
@@ -849,11 +767,19 @@ export default class InventoryModuleService
) )
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async deleteReservationItemByLocationId( async deleteReservationItemByLocationId(
locationId: string | string[], locationId: string | string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<void> {
return await this.deleteReservationItemByLocationId_(locationId, context)
}
@InjectTransactionManager()
protected async deleteReservationItemByLocationId_(
locationId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> { ): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] = const reservations: InventoryTypes.ReservationItemDTO[] =
await this.listReservationItems({ location_id: locationId }, {}, context) await this.listReservationItems({ location_id: locationId }, {}, context)
@@ -863,16 +789,6 @@ export default class InventoryModuleService
context context
) )
moduleEventBuilderFactory({
action: CommonEvents.DELETED,
object: "reservation-item",
source: Modules.INVENTORY,
eventName: InventoryEvents.RESERVATION_ITEM_DELETED,
})({
data: reservations,
sharedContext: context,
})
await this.adjustInventoryLevelsForReservationsDeletion( await this.adjustInventoryLevelsForReservationsDeletion(
reservations, reservations,
context context
@@ -885,11 +801,19 @@ export default class InventoryModuleService
* @param context * @param context
*/ */
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async deleteReservationItemsByLineItem( async deleteReservationItemsByLineItem(
lineItemId: string | string[], lineItemId: string | string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<void> {
return await this.deleteReservationItemsByLineItem_(lineItemId, context)
}
@InjectTransactionManager()
protected async deleteReservationItemsByLineItem_(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> { ): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] = const reservations: InventoryTypes.ReservationItemDTO[] =
await this.listReservationItems({ line_item_id: lineItemId }, {}, context) await this.listReservationItems({ line_item_id: lineItemId }, {}, context)
@@ -903,16 +827,6 @@ export default class InventoryModuleService
reservations, reservations,
context 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 * @param context
*/ */
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async restoreReservationItemsByLineItem( async restoreReservationItemsByLineItem(
lineItemId: string | string[], lineItemId: string | string[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
): Promise<void> {
return await this.restoreReservationItemsByLineItem_(lineItemId, context)
}
@InjectTransactionManager()
protected async restoreReservationItemsByLineItem_(
lineItemId: string | string[],
@MedusaContext() context: Context = {}
): Promise<void> { ): Promise<void> {
const reservations: InventoryTypes.ReservationItemDTO[] = const reservations: InventoryTypes.ReservationItemDTO[] =
await this.listReservationItems({ line_item_id: lineItemId }, {}, context) await this.listReservationItems({ line_item_id: lineItemId }, {}, context)
@@ -939,16 +861,6 @@ export default class InventoryModuleService
reservations, reservations,
context 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 context
) )
results.push(result) 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<InventoryTypes.InventoryLevelDTO>( return await this.baseRepository_.serialize<InventoryTypes.InventoryLevelDTO>(
Array.isArray(inventoryItemIdOrData) ? results : results[0], Array.isArray(inventoryItemIdOrData) ? results : results[0]
{
populate: true,
}
) )
} }
@@ -43,6 +43,10 @@ moduleIntegrationTestRunner<INotificationModuleService>({
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit") eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
}) })
afterEach(() => {
eventBusEmitSpy.mockClear()
})
it(`should export the appropriate linkable configuration`, () => { it(`should export the appropriate linkable configuration`, () => {
const linkable = Module(Modules.NOTIFICATION, { const linkable = Module(Modules.NOTIFICATION, {
service: NotificationModuleService, service: NotificationModuleService,
@@ -119,16 +123,24 @@ moduleIntegrationTestRunner<INotificationModuleService>({
const result = await service.createNotifications(notification) const result = await service.createNotifications(notification)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1) expect(eventBusEmitSpy).toHaveBeenCalledTimes(1)
expect(eventBusEmitSpy).toHaveBeenCalledWith( expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
[ expect(eventBusEmitSpy).toHaveBeenNthCalledWith(
1,
expect.arrayContaining([
composeMessage(NotificationEvents.NOTIFICATION_CREATED, { composeMessage(NotificationEvents.NOTIFICATION_CREATED, {
data: { id: result.id }, data: { id: result.id },
object: "notification", object: "notification",
source: Modules.NOTIFICATION, source: Modules.NOTIFICATION,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
}), }),
], composeMessage(NotificationEvents.NOTIFICATION_UPDATED, {
data: { id: result.id },
object: "notification",
source: Modules.NOTIFICATION,
action: CommonEvents.UPDATED,
}),
]),
{ {
internal: true, internal: true,
} }
@@ -19,7 +19,6 @@ import {
promiseAll, promiseAll,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Notification } from "@models" import { Notification } from "@models"
import { eventBuilders } from "@utils"
import NotificationProviderService from "./notification-provider" import NotificationProviderService from "./notification-provider"
type InjectedDependencies = { type InjectedDependencies = {
@@ -91,11 +90,6 @@ export default class NotificationModuleService
NotificationTypes.NotificationDTO[] NotificationTypes.NotificationDTO[]
>(createdNotifications) >(createdNotifications)
eventBuilders.createdNotification({
data: serialized,
sharedContext,
})
return Array.isArray(data) ? serialized : serialized[0] return Array.isArray(data) ? serialized : serialized[0]
} }
@@ -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,
}),
}
@@ -1 +0,0 @@
export * from "./events"
File diff suppressed because it is too large. Load diff
@@ -41,6 +41,7 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
BigNumber, BigNumber,
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isPresent, isPresent,
@@ -182,8 +183,9 @@ export default class PaymentModuleService
data: CreatePaymentCollectionDTO[], data: CreatePaymentCollectionDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<PaymentCollectionDTO[]> ): Promise<PaymentCollectionDTO[]>
@InjectManager()
@InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createPaymentCollections( async createPaymentCollections(
data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[], data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[],
@@ -197,10 +199,7 @@ export default class PaymentModuleService
) )
return await this.baseRepository_.serialize<PaymentCollectionDTO[]>( return await this.baseRepository_.serialize<PaymentCollectionDTO[]>(
Array.isArray(data) ? collections : collections[0], Array.isArray(data) ? collections : collections[0]
{
populate: true,
}
) )
} }
@@ -226,6 +225,7 @@ export default class PaymentModuleService
): Promise<PaymentCollectionDTO[]> ): Promise<PaymentCollectionDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updatePaymentCollections( async updatePaymentCollections(
idOrSelector: string | FilterablePaymentCollectionProps, idOrSelector: string | FilterablePaymentCollectionProps,
@@ -260,10 +260,7 @@ export default class PaymentModuleService
) )
return await this.baseRepository_.serialize<PaymentCollectionDTO[]>( return await this.baseRepository_.serialize<PaymentCollectionDTO[]>(
Array.isArray(data) ? result : result[0], Array.isArray(data) ? result : result[0]
{
populate: true,
}
) )
} }
@@ -285,10 +282,23 @@ export default class PaymentModuleService
): Promise<PaymentCollectionDTO> ): Promise<PaymentCollectionDTO>
@InjectManager() @InjectManager()
@EmitEvents()
async upsertPaymentCollections( async upsertPaymentCollections(
data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[], data: UpsertPaymentCollectionDTO | UpsertPaymentCollectionDTO[],
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> { ): Promise<PaymentCollectionDTO | PaymentCollectionDTO[]> {
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<InferEntityType<typeof PaymentCollection>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(collection): collection is UpdatePaymentCollectionDTO => !!collection.id (collection): collection is UpdatePaymentCollectionDTO => !!collection.id
@@ -309,9 +319,7 @@ export default class PaymentModuleService
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize< return result
PaymentCollectionDTO[] | PaymentCollectionDTO
>(Array.isArray(data) ? result : result[0])
} }
completePaymentCollections( completePaymentCollections(
@@ -325,6 +333,7 @@ export default class PaymentModuleService
// Should we remove this and use `updatePaymentCollections` instead? // Should we remove this and use `updatePaymentCollections` instead?
@InjectManager() @InjectManager()
@EmitEvents()
async completePaymentCollections( async completePaymentCollections(
paymentCollectionId: string | string[], paymentCollectionId: string | string[],
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -344,12 +353,12 @@ export default class PaymentModuleService
) )
return await this.baseRepository_.serialize( return await this.baseRepository_.serialize(
Array.isArray(paymentCollectionId) ? updated : updated[0], Array.isArray(paymentCollectionId) ? updated : updated[0]
{ populate: true }
) )
} }
@InjectManager() @InjectManager()
@EmitEvents()
async createPaymentSession( async createPaymentSession(
paymentCollectionId: string, paymentCollectionId: string,
input: CreatePaymentSessionDTO, input: CreatePaymentSessionDTO,
@@ -429,6 +438,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async updatePaymentSession( async updatePaymentSession(
data: UpdatePaymentSessionDTO, data: UpdatePaymentSessionDTO,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -462,10 +472,11 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return await this.baseRepository_.serialize(updated, { populate: true }) return await this.baseRepository_.serialize(updated)
} }
@InjectManager() @InjectManager()
@EmitEvents()
async deletePaymentSession( async deletePaymentSession(
id: string, id: string,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -484,6 +495,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async authorizePaymentSession( async authorizePaymentSession(
id: string, id: string,
context: Record<string, unknown>, context: Record<string, unknown>,
@@ -509,9 +521,7 @@ export default class PaymentModuleService
// this method needs to be idempotent // this method needs to be idempotent
if (session.payment && session.authorized_at) { if (session.payment && session.authorized_at) {
return await this.baseRepository_.serialize(session.payment, { return await this.baseRepository_.serialize(session.payment)
populate: true,
})
} }
let { data, status } = await this.paymentProviderService_.authorizePayment( let { data, status } = await this.paymentProviderService_.authorizePayment(
@@ -565,13 +575,11 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return await this.baseRepository_.serialize(payment, { return await this.baseRepository_.serialize(payment)
populate: true,
})
} }
@InjectTransactionManager() @InjectTransactionManager()
async authorizePaymentSession_( protected async authorizePaymentSession_(
session: InferEntityType<typeof PaymentSession>, session: InferEntityType<typeof PaymentSession>,
data: Record<string, unknown> | undefined, data: Record<string, unknown> | undefined,
status: PaymentSessionStatus, status: PaymentSessionStatus,
@@ -620,6 +628,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async updatePayment( async updatePayment(
data: UpdatePaymentDTO, data: UpdatePaymentDTO,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -632,6 +641,7 @@ export default class PaymentModuleService
// TODO: This method should return a capture, not a payment // TODO: This method should return a capture, not a payment
@InjectManager() @InjectManager()
@EmitEvents()
async capturePayment( async capturePayment(
data: CreateCaptureDTO, data: CreateCaptureDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -680,13 +690,11 @@ export default class PaymentModuleService
sharedContext sharedContext
) )
return await this.baseRepository_.serialize(payment, { return await this.baseRepository_.serialize(payment)
populate: true,
})
} }
@InjectTransactionManager() @InjectTransactionManager()
private async capturePayment_( protected async capturePayment_(
data: CreateCaptureDTO, data: CreateCaptureDTO,
payment: InferEntityType<typeof Payment>, payment: InferEntityType<typeof Payment>,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -750,8 +758,9 @@ export default class PaymentModuleService
return { isFullyCaptured, capture } return { isFullyCaptured, capture }
} }
@InjectManager() @InjectManager()
private async capturePaymentFromProvider_( protected async capturePaymentFromProvider_(
payment: InferEntityType<typeof Payment>, payment: InferEntityType<typeof Payment>,
capture: InferEntityType<typeof Capture> | undefined, capture: InferEntityType<typeof Capture> | undefined,
isFullyCaptured: boolean, isFullyCaptured: boolean,
@@ -780,6 +789,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async refundPayment( async refundPayment(
data: CreateRefundDTO, data: CreateRefundDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -862,7 +872,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
private async refundPaymentFromProvider_( protected async refundPaymentFromProvider_(
payment: InferEntityType<typeof Payment>, payment: InferEntityType<typeof Payment>,
refund: InferEntityType<typeof Refund>, refund: InferEntityType<typeof Refund>,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -887,6 +897,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async cancelPayment( async cancelPayment(
paymentId: string, paymentId: string,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -913,7 +924,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
private async maybeUpdatePaymentCollection_( protected async maybeUpdatePaymentCollection_(
paymentCollectionId: string, paymentCollectionId: string,
sharedContext?: Context sharedContext?: Context
) { ) {
@@ -1050,6 +1061,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async createAccountHolder( async createAccountHolder(
input: CreateAccountHolderDTO, input: CreateAccountHolderDTO,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -1089,6 +1101,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async updateAccountHolder( async updateAccountHolder(
input: UpdateAccountHolderDTO, input: UpdateAccountHolderDTO,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -1128,6 +1141,7 @@ export default class PaymentModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async deleteAccountHolder( async deleteAccountHolder(
id: string, id: string,
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -1187,7 +1201,6 @@ export default class PaymentModuleService
return [normalizedResponse, paymentMethods.length] return [normalizedResponse, paymentMethods.length]
} }
// @ts-ignore
createPaymentMethods( createPaymentMethods(
data: CreatePaymentMethodDTO, data: CreatePaymentMethodDTO,
sharedContext?: Context sharedContext?: Context
@@ -1197,7 +1210,9 @@ export default class PaymentModuleService
data: CreatePaymentMethodDTO[], data: CreatePaymentMethodDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<PaymentMethodDTO[]> ): Promise<PaymentMethodDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async createPaymentMethods( async createPaymentMethods(
data: CreatePaymentMethodDTO | CreatePaymentMethodDTO[], data: CreatePaymentMethodDTO | CreatePaymentMethodDTO[],
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -461,35 +461,41 @@ moduleIntegrationTestRunner<IPricingModuleService>({
) )
const events = eventBusEmitSpy.mock.calls[0][0] const events = eventBusEmitSpy.mock.calls[0][0]
expect(events).toHaveLength(3)
expect(events[0]).toEqual( // 4 events: 1 price list created, 2 price list rules created, 1 price created
composeMessage(PricingEvents.PRICE_LIST_CREATED, { expect(events).toHaveLength(4)
source: Modules.PRICING,
action: CommonEvents.CREATED, expect(events).toEqual(
object: "price_list", expect.arrayContaining([
data: { id: priceList.id }, composeMessage(PricingEvents.PRICE_LIST_CREATED, {
}) source: Modules.PRICING,
) action: CommonEvents.CREATED,
expect(events[1]).toEqual( object: "price_list",
composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, { data: { id: priceList.id },
source: Modules.PRICING, }),
action: CommonEvents.CREATED, composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, {
object: "price_list_rule", source: Modules.PRICING,
data: { action: CommonEvents.CREATED,
id: [ object: "price_list_rule",
priceList.price_list_rules?.[0].id, data: {
priceList.price_list_rules?.[1].id, id: priceList.price_list_rules?.[1].id,
], },
}, }),
}) composeMessage(PricingEvents.PRICE_LIST_RULE_CREATED, {
) source: Modules.PRICING,
expect(events[2]).toEqual( action: CommonEvents.CREATED,
composeMessage(PricingEvents.PRICE_CREATED, { object: "price_list_rule",
source: Modules.PRICING, data: {
action: CommonEvents.CREATED, id: priceList.price_list_rules?.[0].id,
object: "price", },
data: { id: priceList.prices![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<IPricingModuleService>({
it("should update a price to a priceList successfully", async () => { it("should update a price to a priceList successfully", async () => {
const [priceSet] = await service.createPriceSets([{}]) const [priceSet] = await service.createPriceSets([{}])
await service.addPriceListPrices([ const [priceBeforeUpdate] = await service.addPriceListPrices([
{ {
price_list_id: "price-list-1", price_list_id: "price-list-1",
prices: [ prices: [
@@ -806,7 +812,9 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}, },
]) ])
await service.updatePriceListPrices([ eventBusEmitSpy.mockClear()
const [price] = await service.updatePriceListPrices([
{ {
price_list_id: "price-list-1", price_list_id: "price-list-1",
prices: [ prices: [
@@ -822,28 +830,37 @@ moduleIntegrationTestRunner<IPricingModuleService>({
}, },
]) ])
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(events).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining(
name: "pricing.price-rule.created", composeMessage(PricingEvents.PRICE_UPDATED, {
}), source: Modules.PRICING,
expect.objectContaining({ action: CommonEvents.UPDATED,
name: "pricing.price-rule.created",
}),
expect.objectContaining({
name: "pricing.price.updated",
metadata: {
source: "pricing",
object: "price", object: "price",
action: "updated", data: { id: price.id },
}, })
data: { ),
id: "test-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({ composeMessage(PricingEvents.PRICE_RULE_CREATED, {
name: "pricing.price-rule.deleted", 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 },
}), }),
]) ])
) )
@@ -41,7 +41,6 @@ import {
promiseAll, promiseAll,
removeNullish, removeNullish,
simpleHash, simpleHash,
upperCaseFirst,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
@@ -55,7 +54,7 @@ import {
import { Collection } from "@mikro-orm/core" import { Collection } from "@mikro-orm/core"
import { ServiceTypes } from "@types" import { ServiceTypes } from "@types"
import { eventBuilders, validatePriceListDates } from "@utils" import { validatePriceListDates } from "@utils"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
type InjectedDependencies = { type InjectedDependencies = {
@@ -169,6 +168,8 @@ export default class PricingModuleService
return pricingContext return pricingContext
} }
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createPriceRules( async createPriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["createPriceRules"]> ...args: Parameters<PricingTypes.IPricingModuleService["createPriceRules"]>
@@ -180,6 +181,8 @@ export default class PricingModuleService
} }
} }
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updatePriceRules( async updatePriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["updatePriceRules"]> ...args: Parameters<PricingTypes.IPricingModuleService["updatePriceRules"]>
@@ -191,6 +194,8 @@ export default class PricingModuleService
} }
} }
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createPriceListRules( async createPriceListRules(
...args: any[] ...args: any[]
@@ -203,6 +208,8 @@ export default class PricingModuleService
} }
} }
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updatePriceListRules( async updatePriceListRules(
...args: any[] ...args: any[]
@@ -269,7 +276,7 @@ export default class PricingModuleService
priceSet.calculated_price = calculatedPrice ?? null priceSet.calculated_price = calculatedPrice ?? null
} }
return priceSets return await this.baseRepository_.serialize<PriceSetDTO[]>(priceSets)
} }
@InjectManager() @InjectManager()
@@ -291,7 +298,10 @@ export default class PricingModuleService
sharedContext sharedContext
) )
if (!pricingContext || !priceSets.length) { if (!pricingContext || !priceSets.length) {
return [priceSets, count] const serializedPriceSets = await this.baseRepository_.serialize<
PriceSetDTO[]
>(priceSets)
return [serializedPriceSets, count]
} }
const calculatedPrices = await this.calculatePrices( const calculatedPrices = await this.calculatePrices(
@@ -310,7 +320,11 @@ export default class PricingModuleService
priceSet.calculated_price = calculatedPrice ?? null priceSet.calculated_price = calculatedPrice ?? null
} }
return [priceSets, count] const serializedPriceSets = await this.baseRepository_.serialize<
PriceSetDTO[]
>(priceSets)
return [serializedPriceSets, count]
} }
@InjectManager() @InjectManager()
@@ -405,73 +419,73 @@ export default class PricingModuleService
sharedContext sharedContext
) )
const calculatedPrices: PricingTypes.CalculatedPriceSet[] = 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
return { for (const priceSetId of pricingFilters.id) {
id: priceSetId, const prices = pricesSetPricesMap.get(priceSetId)
is_calculated_price_price_list: !!calculatedPrice?.price_list_id, if (!prices) {
is_calculated_price_tax_inclusive: isTaxInclusive( continue
priceRulesPriceMap.get(calculatedPrice.id), }
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, pricingPreferences,
calculatedPrice.currency_code!, originalPrice.currency_code || calculatedPrice.currency_code!,
pricingContext.context?.region_id as string pricingContext.context?.region_id as string
), )
calculated_amount: isPresent(calculatedPrice?.amount) : false,
? parseFloat(calculatedPrice?.amount as string) original_amount: isPresent(originalPrice?.amount)
: null, ? parseFloat(originalPrice?.amount as string)
raw_calculated_amount: calculatedPrice?.raw_amount || null, : null,
raw_original_amount: originalPrice?.raw_amount || null,
is_original_price_price_list: !!originalPrice?.price_list_id, currency_code: calculatedPrice?.currency_code || null,
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, 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: { original_price: {
id: calculatedPrice?.id || null, id: originalPrice?.id || null,
price_list_id: calculatedPrice?.price_list_id || null, price_list_id: originalPrice?.price_list_id || null,
price_list_type: calculatedPrice?.price_list_type || null, price_list_type: originalPrice?.price_list_type || null,
min_quantity: min_quantity: parseInt(originalPrice?.min_quantity || "") || null,
parseInt(calculatedPrice?.min_quantity || "") || null, max_quantity: parseInt(originalPrice?.max_quantity || "") || null,
max_quantity: },
parseInt(calculatedPrice?.max_quantity || "") || null, }
},
original_price: { calculatedPrices.push(calculatedPrice_)
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[]
return JSON.parse(JSON.stringify(calculatedPrices)) return calculatedPrices
} }
// @ts-expect-error // @ts-expect-error
@@ -534,6 +548,22 @@ export default class PricingModuleService
data: UpsertPriceSetDTO | UpsertPriceSetDTO[], data: UpsertPriceSetDTO | UpsertPriceSetDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO | PriceSetDTO[]> { ): Promise<PriceSetDTO | PriceSetDTO[]> {
const result = await this.upsertPriceSets_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? result : result[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
protected async upsertPriceSets_(
data: UpsertPriceSetDTO | UpsertPriceSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof PriceSet>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
@@ -554,13 +584,7 @@ export default class PricingModuleService
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
try { return result
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? result : result[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
} }
// @ts-expect-error // @ts-expect-error
@@ -606,12 +630,13 @@ export default class PricingModuleService
normalizedInput, normalizedInput,
sharedContext sharedContext
) )
const priceSets = await this.baseRepository_.serialize<
const serializedUpdateResult = await this.baseRepository_.serialize<
PriceSetDTO[] | PriceSetDTO PriceSetDTO[] | PriceSetDTO
>(updateResult) >(isString(idOrSelector) ? updateResult[0] : updateResult)
try { try {
return isString(idOrSelector) ? priceSets[0] : priceSets return serializedUpdateResult
} finally { } finally {
this.pricingRepository_.clearAvailableAttributes?.() this.pricingRepository_.clearAvailableAttributes?.()
} }
@@ -707,10 +732,6 @@ export default class PricingModuleService
})), })),
sharedContext sharedContext
) )
eventBuilders.createdPriceRule({
data: createdPriceRules.map((r) => ({ id: r.id })),
sharedContext,
})
} }
if (priceRulesToUpdate.length > 0) { if (priceRulesToUpdate.length > 0) {
@@ -718,10 +739,6 @@ export default class PricingModuleService
priceRulesToUpdate, priceRulesToUpdate,
sharedContext sharedContext
) )
eventBuilders.updatedPriceRule({
data: updatedPriceRules.map((r) => ({ id: r.id })),
sharedContext,
})
} }
if (priceRulesToDelete.length > 0) { if (priceRulesToDelete.length > 0) {
@@ -729,10 +746,6 @@ export default class PricingModuleService
priceRulesToDelete, priceRulesToDelete,
sharedContext sharedContext
) )
eventBuilders.deletedPriceRule({
data: priceRulesToDelete.map((id) => ({ id })),
sharedContext,
})
} }
const upsertedPriceRules = [ const upsertedPriceRules = [
@@ -755,10 +768,6 @@ export default class PricingModuleService
priceRuleToDelete, priceRuleToDelete,
sharedContext sharedContext
) )
eventBuilders.deletedPriceRule({
data: priceRuleToDelete.map((id) => ({ id })),
sharedContext,
})
} }
;(priceToUpdate as InferEntityType<typeof Price>).rules_count = 0 ;(priceToUpdate as InferEntityType<typeof Price>).rules_count = 0
@@ -780,27 +789,6 @@ export default class PricingModuleService
await this.priceService_.delete(pricesToDelete, sharedContext) 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( const priceSets = await this.priceSetService_.list(
{ id: normalizedData.map(({ id }) => id) }, { id: normalizedData.map(({ id }) => id) },
{ {
@@ -825,11 +813,6 @@ export default class PricingModuleService
ps.prices = upsertedPricesMap.get(ps.id) || [] ps.prices = upsertedPricesMap.get(ps.id) || []
}) })
eventBuilders.updatedPriceSet({
data: priceSets.map((ps) => ({ id: ps.id })),
sharedContext,
})
return priceSets return priceSets
} }
@@ -952,8 +935,12 @@ export default class PricingModuleService
return dbPrices.find((p) => p.id === inputItem.priceSetId)! return dbPrices.find((p) => p.id === inputItem.priceSetId)!
}) })
const serializedOrderedPriceSets = await this.baseRepository_.serialize<
PricingTypes.PriceSetDTO[]
>(Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0])
try { try {
return Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0] return serializedOrderedPriceSets
} finally { } finally {
this.pricingRepository_.clearAvailableAttributes?.() this.pricingRepository_.clearAvailableAttributes?.()
} }
@@ -977,7 +964,7 @@ export default class PricingModuleService
} }
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
// @ts-ignore // @ts-ignore
async updatePriceLists( async updatePriceLists(
@@ -1117,6 +1104,18 @@ export default class PricingModuleService
data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[], data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<PricePreferenceDTO | PricePreferenceDTO[]> { ): Promise<PricePreferenceDTO | PricePreferenceDTO[]> {
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<InferEntityType<typeof PricePreference>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
( (
@@ -1139,9 +1138,8 @@ export default class PricingModuleService
} }
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<
PricePreferenceDTO[] | PricePreferenceDTO return result
>(Array.isArray(data) ? result : result[0])
} }
// @ts-expect-error // @ts-expect-error
@@ -1158,6 +1156,7 @@ export default class PricingModuleService
): Promise<PricePreferenceDTO[]> ): Promise<PricePreferenceDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updatePricePreferences( async updatePricePreferences(
idOrSelector: string | PricingTypes.FilterablePricePreferenceProps, idOrSelector: string | PricingTypes.FilterablePricePreferenceProps,
@@ -1248,49 +1247,6 @@ export default class PricingModuleService
sharedContext 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 return priceSets
} }
@@ -1336,18 +1292,11 @@ export default class PricingModuleService
} }
}) })
const { entities, performedActions } = const { entities } = await this.priceService_.upsertWithReplace(
await this.priceService_.upsertWithReplace( pricesToUpsert,
pricesToUpsert, { relations: ["price_rules"] },
{ relations: ["price_rules"] }, sharedContext
sharedContext )
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities return entities
} }
@@ -1401,67 +1350,6 @@ export default class PricingModuleService
sharedContext 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 return priceLists
} }
@@ -1519,16 +1407,12 @@ export default class PricingModuleService
} }
) )
const { entities, performedActions } = const { entities } = await this.priceListService_.upsertWithReplace(
await this.priceListService_.upsertWithReplace(normalizedData, { normalizedData,
{
relations: ["price_list_rules"], relations: ["price_list_rules"],
}) }
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities return entities
} }
@@ -1572,18 +1456,11 @@ export default class PricingModuleService
} }
} }
const { entities, performedActions } = const { entities } = await this.priceService_.upsertWithReplace(
await this.priceService_.upsertWithReplace( pricesToUpsert,
pricesToUpsert, { relations: ["price_rules"] },
{ relations: ["price_rules"] }, sharedContext
sharedContext )
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities return entities
} }
@@ -1634,18 +1511,11 @@ export default class PricingModuleService
} }
}) })
const { entities, performedActions } = const { entities } = await this.priceService_.upsertWithReplace(
await this.priceService_.upsertWithReplace( pricesToUpsert,
pricesToUpsert, { relations: ["price_rules"] },
{ relations: ["price_rules"] }, sharedContext
sharedContext )
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities return entities
} }
@@ -1705,18 +1575,11 @@ export default class PricingModuleService
}) })
.filter(Boolean) .filter(Boolean)
const { entities, performedActions } = const { entities } = await this.priceListService_.upsertWithReplace(
await this.priceListService_.upsertWithReplace( priceListsUpsert,
priceListsUpsert, { relations: ["price_list_rules"] },
{ relations: ["price_list_rules"] }, sharedContext
sharedContext )
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities return entities
} }
@@ -1779,18 +1642,11 @@ export default class PricingModuleService
}) })
.filter(Boolean) .filter(Boolean)
const { entities, performedActions } = const { entities } = await this.priceListService_.upsertWithReplace(
await this.priceListService_.upsertWithReplace( priceListsUpsert,
priceListsUpsert, { relations: ["price_list_rules"] },
{ relations: ["price_list_rules"] }, sharedContext
sharedContext )
)
composeAllEvents({
eventBuilders,
performedActions,
sharedContext,
})
return entities 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 = ( const isTaxInclusive = (
priceRules: InferEntityType<typeof PriceRule>[], priceRules: InferEntityType<typeof PriceRule>[],
preferences: InferEntityType<typeof PricePreference>[], preferences: InferEntityType<typeof PricePreference>[],
@@ -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,
}),
}
@@ -1,2 +1 @@
export * from "./validate-price-list-dates" export * from "./validate-price-list-dates"
export * from "./events"
@@ -79,7 +79,10 @@ export const buildProductAndRelationsData = ({
title: faker.commerce.productName(), title: faker.commerce.productName(),
sku: faker.commerce.productName(), sku: faker.commerce.productName(),
options: options options: options
? { [options[0].title]: options[0].values[0] } ? options.reduce((acc, option) => {
acc[option.title] = option.values[0]
return acc
}, {} as Record<string, string>)
: { : {
[defaultOptionTitle]: defaultOptionValue, [defaultOptionTitle]: defaultOptionValue,
}, },
@@ -1,15 +1,11 @@
import { IProductModuleService } from "@medusajs/framework/types" import { IProductModuleService } from "@medusajs/framework/types"
import { import {
CommonEvents,
composeMessage,
Modules, Modules,
ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity, toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Product, ProductCategory } from "@models" import { Product, ProductCategory } from "@models"
import { import {
MockEventBusService,
moduleIntegrationTestRunner, moduleIntegrationTestRunner,
} from "@medusajs/test-utils" } from "@medusajs/test-utils"
import { productCategoriesRankData } from "../../__fixtures__/product-category/data" import { productCategoriesRankData } from "../../__fixtures__/product-category/data"
@@ -18,9 +14,6 @@ jest.setTimeout(30000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
injectedDependencies: {
[Modules.EVENT_BUS]: new MockEventBusService(),
},
testSuite: ({ MikroOrmWrapper, service }) => { testSuite: ({ MikroOrmWrapper, service }) => {
describe("ProductModuleService product categories", () => { describe("ProductModuleService product categories", () => {
let productOne: Product let productOne: Product
@@ -404,29 +397,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
) )
}) })
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 () => { it("should append rank from an existing category depending on parent", async () => {
await service.createProductCategories({ await service.createProductCategories({
@@ -504,29 +474,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
productCategoryZeroTwo = categories[5] 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 () => { it("should update the name of the category successfully", async () => {
await service.updateProductCategories(productCategoryZero.id, { await service.updateProductCategories(productCategoryZero.id, {
@@ -683,30 +630,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
productCategoryTwo = categories[2] 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 () => { it("should throw an error when an id does not exist", async () => {
let error let error
@@ -1,14 +1,10 @@
import { IProductModuleService } from "@medusajs/framework/types" import { IProductModuleService } from "@medusajs/framework/types"
import { import {
CommonEvents,
composeMessage,
Modules, Modules,
ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity, toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
MockEventBusService,
moduleIntegrationTestRunner, moduleIntegrationTestRunner,
} from "@medusajs/test-utils" } from "@medusajs/test-utils"
import { Product, ProductCollection } from "@models" import { Product, ProductCollection } from "@models"
@@ -18,9 +14,6 @@ jest.setTimeout(30000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
injectedDependencies: {
[Modules.EVENT_BUS]: new MockEventBusService(),
},
testSuite: ({ MikroOrmWrapper, service }) => { testSuite: ({ MikroOrmWrapper, service }) => {
describe("ProductModuleService product collections", () => { describe("ProductModuleService product collections", () => {
let productOne: Product let productOne: Product
@@ -284,59 +277,11 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(collections).toHaveLength(0) 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", () => { describe("updateCollections", () => {
const collectionId = "test-1" 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 () => { it("should update the value of the collection successfully", async () => {
await service.upsertProductCollections([ await service.upsertProductCollections([
@@ -534,28 +479,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
) )
}) })
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,
}
)
})
}) })
}) })
}, },
@@ -1,15 +1,11 @@
import { IProductModuleService } from "@medusajs/framework/types" import { IProductModuleService } from "@medusajs/framework/types"
import { import {
CommonEvents,
composeMessage,
Modules, Modules,
ProductEvents,
ProductStatus, ProductStatus,
toMikroORMEntity, toMikroORMEntity,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Product, ProductTag } from "@models" import { Product, ProductTag } from "@models"
import { import {
MockEventBusService,
moduleIntegrationTestRunner, moduleIntegrationTestRunner,
} from "@medusajs/test-utils" } from "@medusajs/test-utils"
@@ -18,15 +14,6 @@ jest.setTimeout(30000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
testSuite: ({ MikroOrmWrapper, service }) => { testSuite: ({ MikroOrmWrapper, service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("ProductModuleService product tags", () => { describe("ProductModuleService product tags", () => {
let tagOne: ProductTag let tagOne: ProductTag
@@ -303,20 +290,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(productTag.value).toEqual("UK") 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 () => { it("should throw an error when an id does not exist", async () => {
@@ -350,20 +323,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(productTag[0]?.value).toEqual("UK") 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<IProductModuleService>({
const newTag = productTags.find((t) => t.value === "new")! const newTag = productTags.find((t) => t.value === "new")!
const updatedTag = productTags.find((t) => t.value === "updated")! 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,
}
)
}) })
}) })
}) })
@@ -7,15 +7,11 @@ import {
UpdateProductVariantDTO, UpdateProductVariantDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
CommonEvents,
composeMessage,
Modules, Modules,
ProductEvents,
ProductStatus, ProductStatus,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
MockEventBusService,
moduleIntegrationTestRunner, moduleIntegrationTestRunner,
} from "@medusajs/test-utils" } from "@medusajs/test-utils"
@@ -24,15 +20,6 @@ jest.setTimeout(30000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
testSuite: ({ service }) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("ProductModuleService product variants", () => { describe("ProductModuleService product variants", () => {
let variantOne: ProductVariantDTO let variantOne: ProductVariantDTO
@@ -212,20 +199,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
) )
expect(productVariant.title).toEqual("new test") 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 () => { it("should do a partial update on the options of a variant successfully", async () => {
@@ -294,20 +267,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
}) })
) )
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 () => { it("should correctly associate variants with own product options", async () => {
@@ -4,11 +4,8 @@ import {
ProductTagDTO, ProductTagDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
CommonEvents,
composeMessage,
kebabCase, kebabCase,
Modules, Modules,
ProductEvents,
ProductStatus, ProductStatus,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { import {
@@ -20,7 +17,6 @@ import {
} from "@models" } from "@models"
import { import {
MockEventBusService,
moduleIntegrationTestRunner, moduleIntegrationTestRunner,
} from "@medusajs/test-utils" } from "@medusajs/test-utils"
import { UpdateProductInput } from "@types" import { UpdateProductInput } from "@types"
@@ -34,9 +30,6 @@ jest.setTimeout(300000)
moduleIntegrationTestRunner<IProductModuleService>({ moduleIntegrationTestRunner<IProductModuleService>({
moduleName: Modules.PRODUCT, moduleName: Modules.PRODUCT,
injectedDependencies: {
[Modules.EVENT_BUS]: new MockEventBusService(),
},
testSuite: ({ MikroOrmWrapper, service }) => { testSuite: ({ MikroOrmWrapper, service }) => {
describe("ProductModuleService products", function () { describe("ProductModuleService products", function () {
let productCollectionOne: ProductCollection let productCollectionOne: ProductCollection
@@ -573,37 +566,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
) )
}) })
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 () => { it("should add relationships to a product", async () => {
const updateData = { const updateData = {
@@ -1086,29 +1048,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
) )
}) })
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 () => { it("should throw because variant doesn't have all options set", async () => {
const error = await service const error = await service
@@ -1289,75 +1228,6 @@ moduleIntegrationTestRunner<IProductModuleService>({
expect(softDeleted).toHaveLength(1) 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 () { describe("restore", function () {
+1 -1
View File
@@ -29,7 +29,7 @@
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", "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", "build": "rimraf dist && tsc --build && npm run resolve:aliases",
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", "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: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: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", "migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
@@ -452,9 +452,7 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
} }
let productCategory = await manager.findOne< let productCategory = await manager.findOne<
InferEntityType<typeof ProductCategory> InferEntityType<typeof ProductCategory>
>(ProductCategory.name, { >(ProductCategory.name, categoryData.id!)
id: categoryData.id,
})
if (!productCategory) { if (!productCategory) {
throw new MedusaError( throw new MedusaError(
@@ -9,6 +9,7 @@ import {
isPresent, isPresent,
mergeMetadata, mergeMetadata,
isDefined, isDefined,
deepCopy,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { SqlEntityManager, wrap } from "@mikro-orm/postgresql" import { SqlEntityManager, wrap } from "@mikro-orm/postgresql"
@@ -80,21 +81,22 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
) => void, ) => void,
context: Context = {} context: Context = {}
): Promise<InferEntityType<typeof Product>[]> { ): Promise<InferEntityType<typeof Product>[]> {
const productsToUpdate_ = deepCopy(productsToUpdate)
const productIdsToUpdate: string[] = [] const productIdsToUpdate: string[] = []
productsToUpdate.forEach((productToUpdate) => { productsToUpdate_.forEach((productToUpdate) => {
ProductRepository.#correctUpdateDTOTypes(productToUpdate) ProductRepository.#correctUpdateDTOTypes(productToUpdate)
productIdsToUpdate.push(productToUpdate.id) productIdsToUpdate.push(productToUpdate.id)
}) })
const relationsToLoad = const relationsToLoad =
ProductRepository.#getProductDeepUpdateRelationsToLoad(productsToUpdate) ProductRepository.#getProductDeepUpdateRelationsToLoad(productsToUpdate_)
const findOptions = buildQuery( const findOptions = buildQuery(
{ id: productIdsToUpdate }, { id: productIdsToUpdate },
{ {
relations: relationsToLoad, 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 product = productsMap.get(productToUpdate.id)!
const wrappedProduct = wrap(product) 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, // Doing this to ensure updates are returned in the same order they were provided,
// since some core flows rely on this. // since some core flows rely on this.
// This is a high level of coupling though. // This is a high level of coupling though.
return productsToUpdate.map( return productsToUpdate_.map(
(productToUpdate) => productsMap.get(productToUpdate.id)! (productToUpdate) => productsMap.get(productToUpdate.id)!
) )
} }
@@ -6,30 +6,45 @@ import {
ProductTypes, ProductTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
createMedusaMikroOrmEventSubscriber,
FreeTextSearchFilterKeyPrefix, FreeTextSearchFilterKeyPrefix,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isDefined, isDefined,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaInternalService,
MedusaService,
ModulesSdkUtils, ModulesSdkUtils,
registerInternalServiceEventSubscriber,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { EntityManager, EventType } from "@mikro-orm/core"
import { ProductCategory } from "@models" import { ProductCategory } from "@models"
import { ProductCategoryRepository } from "@repositories" import { ProductCategoryRepository } from "@repositories"
import { UpdateCategoryInput } from "@types" import { UpdateCategoryInput } from "@types"
type InjectedDependencies = { type InjectedDependencies = {
productCategoryRepository: DAL.TreeRepositoryService productCategoryRepository: DAL.TreeRepositoryService
productModuleService: ReturnType<typeof MedusaService>
} }
export default class ProductCategoryService {
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
constructor({ productCategoryRepository }: InjectedDependencies) { export default class ProductCategoryService extends MedusaInternalService<
this.productCategoryRepository_ = productCategoryRepository 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 // TODO: Add support for object filter
@InjectManager("productCategoryRepository_") @InjectManager("productCategoryRepository_")
// @ts-expect-error
async retrieve( async retrieve(
productCategoryId: string, productCategoryId: string,
config: FindConfig<ProductTypes.ProductCategoryDTO> = {}, config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
@@ -150,6 +165,7 @@ export default class ProductCategoryService {
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
// @ts-expect-error
async update( async update(
data: UpdateCategoryInput[], data: UpdateCategoryInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -160,14 +176,45 @@ export default class ProductCategoryService {
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
// @ts-expect-error
async delete( async delete(
ids: string[], ids: string[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<string[]> { ): Promise<string[]> {
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<typeof eventManager.dispatchEvent>[2],
})
})
}
return deletedIds
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
// @ts-expect-error
async softDelete( async softDelete(
ids: string[], ids: string[],
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -178,6 +225,7 @@ export default class ProductCategoryService {
} }
@InjectTransactionManager("productCategoryRepository_") @InjectTransactionManager("productCategoryRepository_")
// @ts-expect-error
async restore( async restore(
ids: string[], ids: string[],
@MedusaContext() sharedContext?: Context @MedusaContext() sharedContext?: Context
@@ -25,6 +25,7 @@ import { ProductCategoryService } from "@services"
import { import {
arrayDifference, arrayDifference,
createMedusaMikroOrmEventSubscriber,
EmitEvents, EmitEvents,
generateEntityId, generateEntityId,
InjectManager, InjectManager,
@@ -37,11 +38,13 @@ import {
MedusaContext, MedusaContext,
MedusaError, MedusaError,
MedusaService, MedusaService,
MessageAggregator,
Modules, Modules,
ProductStatus, ProductStatus,
removeUndefined, removeUndefined,
toHandle, toHandle,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { EntityManager } from "@mikro-orm/core"
import { ProductRepository } from "../repositories" import { ProductRepository } from "../repositories"
import { import {
UpdateCategoryInput, UpdateCategoryInput,
@@ -52,8 +55,8 @@ import {
UpdateTagInput, UpdateTagInput,
UpdateTypeInput, UpdateTypeInput,
} from "../types" } from "../types"
import { eventBuilders } from "../utils"
import { joinerConfig } from "./../joiner-config" import { joinerConfig } from "./../joiner-config"
import { eventBuilders } from "../utils/events"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -331,11 +334,6 @@ export default class ProductModuleService
sharedContext sharedContext
) )
eventBuilders.createdProductVariant({
data: createdVariants,
sharedContext,
})
return createdVariants return createdVariants
} }
@@ -366,11 +364,11 @@ export default class ProductModuleService
(variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id (variant): variant is ProductTypes.CreateProductVariantDTO => !variant.id
) )
let created: InferEntityType<typeof ProductVariant>[] = [] let created: ProductTypes.ProductVariantDTO[] = []
let updated: InferEntityType<typeof ProductVariant>[] = [] let updated: InferEntityType<typeof ProductVariant>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createVariants_(forCreate, sharedContext) created = await this.createProductVariants(forCreate, sharedContext)
} }
if (forUpdate.length) { if (forUpdate.length) {
updated = await this.updateVariants_(forUpdate, sharedContext) 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( await this.productVariantService_.upsertWithReplace(
productVariantsWithOptions, productVariantsWithOptions,
{ {
@@ -502,19 +500,6 @@ export default class ProductModuleService
sharedContext 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 return productVariants
} }
@@ -544,11 +529,6 @@ export default class ProductModuleService
ProductTypes.ProductTagDTO[] ProductTypes.ProductTagDTO[]
>(tags) >(tags)
eventBuilders.createdProductTag({
data: createdTags,
sharedContext,
})
return Array.isArray(data) ? createdTags : createdTags[0] return Array.isArray(data) ? createdTags : createdTags[0]
} }
@@ -561,12 +541,26 @@ export default class ProductModuleService
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductTagDTO> ): Promise<ProductTypes.ProductTagDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async upsertProductTags( async upsertProductTags(
data: ProductTypes.UpsertProductTagDTO[] | ProductTypes.UpsertProductTagDTO, data: ProductTypes.UpsertProductTagDTO[] | ProductTypes.UpsertProductTagDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO> { ): Promise<ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO> {
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<InferEntityType<typeof ProductTag>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter((tag): tag is UpdateTagInput => !!tag.id) const forUpdate = input.filter((tag): tag is UpdateTagInput => !!tag.id)
const forCreate = input.filter( const forCreate = input.filter(
@@ -578,25 +572,12 @@ export default class ProductModuleService
if (forCreate.length) { if (forCreate.length) {
created = await this.productTagService_.create(forCreate, sharedContext) created = await this.productTagService_.create(forCreate, sharedContext)
eventBuilders.createdProductTag({
data: created,
sharedContext,
})
} }
if (forUpdate.length) { if (forUpdate.length) {
updated = await this.productTagService_.update(forUpdate, sharedContext) updated = await this.productTagService_.update(forUpdate, sharedContext)
eventBuilders.updatedProductTag({
data: updated,
sharedContext,
})
} }
const result = [...created, ...updated] return [...created, ...updated]
const allTags = await this.baseRepository_.serialize<
ProductTypes.ProductTagDTO[] | ProductTypes.ProductTagDTO
>(result)
return Array.isArray(data) ? allTags : allTags[0]
} }
// @ts-expect-error // @ts-expect-error
@@ -647,11 +628,6 @@ export default class ProductModuleService
ProductTypes.ProductTagDTO[] ProductTypes.ProductTagDTO[]
>(tags) >(tags)
eventBuilders.updatedProductTag({
data: updatedTags,
sharedContext,
})
return isString(idOrSelector) ? updatedTags[0] : updatedTags return isString(idOrSelector) ? updatedTags[0] : updatedTags
} }
@@ -667,6 +643,7 @@ export default class ProductModuleService
): Promise<ProductTypes.ProductTypeDTO> ): Promise<ProductTypes.ProductTypeDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createProductTypes( async createProductTypes(
data: data:
@@ -694,13 +671,30 @@ export default class ProductModuleService
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductTypeDTO> ): Promise<ProductTypes.ProductTypeDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async upsertProductTypes( async upsertProductTypes(
data: data:
| ProductTypes.UpsertProductTypeDTO[] | ProductTypes.UpsertProductTypeDTO[]
| ProductTypes.UpsertProductTypeDTO, | ProductTypes.UpsertProductTypeDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO> { ): Promise<ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO> {
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<InferEntityType<typeof ProductType>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter((type): type is UpdateTypeInput => !!type.id) const forUpdate = input.filter((type): type is UpdateTypeInput => !!type.id)
const forCreate = input.filter( const forCreate = input.filter(
@@ -717,12 +711,7 @@ export default class ProductModuleService
updated = await this.productTypeService_.update(forUpdate, sharedContext) updated = await this.productTypeService_.update(forUpdate, sharedContext)
} }
const result = [...created, ...updated] return [...created, ...updated]
const allTypes = await this.baseRepository_.serialize<
ProductTypes.ProductTypeDTO[] | ProductTypes.ProductTypeDTO
>(result)
return Array.isArray(data) ? allTypes : allTypes[0]
} }
// @ts-expect-error // @ts-expect-error
@@ -739,6 +728,7 @@ export default class ProductModuleService
): Promise<ProductTypes.ProductTypeDTO[]> ): Promise<ProductTypes.ProductTypeDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateProductTypes( async updateProductTypes(
idOrSelector: string | ProductTypes.FilterableProductTypeProps, idOrSelector: string | ProductTypes.FilterableProductTypeProps,
@@ -787,6 +777,7 @@ export default class ProductModuleService
): Promise<ProductTypes.ProductOptionDTO> ): Promise<ProductTypes.ProductOptionDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createProductOptions( async createProductOptions(
data: data:
@@ -842,6 +833,7 @@ export default class ProductModuleService
): Promise<ProductTypes.ProductOptionDTO> ): Promise<ProductTypes.ProductOptionDTO>
@InjectTransactionManager() @InjectTransactionManager()
@EmitEvents()
async upsertProductOptions( async upsertProductOptions(
data: data:
| ProductTypes.UpsertProductOptionDTO[] | ProductTypes.UpsertProductOptionDTO[]
@@ -888,6 +880,7 @@ export default class ProductModuleService
): Promise<ProductTypes.ProductOptionDTO[]> ): Promise<ProductTypes.ProductOptionDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateProductOptions( async updateProductOptions(
idOrSelector: string | ProductTypes.FilterableProductOptionProps, idOrSelector: string | ProductTypes.FilterableProductOptionProps,
@@ -1023,11 +1016,6 @@ export default class ProductModuleService
ProductTypes.ProductCollectionDTO[] ProductTypes.ProductCollectionDTO[]
>(collections) >(collections)
eventBuilders.createdProductCollection({
data: collections,
sharedContext,
})
return Array.isArray(data) ? createdCollections : createdCollections[0] return Array.isArray(data) ? createdCollections : createdCollections[0]
} }
@@ -1061,7 +1049,7 @@ export default class ProductModuleService
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductCollectionDTO> ): Promise<ProductTypes.ProductCollectionDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async upsertProductCollections( async upsertProductCollections(
data: data:
@@ -1071,6 +1059,24 @@ export default class ProductModuleService
): Promise< ): Promise<
ProductTypes.ProductCollectionDTO[] | ProductTypes.ProductCollectionDTO 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<InferEntityType<typeof ProductCollection>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(collection): collection is UpdateCollectionInput => !!collection.id (collection): collection is UpdateCollectionInput => !!collection.id
@@ -1091,26 +1097,7 @@ export default class ProductModuleService
updated = await this.updateCollections_(forUpdate, sharedContext) updated = await this.updateCollections_(forUpdate, sharedContext)
} }
const result = [...created, ...updated] return [...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]
} }
// @ts-expect-error // @ts-expect-error
@@ -1166,11 +1153,6 @@ export default class ProductModuleService
ProductTypes.ProductCollectionDTO[] ProductTypes.ProductCollectionDTO[]
>(collections) >(collections)
eventBuilders.updatedProductCollection({
data: updatedCollections,
sharedContext,
})
return isString(idOrSelector) ? updatedCollections[0] : updatedCollections return isString(idOrSelector) ? updatedCollections[0] : updatedCollections
} }
@@ -1283,6 +1265,7 @@ export default class ProductModuleService
ProductTypes.ProductCategoryDTO[] ProductTypes.ProductCategoryDTO[]
>(categories) >(categories)
// TODO: Same as the update categories, for some reason I cant get the tree repository update
eventBuilders.createdProductCategory({ eventBuilders.createdProductCategory({
data: createdCategories, data: createdCategories,
sharedContext, sharedContext,
@@ -1300,7 +1283,7 @@ export default class ProductModuleService
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductCategoryDTO> ): Promise<ProductTypes.ProductCategoryDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents() @EmitEvents()
async upsertProductCategories( async upsertProductCategories(
data: data:
@@ -1310,11 +1293,27 @@ export default class ProductModuleService
): Promise< ): Promise<
ProductTypes.ProductCategoryDTO[] | ProductTypes.ProductCategoryDTO 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<InferEntityType<typeof ProductCategory>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(category): category is UpdateCategoryInput => !!category.id (category): category is UpdateCategoryInput => !!category.id
) )
const forCreate = input.filter( let forCreate = input.filter(
(category): category is ProductTypes.CreateProductCategoryDTO => (category): category is ProductTypes.CreateProductCategoryDTO =>
!category.id !category.id
) )
@@ -1323,6 +1322,11 @@ export default class ProductModuleService
let updated: InferEntityType<typeof ProductCategory>[] = [] let updated: InferEntityType<typeof ProductCategory>[] = []
if (forCreate.length) { if (forCreate.length) {
forCreate = forCreate.map((productCategory) => {
productCategory.handle ??= kebabCase(productCategory.name)
return productCategory
})
created = await this.productCategoryService_.create( created = await this.productCategoryService_.create(
forCreate, forCreate,
sharedContext sharedContext
@@ -1335,25 +1339,22 @@ export default class ProductModuleService
) )
} }
const createdCategories = await this.baseRepository_.serialize< // TODO: Same as the update categories, for some reason I cant get the tree repository update
ProductTypes.ProductCategoryDTO[] // event. I ll need to investigate this
>(created) if (created.length) {
const updatedCategories = await this.baseRepository_.serialize< eventBuilders.createdProductCategory({
ProductTypes.ProductCategoryDTO[] data: created,
>(updated) sharedContext,
})
}
if (updated.length) {
eventBuilders.updatedProductCategory({
data: updated,
sharedContext,
})
}
eventBuilders.createdProductCategory({ return [...created, ...updated]
data: createdCategories,
sharedContext,
})
eventBuilders.updatedProductCategory({
data: updatedCategories,
sharedContext,
})
const result = [...createdCategories, ...updatedCategories]
return Array.isArray(data) ? result : result[0]
} }
// @ts-expect-error // @ts-expect-error
@@ -1377,8 +1378,36 @@ export default class ProductModuleService
data: ProductTypes.UpdateProductCategoryDTO, data: ProductTypes.UpdateProductCategoryDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise< ): 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<InferEntityType<typeof ProductCategory>[]> {
let normalizedInput: UpdateCategoryInput[] = [] let normalizedInput: UpdateCategoryInput[] = []
if (isString(idOrSelector)) { if (isString(idOrSelector)) {
// Check if the type exists in the first place // Check if the type exists in the first place
@@ -1406,16 +1435,7 @@ export default class ProductModuleService
sharedContext sharedContext
) )
const updatedCategories = await this.baseRepository_.serialize< return categories
ProductTypes.ProductCategoryDTO[]
>(categories)
eventBuilders.updatedProductCategory({
data: updatedCategories,
sharedContext,
})
return isString(idOrSelector) ? updatedCategories[0] : updatedCategories
} }
//@ts-expect-error //@ts-expect-error
@@ -1443,11 +1463,6 @@ export default class ProductModuleService
ProductTypes.ProductDTO[] ProductTypes.ProductDTO[]
>(products) >(products)
eventBuilders.createdProduct({
data: createdProducts,
sharedContext,
})
return Array.isArray(data) ? createdProducts : createdProducts[0] return Array.isArray(data) ? createdProducts : createdProducts[0]
} }
@@ -1474,11 +1489,11 @@ export default class ProductModuleService
(product): product is ProductTypes.CreateProductDTO => !product.id (product): product is ProductTypes.CreateProductDTO => !product.id
) )
let created: InferEntityType<typeof Product>[] = [] let created: ProductTypes.ProductDTO[] = []
let updated: InferEntityType<typeof Product>[] = [] let updated: InferEntityType<typeof Product>[] = []
if (forCreate.length) { if (forCreate.length) {
created = await this.createProducts_(forCreate, sharedContext) created = await this.createProducts(forCreate, sharedContext)
} }
if (forUpdate.length) { if (forUpdate.length) {
updated = await this.updateProducts_(forUpdate, sharedContext) updated = await this.updateProducts_(forUpdate, sharedContext)
@@ -1489,20 +1504,6 @@ export default class ProductModuleService
ProductTypes.ProductDTO[] | ProductTypes.ProductDTO ProductTypes.ProductDTO[] | ProductTypes.ProductDTO
>(result) >(result)
if (created.length) {
eventBuilders.createdProduct({
data: created,
sharedContext,
})
}
if (updated.length) {
eventBuilders.updatedProduct({
data: updated,
sharedContext,
})
}
return Array.isArray(data) ? allProducts : allProducts[0] return Array.isArray(data) ? allProducts : allProducts[0]
} }
@@ -1552,11 +1553,6 @@ export default class ProductModuleService
ProductTypes.ProductDTO[] ProductTypes.ProductDTO[]
>(products) >(products)
eventBuilders.updatedProduct({
data: updatedProducts,
sharedContext,
})
return isString(idOrSelector) ? updatedProducts[0] : updatedProducts return isString(idOrSelector) ? updatedProducts[0] : updatedProducts
} }
@@ -1657,20 +1653,47 @@ export default class ProductModuleService
data: UpdateProductInput[], data: UpdateProductInput[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof Product>[]> { ): Promise<InferEntityType<typeof Product>[]> {
// 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<typeof MedusaService<any>>
)
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( const normalizedProducts = await this.normalizeUpdateProductInput(
data, data,
sharedContext originalProducts
) )
for (const product of normalizedProducts) { for (const product of normalizedProducts) {
this.validateProductUpdatePayload(product) this.validateProductUpdatePayload(product)
} }
return this.productRepository_.deepUpdate( const updatedProducts = await this.productRepository_.deepUpdate(
normalizedProducts, normalizedProducts,
ProductModuleService.validateVariantOptions, ProductModuleService.validateVariantOptions,
sharedContext sharedContext
) )
return updatedProducts
} }
// @ts-expect-error // @ts-expect-error
@@ -1685,6 +1708,7 @@ export default class ProductModuleService
data: ProductTypes.UpdateProductOptionValueDTO, data: ProductTypes.UpdateProductOptionValueDTO,
sharedContext?: Context sharedContext?: Context
): Promise<ProductTypes.ProductOptionValueDTO[]> ): Promise<ProductTypes.ProductOptionValueDTO[]>
// @ts-expect-error // @ts-expect-error
async updateProductOptionValues( async updateProductOptionValues(
idOrSelector: string | FilterableProductOptionValueProps, idOrSelector: string | FilterableProductOptionValueProps,
@@ -1693,6 +1717,11 @@ export default class ProductModuleService
): Promise< ): Promise<
ProductTypes.ProductOptionValueDTO | ProductTypes.ProductOptionValueDTO[] 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: ({ let normalizedInput: ({
id: string id: string
} & ProductTypes.UpdateProductOptionValueDTO)[] = [] } & ProductTypes.UpdateProductOptionValueDTO)[] = []
@@ -1718,7 +1747,7 @@ export default class ProductModuleService
})) }))
} }
const productOptionValues = await super.updateProductOptionValues( const productOptionValues = await this.updateProductOptionValues_(
normalizedInput, normalizedInput,
sharedContext sharedContext
) )
@@ -1727,16 +1756,45 @@ export default class ProductModuleService
ProductTypes.ProductOptionValueDTO[] ProductTypes.ProductOptionValueDTO[]
>(productOptionValues) >(productOptionValues)
eventBuilders.updatedProductOptionValue({ // TODO: Because of the wrong method override, we have to compensate to prevent breaking
data: updatedProductOptionValues, // changes right now
sharedContext: sharedContext, const groupedEvents = sharedContext.messageAggregator!.getMessages()
}) if (
Object.values(groupedEvents).flat().length > 0 &&
this.eventBusModuleService_
) {
const promises: Promise<void>[] = []
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) return isString(idOrSelector)
? updatedProductOptionValues[0] ? updatedProductOptionValues[0]
: updatedProductOptionValues : updatedProductOptionValues
} }
@InjectTransactionManager()
protected async updateProductOptionValues_(
normalizedInput: ({
id: string
} & ProductTypes.UpdateProductOptionValueDTO)[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ProductOptionValue>[]> {
return await this.productOptionValueService_.update(
normalizedInput,
sharedContext
)
}
/** /**
* Validates the manually provided handle value of the product * Validates the manually provided handle value of the product
* to be URL-safe * to be URL-safe
@@ -1805,8 +1863,7 @@ export default class ProductModuleService
const products_ = Array.isArray(products) ? products : [products] const products_ = Array.isArray(products) ? products : [products]
const normalizedProducts = (await this.normalizeUpdateProductInput( const normalizedProducts = (await this.normalizeUpdateProductInput(
products_ as UpdateProductInput[], products_ as UpdateProductInput[]
sharedContext
)) as ProductTypes.CreateProductDTO[] )) as ProductTypes.CreateProductDTO[]
for (const productData of normalizedProducts) { for (const productData of normalizedProducts) {
@@ -1839,6 +1896,12 @@ export default class ProductModuleService
) as TOutput ) 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< protected async normalizeUpdateProductInput<
T extends UpdateProductInput | UpdateProductInput[], T extends UpdateProductInput | UpdateProductInput[],
TOutput = T extends UpdateProductInput[] TOutput = T extends UpdateProductInput[]
@@ -1846,7 +1909,7 @@ export default class ProductModuleService
: UpdateProductInput : UpdateProductInput
>( >(
products: T, products: T,
@MedusaContext() sharedContext: Context = {} originalProducts?: InferEntityType<typeof Product>[]
): Promise<TOutput> { ): Promise<TOutput> {
const products_ = Array.isArray(products) ? products : [products] const products_ = Array.isArray(products) ? products : [products]
const productsIds = products_.map((p) => p.id).filter(Boolean) const productsIds = products_.map((p) => p.id).filter(Boolean)
@@ -1854,11 +1917,14 @@ export default class ProductModuleService
let dbOptions: InferEntityType<typeof ProductOption>[] = [] let dbOptions: InferEntityType<typeof ProductOption>[] = []
if (productsIds.length) { if (productsIds.length) {
dbOptions = await this.productOptionService_.list( // Re map options to handle non serialized data as well
{ product_id: productsIds }, dbOptions =
{ relations: ["values"] }, originalProducts
sharedContext ?.map((originalProduct) =>
) originalProduct.options.map((option) => option)
)
.flat()
.filter(Boolean) ?? []
} }
const normalizedProducts: UpdateProductInput[] = [] const normalizedProducts: UpdateProductInput[] = []
@@ -1872,7 +1938,9 @@ export default class ProductModuleService
if (productData.options?.length) { if (productData.options?.length) {
;(productData as any).options = productData.options?.map((option) => { ;(productData as any).options = productData.options?.map((option) => {
const dbOption = dbOptions.find( 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 { return {
title: option.title, title: option.title,
@@ -6,96 +6,6 @@ import {
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
export const eventBuilders = { 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({ createdProductCategory: moduleEventBuilderFactory({
source: Modules.PRODUCT, source: Modules.PRODUCT,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
@@ -114,40 +24,4 @@ export const eventBuilders = {
object: "product_category", object: "product_category",
eventName: ProductEvents.PRODUCT_CATEGORY_DELETED, 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,
}),
} }
@@ -18,6 +18,7 @@ import {
CampaignBudgetType, CampaignBudgetType,
ComputedActions, ComputedActions,
deduplicate, deduplicate,
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isDefined, isDefined,
@@ -193,6 +194,7 @@ export default class PromotionModuleService
} }
@InjectTransactionManager() @InjectTransactionManager()
@EmitEvents()
async registerUsage( async registerUsage(
computedActions: PromotionTypes.UsageComputedActions[], computedActions: PromotionTypes.UsageComputedActions[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -306,6 +308,7 @@ export default class PromotionModuleService
} }
@InjectTransactionManager() @InjectTransactionManager()
@EmitEvents()
async revertUsage( async revertUsage(
computedActions: PromotionTypes.UsageComputedActions[], computedActions: PromotionTypes.UsageComputedActions[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -625,6 +628,7 @@ export default class PromotionModuleService
): Promise<PromotionTypes.PromotionDTO[]> ): Promise<PromotionTypes.PromotionDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createPromotions( async createPromotions(
data: data:
@@ -1041,6 +1045,7 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-ignore // @ts-ignore
async updatePromotionRules( async updatePromotionRules(
data: PromotionTypes.UpdatePromotionRuleDTO[], data: PromotionTypes.UpdatePromotionRuleDTO[],
@@ -1149,6 +1154,7 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async addPromotionTargetRules( async addPromotionTargetRules(
promotionId: string, promotionId: string,
rulesData: PromotionTypes.CreatePromotionRuleDTO[], rulesData: PromotionTypes.CreatePromotionRuleDTO[],
@@ -1182,6 +1188,7 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async addPromotionBuyRules( async addPromotionBuyRules(
promotionId: string, promotionId: string,
rulesData: PromotionTypes.CreatePromotionRuleDTO[], rulesData: PromotionTypes.CreatePromotionRuleDTO[],
@@ -1278,6 +1285,7 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async removePromotionRules( async removePromotionRules(
promotionId: string, promotionId: string,
ruleIds: string[], ruleIds: string[],
@@ -1305,6 +1313,7 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async removePromotionTargetRules( async removePromotionTargetRules(
promotionId: string, promotionId: string,
ruleIds: string[], ruleIds: string[],
@@ -1379,6 +1388,7 @@ export default class PromotionModuleService
): Promise<PromotionTypes.CampaignDTO[]> ): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createCampaigns( async createCampaigns(
data: PromotionTypes.CreateCampaignDTO | PromotionTypes.CreateCampaignDTO[], data: PromotionTypes.CreateCampaignDTO | PromotionTypes.CreateCampaignDTO[],
@@ -1490,6 +1500,7 @@ export default class PromotionModuleService
): Promise<PromotionTypes.CampaignDTO[]> ): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateCampaigns( async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO | PromotionTypes.UpdateCampaignDTO[], data: PromotionTypes.UpdateCampaignDTO | PromotionTypes.UpdateCampaignDTO[],
@@ -1583,9 +1594,10 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async addPromotionsToCampaign( async addPromotionsToCampaign(
data: PromotionTypes.AddPromotionsToCampaignDTO, data: PromotionTypes.AddPromotionsToCampaignDTO,
sharedContext?: Context @MedusaContext() sharedContext?: Context
): Promise<{ ids: string[] }> { ): Promise<{ ids: string[] }> {
const ids = await this.addPromotionsToCampaign_(data, sharedContext) const ids = await this.addPromotionsToCampaign_(data, sharedContext)
@@ -1650,9 +1662,10 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
async removePromotionsFromCampaign( async removePromotionsFromCampaign(
data: PromotionTypes.AddPromotionsToCampaignDTO, data: PromotionTypes.AddPromotionsToCampaignDTO,
sharedContext?: Context @MedusaContext() sharedContext: Context = {}
): Promise<{ ids: string[] }> { ): Promise<{ ids: string[] }> {
const ids = await this.removePromotionsFromCampaign_(data, sharedContext) const ids = await this.removePromotionsFromCampaign_(data, sharedContext)
@@ -15,6 +15,7 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
arrayDifference, arrayDifference,
EmitEvents,
getDuplicates, getDuplicates,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
@@ -78,6 +79,7 @@ export default class RegionModuleService
): Promise<RegionDTO> ): Promise<RegionDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createRegions( async createRegions(
data: CreateRegionDTO | CreateRegionDTO[], data: CreateRegionDTO | CreateRegionDTO[],
@@ -132,6 +134,7 @@ export default class RegionModuleService
} }
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async softDeleteRegions( async softDeleteRegions(
ids: string | object | string[] | object[], ids: string | object | string[] | object[],
@@ -160,11 +163,24 @@ export default class RegionModuleService
sharedContext?: Context sharedContext?: Context
): Promise<RegionDTO> ): Promise<RegionDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async upsertRegions( async upsertRegions(
data: UpsertRegionDTO | UpsertRegionDTO[], data: UpsertRegionDTO | UpsertRegionDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<RegionDTO | RegionDTO[]> { ): Promise<RegionDTO | RegionDTO[]> {
const result = await this.upsertRegions_(data, sharedContext)
return await this.baseRepository_.serialize<RegionDTO[] | RegionDTO>(
Array.isArray(data) ? result : result[0]
)
}
@InjectTransactionManager()
protected async upsertRegions_(
data: UpsertRegionDTO | UpsertRegionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof Region>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(region): region is UpdateRegionInput => !!region.id (region): region is UpdateRegionInput => !!region.id
@@ -183,9 +199,8 @@ export default class RegionModuleService
} }
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<RegionDTO[] | RegionDTO>(
Array.isArray(data) ? result : result[0] return result
)
} }
// @ts-expect-error // @ts-expect-error
@@ -202,6 +217,7 @@ export default class RegionModuleService
): Promise<RegionDTO[]> ): Promise<RegionDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateRegions( async updateRegions(
idOrSelector: string | FilterableRegionProps, idOrSelector: string | FilterableRegionProps,
@@ -13,6 +13,7 @@ import {
UpsertSalesChannelDTO, UpsertSalesChannelDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isString, isString,
@@ -67,6 +68,7 @@ export default class SalesChannelModuleService
): Promise<SalesChannelDTO> ): Promise<SalesChannelDTO>
@InjectManager() @InjectManager()
// @ts-expect-error // @ts-expect-error
async createSalesChannels( async createSalesChannels(
data: CreateSalesChannelDTO | CreateSalesChannelDTO[], data: CreateSalesChannelDTO | CreateSalesChannelDTO[],
@@ -77,10 +79,7 @@ export default class SalesChannelModuleService
const result = await this.createSalesChannels_(input, sharedContext) const result = await this.createSalesChannels_(input, sharedContext)
return await this.baseRepository_.serialize<SalesChannelDTO[]>( return await this.baseRepository_.serialize<SalesChannelDTO[]>(
Array.isArray(data) ? result : result[0], Array.isArray(data) ? result : result[0]
{
populate: true,
}
) )
} }
@@ -106,6 +105,7 @@ export default class SalesChannelModuleService
): Promise<SalesChannelDTO[]> ): Promise<SalesChannelDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateSalesChannels( async updateSalesChannels(
idOrSelector: string | FilterableSalesChannelProps, idOrSelector: string | FilterableSalesChannelProps,
@@ -134,10 +134,7 @@ export default class SalesChannelModuleService
) )
return await this.baseRepository_.serialize<SalesChannelDTO[]>( return await this.baseRepository_.serialize<SalesChannelDTO[]>(
Array.isArray(data) ? result : result[0], Array.isArray(data) ? result : result[0]
{
populate: true,
}
) )
} }
@@ -157,11 +154,25 @@ export default class SalesChannelModuleService
data: UpsertSalesChannelDTO, data: UpsertSalesChannelDTO,
sharedContext?: Context sharedContext?: Context
): Promise<SalesChannelDTO> ): Promise<SalesChannelDTO>
@InjectManager()
@EmitEvents()
@InjectTransactionManager() @InjectTransactionManager()
async upsertSalesChannels( async upsertSalesChannels(
data: UpsertSalesChannelDTO | UpsertSalesChannelDTO[], data: UpsertSalesChannelDTO | UpsertSalesChannelDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<SalesChannelDTO | SalesChannelDTO[]> { ): Promise<SalesChannelDTO | SalesChannelDTO[]> {
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<InferEntityType<typeof SalesChannel>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(channel): channel is UpdateSalesChannelDTO => !!channel.id (channel): channel is UpdateSalesChannelDTO => !!channel.id
@@ -180,8 +191,7 @@ export default class SalesChannelModuleService
} }
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<
SalesChannelDTO[] | SalesChannelDTO return result
>(Array.isArray(data) ? result : result[0])
} }
} }
@@ -7,6 +7,7 @@ import {
SettingsTypes, SettingsTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
@@ -50,7 +51,8 @@ export default class SettingsModuleService
this.userPreferenceService_ = userPreferenceService this.userPreferenceService_ = userPreferenceService
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createViewConfigurations( async createViewConfigurations(
data: data:
@@ -97,10 +99,14 @@ export default class SettingsModuleService
dataArray, dataArray,
sharedContext 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 // @ts-expect-error
async updateViewConfigurations( async updateViewConfigurations(
idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps, idOrSelector: string | SettingsTypes.FilterableViewConfigurationProps,
@@ -109,6 +115,25 @@ export default class SettingsModuleService
): Promise< ): Promise<
SettingsTypes.ViewConfigurationDTO | SettingsTypes.ViewConfigurationDTO[] 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<InferEntityType<typeof ViewConfiguration>[]> {
let selector: SettingsTypes.FilterableViewConfigurationProps = {} let selector: SettingsTypes.FilterableViewConfigurationProps = {}
if (typeof idOrSelector === "string") { if (typeof idOrSelector === "string") {
@@ -164,11 +189,7 @@ export default class SettingsModuleService
sharedContext sharedContext
) )
const serialized = await this.baseRepository_.serialize< return updatedEntities
SettingsTypes.ViewConfigurationDTO[]
>(updatedEntities, { populate: true })
return typeof idOrSelector === "string" ? serialized[0] : serialized
} }
// For non-configuration updates, use the standard update method // For non-configuration updates, use the standard update method
@@ -177,11 +198,7 @@ export default class SettingsModuleService
sharedContext sharedContext
) )
const serialized = await this.baseRepository_.serialize< return updated as unknown as InferEntityType<typeof ViewConfiguration>[]
SettingsTypes.ViewConfigurationDTO[]
>(updated, { populate: true })
return typeof idOrSelector === "string" ? serialized[0] : serialized
} }
@InjectManager() @InjectManager()
@@ -201,12 +218,12 @@ export default class SettingsModuleService
} }
return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>( return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>(
prefs[0], prefs[0]
{ populate: true }
) )
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async setUserPreference( async setUserPreference(
userId: string, userId: string,
key: string, key: string,
@@ -236,8 +253,7 @@ export default class SettingsModuleService
} }
return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>( return await this.baseRepository_.serialize<SettingsTypes.UserPreferenceDTO>(
result, result
{ populate: true }
) )
} }
@@ -297,7 +313,8 @@ export default class SettingsModuleService
return systemDefaults.length > 0 ? systemDefaults[0] : null return systemDefaults.length > 0 ? systemDefaults[0] : null
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async setActiveViewConfiguration( async setActiveViewConfiguration(
entity: string, entity: string,
userId: string, userId: string,
@@ -347,7 +364,8 @@ export default class SettingsModuleService
return systemDefaults.length > 0 ? systemDefaults[0] : null return systemDefaults.length > 0 ? systemDefaults[0] : null
} }
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async clearActiveViewConfiguration( async clearActiveViewConfiguration(
entity: string, entity: string,
userId: string, userId: string,
@@ -17,6 +17,7 @@ import {
UpsertStockLocationInput, UpsertStockLocationInput,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isString, isString,
@@ -86,7 +87,9 @@ export default class StockLocationModuleService
data: CreateStockLocationInput[], data: CreateStockLocationInput[],
context: Context context: Context
): Promise<StockLocationTypes.StockLocationDTO[]> ): Promise<StockLocationTypes.StockLocationDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createStockLocations( async createStockLocations(
data: CreateStockLocationInput | CreateStockLocationInput[], data: CreateStockLocationInput | CreateStockLocationInput[],
@@ -101,7 +104,7 @@ export default class StockLocationModuleService
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
| StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO
| StockLocationTypes.StockLocationDTO[] | StockLocationTypes.StockLocationDTO[]
>(created, { populate: true }) >(created)
return Array.isArray(data) ? serialized : serialized[0] return Array.isArray(data) ? serialized : serialized[0]
} }
@@ -124,6 +127,7 @@ export default class StockLocationModuleService
): Promise<StockLocationTypes.StockLocationDTO[]> ): Promise<StockLocationTypes.StockLocationDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async upsertStockLocations( async upsertStockLocations(
data: UpsertStockLocationInput | UpsertStockLocationInput[], data: UpsertStockLocationInput | UpsertStockLocationInput[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
@@ -179,6 +183,7 @@ export default class StockLocationModuleService
input: UpdateStockLocationInput, input: UpdateStockLocationInput,
context?: Context context?: Context
): Promise<StockLocationTypes.StockLocationDTO[]> ): Promise<StockLocationTypes.StockLocationDTO[]>
/** /**
* Updates an existing stock location. * Updates an existing stock location.
* @param stockLocationId - The ID of the stock location to update. * @param stockLocationId - The ID of the stock location to update.
@@ -187,6 +192,7 @@ export default class StockLocationModuleService
* @returns The updated stock location. * @returns The updated stock location.
*/ */
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateStockLocations( async updateStockLocations(
idOrSelector: string | FilterableStockLocationProps, idOrSelector: string | FilterableStockLocationProps,
@@ -208,7 +214,7 @@ export default class StockLocationModuleService
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
| StockLocationTypes.StockLocationDTO | StockLocationTypes.StockLocationDTO
| StockLocationTypes.StockLocationDTO[] | StockLocationTypes.StockLocationDTO[]
>(updated, { populate: true }) >(updated)
return Array.isArray(data) ? serialized : serialized[0] return Array.isArray(data) ? serialized : serialized[0]
} }
@@ -239,6 +245,7 @@ export default class StockLocationModuleService
): Promise<StockLocationTypes.StockLocationAddressDTO[]> ): Promise<StockLocationTypes.StockLocationAddressDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateStockLocationAddresses( async updateStockLocationAddresses(
data: data:
@@ -253,7 +260,7 @@ export default class StockLocationModuleService
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
| StockLocationTypes.StockLocationAddressDTO | StockLocationTypes.StockLocationAddressDTO
| StockLocationTypes.StockLocationAddressDTO[] | StockLocationTypes.StockLocationAddressDTO[]
>(updated, { populate: true }) >(updated)
return Array.isArray(data) ? serialized : serialized[0] return Array.isArray(data) ? serialized : serialized[0]
} }
@@ -276,6 +283,7 @@ export default class StockLocationModuleService
): Promise<StockLocationTypes.StockLocationAddressDTO[]> ): Promise<StockLocationTypes.StockLocationAddressDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
async upsertStockLocationAddresses( async upsertStockLocationAddresses(
data: UpsertStockLocationAddressInput | UpsertStockLocationAddressInput[], data: UpsertStockLocationAddressInput | UpsertStockLocationAddressInput[],
@MedusaContext() context: Context = {} @MedusaContext() context: Context = {}
@@ -8,6 +8,7 @@ import {
StoreTypes, StoreTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
getDuplicates, getDuplicates,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
@@ -59,7 +60,9 @@ export default class StoreModuleService
data: StoreTypes.CreateStoreDTO, data: StoreTypes.CreateStoreDTO,
sharedContext?: Context sharedContext?: Context
): Promise<StoreTypes.StoreDTO> ): Promise<StoreTypes.StoreDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createStores( async createStores(
data: StoreTypes.CreateStoreDTO | StoreTypes.CreateStoreDTO[], data: StoreTypes.CreateStoreDTO | StoreTypes.CreateStoreDTO[],
@@ -99,11 +102,25 @@ export default class StoreModuleService
data: StoreTypes.UpsertStoreDTO, data: StoreTypes.UpsertStoreDTO,
sharedContext?: Context sharedContext?: Context
): Promise<StoreTypes.StoreDTO> ): Promise<StoreTypes.StoreDTO>
@InjectTransactionManager()
@InjectManager()
@EmitEvents()
async upsertStores( async upsertStores(
data: StoreTypes.UpsertStoreDTO | StoreTypes.UpsertStoreDTO[], data: StoreTypes.UpsertStoreDTO | StoreTypes.UpsertStoreDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<StoreTypes.StoreDTO | StoreTypes.StoreDTO[]> { ): Promise<StoreTypes.StoreDTO | StoreTypes.StoreDTO[]> {
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<InferEntityType<typeof Store>[]> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter( const forUpdate = input.filter(
(store): store is UpdateStoreInput => !!store.id (store): store is UpdateStoreInput => !!store.id
@@ -122,9 +139,8 @@ export default class StoreModuleService
} }
const result = (await promiseAll(operations)).flat() const result = (await promiseAll(operations)).flat()
return await this.baseRepository_.serialize<
StoreTypes.StoreDTO[] | StoreTypes.StoreDTO return result
>(Array.isArray(data) ? result : result[0])
} }
// @ts-expect-error // @ts-expect-error
@@ -139,7 +155,9 @@ export default class StoreModuleService
data: StoreTypes.UpdateStoreDTO, data: StoreTypes.UpdateStoreDTO,
sharedContext?: Context sharedContext?: Context
): Promise<StoreTypes.StoreDTO[]> ): Promise<StoreTypes.StoreDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateStores( async updateStores(
idOrSelector: string | StoreTypes.FilterableStoreProps, idOrSelector: string | StoreTypes.FilterableStoreProps,
@@ -10,6 +10,7 @@ import {
TaxTypes, TaxTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
EmitEvents,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isDefined, isDefined,
@@ -17,7 +18,6 @@ import {
MedusaContext, MedusaContext,
MedusaError, MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
promiseAll,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models" import { TaxProvider, TaxRate, TaxRateRule, TaxRegion } from "@models"
import { TaxProviderService } from "@services" import { TaxProviderService } from "@services"
@@ -94,6 +94,7 @@ export default class TaxModuleService
): Promise<TaxTypes.TaxRateDTO> ): Promise<TaxTypes.TaxRateDTO>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createTaxRates( async createTaxRates(
data: TaxTypes.CreateTaxRateDTO[] | TaxTypes.CreateTaxRateDTO, data: TaxTypes.CreateTaxRateDTO[] | TaxTypes.CreateTaxRateDTO,
@@ -101,7 +102,12 @@ export default class TaxModuleService
): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> { ): Promise<TaxTypes.TaxRateDTO[] | TaxTypes.TaxRateDTO> {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const rates = await this.createTaxRates_(input, sharedContext) 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() @InjectTransactionManager()
@@ -145,9 +151,7 @@ export default class TaxModuleService
await this.taxRateRuleService_.create(rulesToCreate, sharedContext) await this.taxRateRuleService_.create(rulesToCreate, sharedContext)
} }
return await this.baseRepository_.serialize<TaxTypes.TaxRateDTO[]>(rates, { return rates
populate: true,
})
} }
// @ts-expect-error // @ts-expect-error
@@ -170,6 +174,7 @@ export default class TaxModuleService
): Promise<TaxTypes.TaxRateDTO[]> ): Promise<TaxTypes.TaxRateDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async updateTaxRates( async updateTaxRates(
selector: string | string[] | TaxTypes.FilterableTaxRateProps, selector: string | string[] | TaxTypes.FilterableTaxRateProps,
@@ -177,9 +182,11 @@ export default class TaxModuleService
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> { ): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const rates = await this.updateTaxRates_(selector, data, sharedContext) const rates = await this.updateTaxRates_(selector, data, sharedContext)
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[] TaxTypes.TaxRateDTO[]
>(rates, { populate: true }) >(rates)
return isString(selector) ? serialized[0] : serialized return isString(selector) ? serialized[0] : serialized
} }
@@ -282,15 +289,18 @@ export default class TaxModuleService
sharedContext?: Context sharedContext?: Context
): Promise<TaxTypes.TaxRateDTO> ): Promise<TaxTypes.TaxRateDTO>
@InjectTransactionManager() @InjectManager()
@EmitEvents()
async upsertTaxRates( async upsertTaxRates(
data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[], data: TaxTypes.UpsertTaxRateDTO | TaxTypes.UpsertTaxRateDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> { ): Promise<TaxTypes.TaxRateDTO | TaxTypes.TaxRateDTO[]> {
const result = await this.taxRateService_.upsert(data, sharedContext) const result = await this.taxRateService_.upsert(data, sharedContext)
const serialized = await this.baseRepository_.serialize< const serialized = await this.baseRepository_.serialize<
TaxTypes.TaxRateDTO[] TaxTypes.TaxRateDTO[]
>(result, { populate: true }) >(result)
return Array.isArray(data) ? serialized : serialized[0] return Array.isArray(data) ? serialized : serialized[0]
} }
@@ -307,6 +317,7 @@ export default class TaxModuleService
): Promise<TaxRegionDTO[]> ): Promise<TaxRegionDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createTaxRegions( async createTaxRegions(
data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[], data: TaxTypes.CreateTaxRegionDTO | TaxTypes.CreateTaxRegionDTO[],
@@ -314,9 +325,15 @@ export default class TaxModuleService
) { ) {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRegions_(input, sharedContext) 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_( async createTaxRegions_(
data: TaxTypes.CreateTaxRegionDTO[], data: TaxTypes.CreateTaxRegionDTO[],
sharedContext: Context = {} sharedContext: Context = {}
@@ -347,10 +364,7 @@ export default class TaxModuleService
await this.createTaxRates(rates, sharedContext) await this.createTaxRates(rates, sharedContext)
} }
return await this.baseRepository_.serialize<TaxTypes.TaxRegionDTO[]>( return regions
regions,
{ populate: true }
)
} }
// @ts-expect-error // @ts-expect-error
@@ -365,6 +379,7 @@ export default class TaxModuleService
): Promise<TaxTypes.TaxRateRuleDTO[]> ): Promise<TaxTypes.TaxRateRuleDTO[]>
@InjectManager() @InjectManager()
@EmitEvents()
// @ts-expect-error // @ts-expect-error
async createTaxRateRules( async createTaxRateRules(
data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[], data: TaxTypes.CreateTaxRateRuleDTO | TaxTypes.CreateTaxRateRuleDTO[],
@@ -372,7 +387,12 @@ export default class TaxModuleService
) { ) {
const input = Array.isArray(data) ? data : [data] const input = Array.isArray(data) ? data : [data]
const result = await this.createTaxRateRules_(input, sharedContext) 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() @InjectTransactionManager()
@@ -381,12 +401,7 @@ export default class TaxModuleService
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
) { ) {
const rules = await this.taxRateRuleService_.create(data, sharedContext) const rules = await this.taxRateRuleService_.create(data, sharedContext)
return await this.baseRepository_.serialize<TaxTypes.TaxRateRuleDTO[]>( return rules
rules,
{
populate: true,
}
)
} }
@InjectManager() @InjectManager()
@@ -419,30 +434,71 @@ export default class TaxModuleService
return [] return []
} }
const toReturn = await promiseAll( const regionIds = regions.map((r) => r.id)
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 applicableRates = await this.getTaxRatesForItem( // Collect all unique reference IDs for batch query
item, const productIds = new Set<string>()
candidateRates const productTypeIds = new Set<string>()
) const shippingOptionIds = new Set<string>()
return { items.forEach((item) => {
rates: applicableRates, if ("shipping_option_id" in item) {
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( const taxLines = await this.getTaxLinesFromProvider(
parentRegion.provider_id as string, parentRegion.provider_id as string,
toReturn, toReturn,
@@ -574,10 +630,10 @@ export default class TaxModuleService
} }
} }
private async getTaxRatesForItem( private getTaxRatesForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
rates: InferEntityType<typeof TaxRate>[] rates: InferEntityType<typeof TaxRate>[]
): Promise<InferEntityType<typeof TaxRate>[]> { ): InferEntityType<typeof TaxRate>[] {
if (!rates.length) { if (!rates.length) {
return [] return []
} }
@@ -609,7 +665,17 @@ export default class TaxModuleService
private getTaxRateQueryForItem( private getTaxRateQueryForItem(
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO, item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO,
regionIds: string[] 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 const isShipping = "shipping_option_id" in item
let ruleQuery = isShipping let ruleQuery = isShipping
? [ ? [
@@ -637,6 +703,58 @@ export default class TaxModuleService
} }
} }
private rateMatchesQuery(
rate: InferEntityType<typeof TaxRate>,
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<typeof TaxRateRule>) =>
rule.reference === ruleQuery.reference &&
rule.reference_id === ruleQuery.reference_id
)
) {
return true
}
}
}
}
return false
}
private checkRuleMatches( private checkRuleMatches(
rate: InferEntityType<typeof TaxRate>, rate: InferEntityType<typeof TaxRate>,
item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO item: TaxTypes.TaxableItemDTO | TaxTypes.TaxableShippingDTO
@@ -172,17 +172,20 @@ moduleIntegrationTestRunner<IUserModuleService>({
}, },
]) ])
expect(eventBusSpy).toHaveBeenCalledTimes(1) // 2 events: 1 invite updated, 1 invite token generated
expect(eventBusSpy).toHaveBeenCalledWith( const events = eventBusSpy.mock.calls[0][0]
[ expect(events).toHaveLength(2)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
data: { id: "1" }, data: { id: "1" },
name: UserEvents.INVITE_UPDATED, name: UserEvents.INVITE_UPDATED,
}), }),
], expect.objectContaining({
{ data: { id: "1" },
internal: true, name: UserEvents.INVITE_TOKEN_GENERATED,
} }),
])
) )
}) })
}) })
@@ -190,21 +193,25 @@ moduleIntegrationTestRunner<IUserModuleService>({
describe("resendInvite", () => { describe("resendInvite", () => {
it("should emit token generated event for invites", async () => { it("should emit token generated event for invites", async () => {
await service.createInvites(defaultInviteData) await service.createInvites(defaultInviteData)
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
eventBusSpy.mockClear()
await service.refreshInviteTokens(["1"]) await service.refreshInviteTokens(["1"])
expect(eventBusSpy).toHaveBeenCalledTimes(2) const events = eventBusSpy.mock.calls[0][0]
expect(eventBusSpy).toHaveBeenCalledWith( expect(events).toHaveLength(2)
[ expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: UserEvents.INVITE_UPDATED,
data: { id: "1" }, data: { id: "1" },
name: UserEvents.INVITE_TOKEN_GENERATED,
}), }),
], expect.objectContaining({
{ name: UserEvents.INVITE_TOKEN_GENERATED,
internal: true, data: { id: "1" },
} }),
])
) )
}) })
}) })
@@ -251,21 +258,25 @@ moduleIntegrationTestRunner<IUserModuleService>({
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
await service.createInvites(defaultInviteData) await service.createInvites(defaultInviteData)
expect(eventBusSpy).toHaveBeenCalledTimes(1) // 4 events: 2 invites created, 2 invite token generated
expect(eventBusSpy).toHaveBeenCalledWith( const events = eventBusSpy.mock.calls[0][0]
[ expect(events).toHaveLength(3)
expect(events).toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
data: { id: ["1", "2"] }, data: { id: "1" },
name: UserEvents.INVITE_CREATED,
}),
expect.objectContaining({
data: { id: "2" },
name: UserEvents.INVITE_CREATED, name: UserEvents.INVITE_CREATED,
}), }),
expect.objectContaining({ expect.objectContaining({
data: { id: ["1", "2"] }, data: { id: ["1", "2"] },
name: UserEvents.INVITE_TOKEN_GENERATED, name: UserEvents.INVITE_TOKEN_GENERATED,
}), }),
], ])
{
internal: true,
}
) )
}) })
}) })
@@ -253,17 +253,22 @@ moduleIntegrationTestRunner<IUserModuleService>({
const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit") const eventBusSpy = jest.spyOn(MockEventBusService.prototype, "emit")
await service.createUsers(defaultUserData) await service.createUsers(defaultUserData)
expect(eventBusSpy).toHaveBeenCalledTimes(1) // 2 events: 2 user created
expect(eventBusSpy).toHaveBeenCalledWith( 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({ expect.objectContaining({
data: { id: ["1", "2"] }, data: { id: "1" },
name: UserEvents.USER_CREATED, name: UserEvents.USER_CREATED,
}), }),
], expect.objectContaining({
{ data: { id: "2" },
internal: true, name: UserEvents.USER_CREATED,
} }),
])
) )
}) })
}) })
@@ -95,7 +95,7 @@ export default class UserModuleService
} }
} }
@InjectTransactionManager() @InjectManager()
async validateInviteToken( async validateInviteToken(
token: string, token: string,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -129,9 +129,7 @@ export default class UserModuleService
) )
} }
return await this.baseRepository_.serialize<UserTypes.InviteDTO>(invite, { return await this.baseRepository_.serialize<UserTypes.InviteDTO>(invite)
populate: true,
})
} }
@InjectManager() @InjectManager()
@@ -142,22 +140,21 @@ export default class UserModuleService
): Promise<UserTypes.InviteDTO[]> { ): Promise<UserTypes.InviteDTO[]> {
const invites = await this.refreshInviteTokens_(inviteIds, sharedContext) const invites = await this.refreshInviteTokens_(inviteIds, sharedContext)
const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[]
>(invites)
moduleEventBuilderFactory({ moduleEventBuilderFactory({
eventName: UserEvents.INVITE_TOKEN_GENERATED, eventName: UserEvents.INVITE_TOKEN_GENERATED,
source: Modules.USER, source: Modules.USER,
action: "token_generated", action: CommonEvents.CREATED,
object: "invite", object: "invite",
})({ })({
data: invites, data: serializedInvites,
sharedContext, sharedContext,
}) })
return await this.baseRepository_.serialize<UserTypes.InviteDTO[]>( return serializedInvites
invites,
{
populate: true,
}
)
} }
@InjectTransactionManager() @InjectTransactionManager()
@@ -221,19 +218,7 @@ export default class UserModuleService
const serializedUsers = await this.baseRepository_.serialize< const serializedUsers = await this.baseRepository_.serialize<
UserTypes.UserDTO[] | UserTypes.UserDTO UserTypes.UserDTO[] | UserTypes.UserDTO
>(users, { >(users)
populate: true,
})
moduleEventBuilderFactory({
eventName: UserEvents.USER_CREATED,
source: Modules.USER,
action: CommonEvents.CREATED,
object: "user",
})({
data: serializedUsers,
sharedContext,
})
return Array.isArray(data) ? serializedUsers : serializedUsers[0] return Array.isArray(data) ? serializedUsers : serializedUsers[0]
} }
@@ -262,19 +247,7 @@ export default class UserModuleService
const serializedUsers = await this.baseRepository_.serialize< const serializedUsers = await this.baseRepository_.serialize<
UserTypes.UserDTO[] UserTypes.UserDTO[]
>(updatedUsers, { >(updatedUsers)
populate: true,
})
moduleEventBuilderFactory({
eventName: UserEvents.USER_UPDATED,
source: Modules.USER,
action: CommonEvents.UPDATED,
object: "user",
})({
data: serializedUsers,
sharedContext,
})
return Array.isArray(data) ? serializedUsers : serializedUsers[0] return Array.isArray(data) ? serializedUsers : serializedUsers[0]
} }
@@ -303,19 +276,7 @@ export default class UserModuleService
const serializedInvites = await this.baseRepository_.serialize< const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[] | UserTypes.InviteDTO UserTypes.InviteDTO[] | UserTypes.InviteDTO
>(invites, { >(invites)
populate: true,
})
moduleEventBuilderFactory({
eventName: UserEvents.INVITE_CREATED,
source: Modules.USER,
action: CommonEvents.CREATED,
object: "invite",
})({
data: serializedInvites,
sharedContext,
})
moduleEventBuilderFactory({ moduleEventBuilderFactory({
eventName: UserEvents.INVITE_TOKEN_GENERATED, eventName: UserEvents.INVITE_TOKEN_GENERATED,
@@ -390,14 +351,12 @@ export default class UserModuleService
const serializedInvites = await this.baseRepository_.serialize< const serializedInvites = await this.baseRepository_.serialize<
UserTypes.InviteDTO[] UserTypes.InviteDTO[]
>(updatedInvites, { >(updatedInvites)
populate: true,
})
moduleEventBuilderFactory({ moduleEventBuilderFactory({
eventName: UserEvents.INVITE_UPDATED, eventName: UserEvents.INVITE_TOKEN_GENERATED,
source: Modules.USER, source: Modules.USER,
action: CommonEvents.UPDATED, action: "token_generated",
object: "invite", object: "invite",
})({ })({
data: serializedInvites, data: serializedInvites,