feat: Event emitting part 1/N (Fulfillment) (#7391)

**What**
Add support for event emitting in the fulfillment module

**NOTE**
It does not include the review of the events for the abstract module factory if the method is not implemented in the module itself and rely on the default implementation
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-06-03 08:29:35 +00:00
1 parent 28d2a5347a
commit 9608bf06ef
28 files changed
+1660 -292

No files matched your search

@@ -60,11 +60,29 @@ export type UpdateStockLocationSalesChannelsReq = {
} }
export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO
export type CreateServiceZoneReq = CreateServiceZoneDTO export type CreateServiceZoneReq = CreateServiceZoneDTO
export type UpdateServiceZoneReq = UpdateServiceZoneDTO export type UpdateServiceZoneReq =
| UpdateServiceZoneDTO
// Shipping Options // Shipping Options
export type CreateShippingOptionReq = CreateShippingOptionDTO | { region_id: string; amount: number }
export type UpdateShippingOptionReq = UpdateShippingOptionDTO export type CreateShippingOptionReq = CreateShippingOptionDTO & {
/**
* The shipping option pricing
*/
prices: (
| { currency_code: string; amount: number }
| { region_id: string; amount: number }
)[]
}
export type UpdateShippingOptionReq = UpdateShippingOptionDTO & {
/**
* The shipping option pricing
*/
prices: (
| { currency_code: string; amount: number; id?: string }
| { region_id: string; amount: number; id?: string }
)[]
}
// Shipping Profile // Shipping Profile
export type CreateShippingProfileReq = CreateShippingProfileDTO export type CreateShippingProfileReq = CreateShippingProfileDTO
@@ -14,7 +14,7 @@ export interface SuiteOptions<TService = unknown> {
} }
} }
export function moduleIntegrationTestRunner({ export function moduleIntegrationTestRunner<TService = any>({
moduleName, moduleName,
moduleModels, moduleModels,
moduleOptions = {}, moduleOptions = {},
@@ -34,7 +34,7 @@ export function moduleIntegrationTestRunner({
injectedDependencies?: Record<string, any> injectedDependencies?: Record<string, any>
resolve?: string resolve?: string
debug?: boolean debug?: boolean
testSuite: <TService = unknown>(options: SuiteOptions<TService>) => void testSuite: (options: SuiteOptions<TService>) => void
}) { }) {
const moduleSdkImports = require("@medusajs/modules-sdk") const moduleSdkImports = require("@medusajs/modules-sdk")
@@ -111,7 +111,7 @@ export function moduleIntegrationTestRunner({
schema, schema,
clientUrl: dbConfig.clientUrl, clientUrl: dbConfig.clientUrl,
}, },
} as SuiteOptions } as SuiteOptions<TService>
const beforeEach_ = async () => { const beforeEach_ = async () => {
await MikroOrmWrapper.setupDatabase() await MikroOrmWrapper.setupDatabase()
+7 -6
View File
@@ -1,3 +1,5 @@
import { Context } from "../shared-context"
export type Subscriber<T = unknown> = ( export type Subscriber<T = unknown> = (
data: T, data: T,
eventName: string eventName: string
@@ -39,13 +41,12 @@ export type Message<T = unknown> = {
options?: Record<string, unknown> options?: Record<string, unknown>
} }
export type MessageFormat<T = unknown> = { export type RawMessageFormat<T = any> = {
eventName: string eventName: string
metadata: { data: T
service: string service: string
action: string
object: string object: string
eventGroupId?: string action?: string
} context?: Pick<Context, "eventGroupId">
data: T | T[] options?: Record<string, any>
} }
@@ -110,5 +110,8 @@ export interface UpdateFulfillmentDTO {
/** /**
* The labels associated with the fulfillment. * The labels associated with the fulfillment.
*/ */
labels?: Omit<CreateFulfillmentLabelDTO, "fulfillment_id">[] labels?: (
| Omit<CreateFulfillmentLabelDTO, "fulfillment_id">
| { id: string }
)[]
} }
@@ -3,6 +3,10 @@ import {
CreateCountryGeoZoneDTO, CreateCountryGeoZoneDTO,
CreateProvinceGeoZoneDTO, CreateProvinceGeoZoneDTO,
CreateZipGeoZoneDTO, CreateZipGeoZoneDTO,
UpdateCityGeoZoneDTO,
UpdateCountryGeoZoneDTO,
UpdateProvinceGeoZoneDTO,
UpdateZipGeoZoneDTO,
} from "./geo-zone" } from "./geo-zone"
/** /**
@@ -52,6 +56,10 @@ export interface UpdateServiceZoneDTO {
| Omit<CreateProvinceGeoZoneDTO, "service_zone_id"> | Omit<CreateProvinceGeoZoneDTO, "service_zone_id">
| Omit<CreateCityGeoZoneDTO, "service_zone_id"> | Omit<CreateCityGeoZoneDTO, "service_zone_id">
| Omit<CreateZipGeoZoneDTO, "service_zone_id"> | Omit<CreateZipGeoZoneDTO, "service_zone_id">
| UpdateCountryGeoZoneDTO
| UpdateProvinceGeoZoneDTO
| UpdateCityGeoZoneDTO
| UpdateZipGeoZoneDTO
| { | {
/** /**
* The ID of the geo zone. * The ID of the geo zone.
@@ -46,14 +46,6 @@ export interface CreateShippingOptionDTO {
* The shipping option rules associated with the shipping option. * The shipping option rules associated with the shipping option.
*/ */
rules?: Omit<CreateShippingOptionRuleDTO, "shipping_option_id">[] rules?: Omit<CreateShippingOptionRuleDTO, "shipping_option_id">[]
/**
* The shipping option pricing
*/
prices: (
| { currency_code: string; amount: number }
| { region_id: string; amount: number }
)[]
} }
/** /**
@@ -120,14 +112,6 @@ export interface UpdateShippingOptionDTO {
id: string id: string
} }
)[] )[]
/**
* The shipping option pricing
*/
prices: (
| { currency_code: string; amount: number; id?: string }
| { region_id: string; amount: number; id?: string }
)[]
} }
/** /**
+2 -2
View File
@@ -30,8 +30,8 @@ export interface IMessageAggregator {
clearMessages(): void clearMessages(): void
saveRawMessageData<T>( saveRawMessageData<T>(
messageData: messageData:
| EventBusTypes.MessageFormat<T> | EventBusTypes.RawMessageFormat<T>
| EventBusTypes.MessageFormat<T>[], | EventBusTypes.RawMessageFormat<T>[],
options?: Record<string, unknown> options?: Record<string, unknown>
): void ): void
} }
@@ -1,39 +1,4 @@
import { Context, EventBusTypes } from "@medusajs/types" import { Context, EventBusTypes } from "@medusajs/types"
import { CommonEvents } from "./common-events"
/**
* Build messages from message data to be consumed by the event bus and emitted to the consumer
* @param MessageFormat
* @param options
*/
export function buildEventMessages<T>(
messageData:
| EventBusTypes.MessageFormat<T>
| EventBusTypes.MessageFormat<T>[],
options?: Record<string, unknown>
): EventBusTypes.Message<T>[] {
const messageData_ = Array.isArray(messageData) ? messageData : [messageData]
const messages: EventBusTypes.Message<any>[] = []
messageData_.map((data) => {
const data_ = Array.isArray(data.data) ? data.data : [data.data]
data_.forEach((bodyData) => {
const message = composeMessage(data.eventName, {
data: bodyData,
service: data.metadata.service,
entity: data.metadata.object,
action: data.metadata.action,
context: {
eventGroupId: data.metadata.eventGroupId,
} as Context,
options,
})
messages.push(message)
})
})
return messages
}
/** /**
* Helper function to compose and normalize a Message to be emitted by EventBus Module * Helper function to compose and normalize a Message to be emitted by EventBus Module
@@ -48,27 +13,29 @@ export function composeMessage(
{ {
data, data,
service, service,
entity, object,
action, action,
context = {}, context = {},
options, options,
}: { }: {
data: unknown data: any
service: string service: string
entity: string object: string
action?: string action?: string
context?: Context context?: Context
options?: Record<string, unknown> options?: Record<string, any>
} }
): EventBusTypes.Message { ): EventBusTypes.Message {
const act = action || eventName.split(".").pop() const act = action || eventName.split(".").pop()
if (!action && !Object.values(CommonEvents).includes(act as CommonEvents)) { if (
!action /* && !Object.values(CommonEvents).includes(act as CommonEvents)*/
) {
throw new Error("Action is required if eventName is not a CommonEvent") throw new Error("Action is required if eventName is not a CommonEvent")
} }
const metadata: EventBusTypes.MessageBody["metadata"] = { const metadata: EventBusTypes.MessageBody["metadata"] = {
service, service,
object: entity, object,
action: act!, action: act!,
} }
@@ -1,11 +1,12 @@
import { import {
Context,
EventBusTypes, EventBusTypes,
IMessageAggregator, IMessageAggregator,
Message, Message,
MessageAggregatorFormat, MessageAggregatorFormat,
} from "@medusajs/types" } from "@medusajs/types"
import { buildEventMessages } from "./build-event-messages" import { composeMessage } from "./build-event-messages"
export class MessageAggregator implements IMessageAggregator { export class MessageAggregator implements IMessageAggregator {
private messages: Message[] private messages: Message[]
@@ -28,11 +29,25 @@ export class MessageAggregator implements IMessageAggregator {
saveRawMessageData<T>( saveRawMessageData<T>(
messageData: messageData:
| EventBusTypes.MessageFormat<T> | EventBusTypes.RawMessageFormat<T>
| EventBusTypes.MessageFormat<T>[], | EventBusTypes.RawMessageFormat<T>[],
options?: Record<string, unknown> {
options,
sharedContext,
}: { options?: Record<string, unknown>; sharedContext?: Context } = {}
): void { ): void {
this.save(buildEventMessages(messageData, options)) const messages = Array.isArray(messageData) ? messageData : [messageData]
const composedMessages = messages.map((message) => {
return composeMessage(message.eventName, {
data: message.data,
service: message.service,
object: message.object,
action: message.action,
options,
context: sharedContext,
})
})
this.save(composedMessages)
} }
getMessages(format?: MessageAggregatorFormat): { getMessages(format?: MessageAggregatorFormat): {
+6 -6
View File
@@ -58,7 +58,8 @@ type ReturnType<TNames extends string[]> = TNames extends [
* @param names * @param names
*/ */
export function buildEventNamesFromEntityName<TNames extends string[]>( export function buildEventNamesFromEntityName<TNames extends string[]>(
names: TNames names: TNames,
prefix?: string
): ReturnType<TNames> { ): ReturnType<TNames> {
const events = {} const events = {}
@@ -69,16 +70,15 @@ export function buildEventNamesFromEntityName<TNames extends string[]>(
if (i === 0) { if (i === 0) {
for (const event of Object.values(CommonEvents) as string[]) { for (const event of Object.values(CommonEvents) as string[]) {
events[event] = `${kebabCaseName}.${event}` events[event] = `${prefix ? prefix + "." : ""}${kebabCaseName}.${event}`
} }
continue continue
} }
for (const event of Object.values(CommonEvents) as string[]) { for (const event of Object.values(CommonEvents) as string[]) {
events[`${snakedCaseName}_${event}`] = events[`${snakedCaseName}_${event}`] = `${
`${kebabCaseName}.${event}` as `${KebabCase< prefix ? prefix + "." : ""
typeof name }${kebabCaseName}.${event}` as `${KebabCase<typeof name>}.${typeof event}`
>}.${typeof event}`
} }
} }
+14 -2
View File
@@ -1,21 +1,33 @@
import { buildEventNamesFromEntityName } from "../event-bus" import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
const eventBaseNames: [ const eventBaseNames: [
"fulfillmentSet", "fulfillmentSet",
"serviceZone", "serviceZone",
"geoZone", "geoZone",
"shippingOption", "shippingOption",
"shippingOptionType",
"shippingProfile", "shippingProfile",
"shippingOptionRule", "shippingOptionRule",
"fulfillment" "fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel"
] = [ ] = [
"fulfillmentSet", "fulfillmentSet",
"serviceZone", "serviceZone",
"geoZone", "geoZone",
"shippingOption", "shippingOption",
"shippingOptionType",
"shippingProfile", "shippingProfile",
"shippingOptionRule", "shippingOptionRule",
"fulfillment", "fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel",
] ]
export const FulfillmentEvents = buildEventNamesFromEntityName(eventBaseNames) export const FulfillmentEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.FULFILLMENT
)
+12 -13
View File
@@ -1,14 +1,13 @@
import { CommonEvents } from "../event-bus" import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
export const InventoryEvents = { const eventBaseNames: ["inventoryItem", "reservationItem", "inventoryLevel"] = [
created: "inventory-item." + CommonEvents.CREATED, "inventoryItem",
updated: "inventory-item." + CommonEvents.UPDATED, "reservationItem",
deleted: "inventory-item." + CommonEvents.DELETED, "inventoryLevel",
restored: "inventory-item." + CommonEvents.RESTORED, ]
reservation_item_created: "reservation-item." + CommonEvents.CREATED,
reservation_item_updated: "reservation-item." + CommonEvents.UPDATED, export const InventoryEvents = buildEventNamesFromEntityName(
reservation_item_deleted: "reservation-item." + CommonEvents.DELETED, eventBaseNames,
inventory_level_deleted: "inventory-level." + CommonEvents.DELETED, Modules.INVENTORY
inventory_level_created: "inventory-level." + CommonEvents.CREATED, )
inventory_level_updated: "inventory-level." + CommonEvents.UPDATED,
}
@@ -1,16 +1,17 @@
import { MessageAggregator } from "../../event-bus" import { MessageAggregator } from "../../event-bus"
import { InjectIntoContext } from "./inject-into-context" import { InjectIntoContext } from "./inject-into-context"
import {MessageAggregatorFormat} from "@medusajs/types"; import { MessageAggregatorFormat } from "@medusajs/types"
export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggregatorFormat) { export function EmitEvents(
options: MessageAggregatorFormat = {} as MessageAggregatorFormat
) {
return function ( return function (
target: any, target: any,
propertyKey: string | symbol, propertyKey: string | symbol,
descriptor: any descriptor: any
): void { ): void {
const aggregator = new MessageAggregator()
InjectIntoContext({ InjectIntoContext({
messageAggregator: () => aggregator, messageAggregator: () => new MessageAggregator(),
})(target, propertyKey, descriptor) })(target, propertyKey, descriptor)
const original = descriptor.value const original = descriptor.value
@@ -18,6 +19,17 @@ export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggre
descriptor.value = async function (...args: any[]) { descriptor.value = async function (...args: any[]) {
const result = await original.apply(this, args) const result = await original.apply(this, args)
if (!target.emitEvents_) {
const logger = Object.keys(this.__container__ ?? {}).includes("logger")
? this.__container__.logger
: console
logger.warn(
`No emitEvents_ method found on ${target.constructor.name}. No events emitted. To be able to use the @EmitEvents() you need to have the emitEvents_ method implemented in the class.`
)
}
const argIndex = target.MedusaContextIndex_[propertyKey]
const aggregator = args[argIndex].messageAggregator as MessageAggregator
await target.emitEvents_.apply(this, [aggregator.getMessages(options)]) await target.emitEvents_.apply(this, [aggregator.getMessages(options)])
aggregator.clearMessages() aggregator.clearMessages()
@@ -1,5 +1,3 @@
import { MessageAggregator } from "../../event-bus"
export function InjectIntoContext( export function InjectIntoContext(
properties: Record<string, unknown | Function> properties: Record<string, unknown | Function>
): MethodDecorator { ): MethodDecorator {
@@ -0,0 +1,67 @@
import { Context, EventBusTypes } from "@medusajs/types"
/**
* Factory function to create event builders for different entities
*
* @example
* const createdFulfillment = eventBuilderFactory({
* service: Modules.FULFILLMENT,
* action: CommonEvents.CREATED,
* object: "fulfillment",
* eventsEnum: FulfillmentEvents,
* })
*
* createdFulfillment({
* data,
* sharedContext,
* })
*
* @param isMainEntity
* @param action
* @param object
* @param eventsEnum
* @param service
*/
export function eventBuilderFactory({
isMainEntity,
action,
object,
eventsEnum,
service,
}: {
isMainEntity?: boolean
action: string
object: string
eventsEnum: Record<string, string>
service: string
}) {
return function ({
data,
sharedContext,
}: {
data: { id: string }[]
sharedContext: Context
}) {
if (!data.length) {
return
}
const aggregator = sharedContext.messageAggregator!
const messages: EventBusTypes.RawMessageFormat[] = []
data.forEach((dataItem) => {
messages.push({
service,
action,
context: sharedContext,
data: { id: dataItem.id },
eventName: isMainEntity
? eventsEnum[action]
: eventsEnum[`${object}_${action}`],
object,
})
})
aggregator.saveRawMessageData(messages)
}
}
@@ -9,3 +9,4 @@ export * from "./migration-scripts"
export * from "./internal-module-service-factory" export * from "./internal-module-service-factory"
export * from "./abstract-module-service-factory" export * from "./abstract-module-service-factory"
export * from "./definition" export * from "./definition"
export * from "./event-builder-factory"
+6 -6
View File
@@ -1,9 +1,9 @@
import { CommonEvents } from "../event-bus" import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
const eventBaseNames: ["user", "invite"] = ["user", "invite"]
export const UserEvents = { export const UserEvents = {
created: "user." + CommonEvents.CREATED, ...buildEventNamesFromEntityName(eventBaseNames, Modules.USER),
updated: "user." + CommonEvents.UPDATED, invite_token_generated: `${Modules.USER}.user.invite.token_generated`,
invite_created: "invite." + CommonEvents.CREATED,
invite_updated: "invite." + CommonEvents.UPDATED,
invite_token_generated: "invite.token_generated",
} }
@@ -7,15 +7,15 @@ import {
UpdateFulfillmentSetDTO, UpdateFulfillmentSetDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils" import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import { moduleIntegrationTestRunner } from "medusa-test-utils"
import { MockEventBusService } from "medusa-test-utils/dist" import { MockEventBusService } from "medusa-test-utils/dist"
import { buildExpectedEventMessageShape } from "../../__fixtures__" import { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000) jest.setTimeout(100000)
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy let eventBusEmitSpy
beforeEach(() => { beforeEach(() => {
@@ -625,6 +625,15 @@ moduleIntegrationTestRunner({
type: updateData.type, type: updateData.type,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: updatedFulfillmentSets.id },
}),
])
}) })
it("should update a collection of fulfillment sets", async function () { it("should update a collection of fulfillment sets", async function () {
@@ -655,6 +664,7 @@ moduleIntegrationTestRunner({
}) })
expect(updatedFulfillmentSets).toHaveLength(2) expect(updatedFulfillmentSets).toHaveLength(2)
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(
@@ -668,6 +678,17 @@ moduleIntegrationTestRunner({
type: data_.type, type: data_.type,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenLastCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: currentFullfillmentSet.id },
}),
])
)
} }
}) })
@@ -742,6 +763,46 @@ moduleIntegrationTestRunner({
id: updatedFulfillmentSet.service_zones[0].id, id: updatedFulfillmentSet.service_zones[0].id,
}) })
) )
expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(5)
expect(eventBusEmitSpy).toHaveBeenLastCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: updatedFulfillmentSet.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_created,
action: "created",
object: "service_zone",
data: { id: updatedFulfillmentSet.service_zones[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: {
id: updatedFulfillmentSet.service_zones[0].geo_zones[0].id,
},
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_deleted,
action: "deleted",
object: "service_zone",
data: { id: createdFulfillmentSet.service_zones[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_deleted,
action: "deleted",
object: "geo_zone",
data: {
id: createdFulfillmentSet.service_zones[0].geo_zones[0].id,
},
}),
])
)
}) })
it("should update an existing fulfillment set and add a new service zone", async function () { it("should update an existing fulfillment set and add a new service zone", async function () {
@@ -812,6 +873,36 @@ moduleIntegrationTestRunner({
]), ]),
}) })
) )
const createdServiceZone = updatedFulfillmentSet.service_zones.find(
(s) => s.name === "service-zone-test2"
)
expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(3)
expect(eventBusEmitSpy).toHaveBeenLastCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: updatedFulfillmentSet.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_created,
action: "created",
object: "service_zone",
data: { id: createdServiceZone.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: {
id: createdServiceZone.geo_zones[0].id,
},
}),
])
)
}) })
it("should fail on duplicated fulfillment set name", async function () { it("should fail on duplicated fulfillment set name", async function () {
@@ -897,11 +988,16 @@ moduleIntegrationTestRunner({
const updatedFulfillmentSets = await service.update(updateData) const updatedFulfillmentSets = await service.update(updateData)
expect(updatedFulfillmentSets).toHaveLength(2) expect(updatedFulfillmentSets).toHaveLength(2)
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(
(f) => f.id === data_.id (f) => f.id === data_.id
) )
const originalFulfillmentSet = createdFulfillmentSets.find(
(f) => f.id === data_.id
)
expect(expectedFulfillmentSet).toEqual( expect(expectedFulfillmentSet).toEqual(
expect.objectContaining({ expect.objectContaining({
id: data_.id, id: data_.id,
@@ -925,6 +1021,47 @@ moduleIntegrationTestRunner({
]), ]),
}) })
) )
expect(eventBusEmitSpy).toHaveBeenLastCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: expectedFulfillmentSet.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_created,
action: "created",
object: "service_zone",
data: { id: expectedFulfillmentSet.service_zones[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: {
id: expectedFulfillmentSet.service_zones[0].geo_zones[0]
.id,
},
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_deleted,
action: "deleted",
object: "service_zone",
data: { id: originalFulfillmentSet.service_zones[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_deleted,
action: "deleted",
object: "geo_zone",
data: {
id: originalFulfillmentSet.service_zones[0].geo_zones[0]
.id,
},
}),
])
)
} }
const serviceZones = await service.listServiceZones() const serviceZones = await service.listServiceZones()
@@ -1002,6 +1139,7 @@ moduleIntegrationTestRunner({
const updatedFulfillmentSets = await service.update(updateData) const updatedFulfillmentSets = await service.update(updateData)
expect(updatedFulfillmentSets).toHaveLength(2) expect(updatedFulfillmentSets).toHaveLength(2)
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(
@@ -1033,6 +1171,36 @@ moduleIntegrationTestRunner({
]), ]),
}) })
) )
const createdServiceZone =
expectedFulfillmentSet.service_zones.find((s) =>
s.name.includes(`added-service-zone-test`)
)
expect(eventBusEmitSpy).toHaveBeenLastCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.updated,
action: "updated",
object: "fulfillment_set",
data: { id: expectedFulfillmentSet.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_created,
action: "created",
object: "service_zone",
data: { id: createdServiceZone.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: {
id: createdServiceZone.geo_zones[0].id,
},
}),
])
)
} }
const serviceZones = await service.listServiceZones() const serviceZones = await service.listServiceZones()
@@ -1,11 +1,19 @@
import { resolve } from "path" import { resolve } from "path"
import { Modules } from "@medusajs/modules-sdk" import { Modules } from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import { import {
IFulfillmentModuleService,
UpdateFulfillmentDTO,
} from "@medusajs/types"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import {
buildExpectedEventMessageShape,
generateCreateFulfillmentData, generateCreateFulfillmentData,
generateCreateShippingOptionsData, generateCreateShippingOptionsData,
} from "../../__fixtures__" } from "../../__fixtures__"
import { FulfillmentEvents } from "@medusajs/utils"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -27,10 +35,20 @@ const moduleOptions = {
const providerId = "fixtures-fulfillment-provider_test-provider" const providerId = "fixtures-fulfillment-provider_test-provider"
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
moduleOptions: moduleOptions, moduleOptions: moduleOptions,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => { describe("Fulfillment Module Service", () => {
describe("read", () => { describe("read", () => {
it("should list fulfillment", async () => { it("should list fulfillment", async () => {
@@ -103,6 +121,8 @@ moduleIntegrationTestRunner({
}) })
) )
jest.clearAllMocks()
const fulfillment = await service.createFulfillment( const fulfillment = await service.createFulfillment(
generateCreateFulfillmentData({ generateCreateFulfillmentData({
provider_id: providerId, provider_id: providerId,
@@ -136,6 +156,34 @@ moduleIntegrationTestRunner({
], ],
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_created,
action: "created",
object: "fulfillment",
data: { id: fulfillment.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_address_created,
action: "created",
object: "fulfillment_address",
data: { id: fulfillment.delivery_address.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_item_created,
action: "created",
object: "fulfillment_item",
data: { id: fulfillment.items[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_label_created,
action: "created",
object: "fulfillment_label",
data: { id: fulfillment.labels[0].id },
}),
])
}) })
it("should create a return fulfillment", async () => { it("should create a return fulfillment", async () => {
@@ -160,6 +208,8 @@ moduleIntegrationTestRunner({
}) })
) )
jest.clearAllMocks()
const fulfillment = await service.createReturnFulfillment( const fulfillment = await service.createReturnFulfillment(
generateCreateFulfillmentData({ generateCreateFulfillmentData({
provider_id: providerId, provider_id: providerId,
@@ -193,6 +243,169 @@ moduleIntegrationTestRunner({
], ],
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_created,
action: "created",
object: "fulfillment",
data: { id: fulfillment.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_address_created,
action: "created",
object: "fulfillment_address",
data: { id: fulfillment.delivery_address.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_item_created,
action: "created",
object: "fulfillment_item",
data: { id: fulfillment.items[0].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_label_created,
action: "created",
object: "fulfillment_label",
data: { id: fulfillment.labels[0].id },
}),
])
})
})
describe("on update", () => {
it("should update a fulfillment", async () => {
const shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await service.create({
name: "test",
type: "test-type",
})
const serviceZone = await service.createServiceZones({
name: "test",
fulfillment_set_id: fulfillmentSet.id,
})
const shippingOption = await service.createShippingOptions(
generateCreateShippingOptionsData({
provider_id: providerId,
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
})
)
const fulfillment = await service.createFulfillment(
generateCreateFulfillmentData({
provider_id: providerId,
shipping_option_id: shippingOption.id,
labels: [
{
tracking_number: "test-tracking-number-1",
tracking_url: "test-tracking-url-1",
label_url: "test-label-url-1",
},
{
tracking_number: "test-tracking-number-2",
tracking_url: "test-tracking-url-2",
label_url: "test-label-url-2",
},
{
tracking_number: "test-tracking-number-3",
tracking_url: "test-tracking-url-3",
label_url: "test-label-url-3",
},
],
})
)
const label1 = fulfillment.labels.find(
(l) => l.tracking_number === "test-tracking-number-1"
)!
const label2 = fulfillment.labels.find(
(l) => l.tracking_number === "test-tracking-number-2"
)!
const label3 = fulfillment.labels.find(
(l) => l.tracking_number === "test-tracking-number-3"
)!
const updateData: UpdateFulfillmentDTO = {
id: fulfillment.id,
labels: [
{ id: label1.id },
{ ...label2, label_url: "updated-test-label-url-2" },
{
tracking_number: "test-tracking-number-4",
tracking_url: "test-tracking-url-4",
label_url: "test-label-url-4",
},
],
}
jest.clearAllMocks()
const updatedFulfillment = await service.updateFulfillment(
fulfillment.id,
updateData
)
const label4 = updatedFulfillment.labels.find(
(l) => l.tracking_number === "test-tracking-number-4"
)!
expect(updatedFulfillment.labels).toHaveLength(3)
expect(updatedFulfillment).toEqual(
expect.objectContaining({
id: expect.any(String),
labels: expect.arrayContaining([
expect.objectContaining({
tracking_number: "test-tracking-number-1",
tracking_url: "test-tracking-url-1",
label_url: "test-label-url-1",
}),
expect.objectContaining({
tracking_number: "test-tracking-number-2",
tracking_url: "test-tracking-url-2",
label_url: "updated-test-label-url-2",
}),
expect.objectContaining({
tracking_number: "test-tracking-number-4",
tracking_url: "test-tracking-url-4",
label_url: "test-label-url-4",
}),
]),
})
)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_updated,
action: "updated",
object: "fulfillment",
data: { id: updatedFulfillment.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_label_deleted,
action: "deleted",
object: "fulfillment_label",
data: { id: label3.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_label_updated,
action: "updated",
object: "fulfillment_label",
data: { id: label2.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_label_created,
action: "created",
object: "fulfillment_label",
data: { id: label4.id },
}),
])
}) })
}) })
@@ -227,6 +440,8 @@ moduleIntegrationTestRunner({
shipping_option_id: shippingOption.id, shipping_option_id: shippingOption.id,
}) })
) )
jest.clearAllMocks()
}) })
it("should cancel a fulfillment successfully", async () => { it("should cancel a fulfillment successfully", async () => {
@@ -239,6 +454,16 @@ moduleIntegrationTestRunner({
expect(result.canceled_at).not.toBeNull() expect(result.canceled_at).not.toBeNull()
expect(idempotentResult.canceled_at).not.toBeNull() expect(idempotentResult.canceled_at).not.toBeNull()
expect(idempotentResult.canceled_at).toEqual(result.canceled_at) expect(idempotentResult.canceled_at).toEqual(result.canceled_at)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1)
expect(eventBusEmitSpy).toHaveBeenNthCalledWith(1, [
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.fulfillment_updated,
action: "updated",
object: "fulfillment",
data: { id: fulfillment.id },
}),
])
}) })
it("should fail to cancel a fulfillment that is already shipped", async () => { it("should fail to cancel a fulfillment that is already shipped", async () => {
@@ -4,14 +4,28 @@ import {
IFulfillmentModuleService, IFulfillmentModuleService,
UpdateGeoZoneDTO, UpdateGeoZoneDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils" import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000) jest.setTimeout(100000)
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => { describe("Fulfillment Module Service", () => {
describe("read", () => { describe("read", () => {
it("should list geo zones with a filter", async function () { it("should list geo zones with a filter", async function () {
@@ -81,6 +95,8 @@ moduleIntegrationTestRunner({
country_code: "fr", country_code: "fr",
} }
jest.clearAllMocks()
const geoZone = await service.createGeoZones(data) const geoZone = await service.createGeoZones(data)
expect(geoZone).toEqual( expect(geoZone).toEqual(
@@ -90,6 +106,16 @@ moduleIntegrationTestRunner({
country_code: data.country_code, country_code: data.country_code,
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: { id: geoZone.id },
}),
])
}) })
it("should create a collection of geo zones", async function () { it("should create a collection of geo zones", async function () {
@@ -115,9 +141,12 @@ moduleIntegrationTestRunner({
}, },
] ]
jest.clearAllMocks()
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(2)
let i = 0 let i = 0
for (const data_ of data) { for (const data_ of data) {
@@ -128,6 +157,18 @@ moduleIntegrationTestRunner({
country_code: data_.country_code, country_code: data_.country_code,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: { id: geoZones[i].id },
}),
])
)
++i ++i
} }
}) })
@@ -213,6 +254,8 @@ moduleIntegrationTestRunner({
country_code: "us", country_code: "us",
} }
jest.clearAllMocks()
const updatedGeoZone = await service.updateGeoZones(updateData) const updatedGeoZone = await service.updateGeoZones(updateData)
expect(updatedGeoZone).toEqual( expect(updatedGeoZone).toEqual(
@@ -222,6 +265,15 @@ moduleIntegrationTestRunner({
country_code: updateData.country_code, country_code: updateData.country_code,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_updated,
action: "updated",
object: "geo_zone",
data: { id: updatedGeoZone.id },
}),
])
}) })
it("should update a collection of geo zones", async function () { it("should update a collection of geo zones", async function () {
@@ -258,14 +310,18 @@ moduleIntegrationTestRunner({
}) })
) )
jest.clearAllMocks()
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(2)
for (const data_ of updateData) { for (const data_ of updateData) {
const expectedGeoZone = updatedGeoZones.find( const expectedGeoZone = updatedGeoZones.find(
(geoZone) => geoZone.id === data_.id (geoZone) => geoZone.id === data_.id
) )!
expect(expectedGeoZone).toEqual( expect(expectedGeoZone).toEqual(
expect.objectContaining({ expect.objectContaining({
id: data_.id, id: data_.id,
@@ -273,6 +329,17 @@ moduleIntegrationTestRunner({
country_code: data_.country_code, country_code: data_.country_code,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_updated,
action: "updated",
object: "geo_zone",
data: { id: expectedGeoZone.id },
}),
])
)
} }
}) })
}) })
@@ -5,14 +5,28 @@ import {
IFulfillmentModuleService, IFulfillmentModuleService,
UpdateServiceZoneDTO, UpdateServiceZoneDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils" import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils" import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000) jest.setTimeout(100000)
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => { describe("Fulfillment Module Service", () => {
describe("read", () => { describe("read", () => {
it("should list service zones with a filter", async function () { it("should list service zones with a filter", async function () {
@@ -276,6 +290,14 @@ moduleIntegrationTestRunner({
type: GeoZoneType.COUNTRY, type: GeoZoneType.COUNTRY,
country_code: "fr", country_code: "fr",
}, },
{
type: GeoZoneType.COUNTRY,
country_code: "us",
},
{
type: GeoZoneType.COUNTRY,
country_code: "uk",
},
], ],
} }
@@ -283,35 +305,94 @@ moduleIntegrationTestRunner({
createData createData
) )
const updateData = { const usGeoZone = createdServiceZone.geo_zones.find(
(geoZone) => geoZone.country_code === "us"
)!
const frGeoZone = createdServiceZone.geo_zones.find(
(geoZone) => geoZone.country_code === "fr"
)!
const ukGeoZone = createdServiceZone.geo_zones.find(
(geoZone) => geoZone.country_code === "uk"
)!
const updateData: UpdateServiceZoneDTO = {
name: "updated-service-zone-test", name: "updated-service-zone-test",
geo_zones: [ geo_zones: [
{ {
id: createdServiceZone.geo_zones[0].id, id: usGeoZone.id,
type: GeoZoneType.COUNTRY, type: GeoZoneType.COUNTRY,
country_code: "us", country_code: "es",
}, },
{
type: GeoZoneType.COUNTRY,
country_code: "ch",
},
{ id: frGeoZone.id },
], ],
} }
jest.clearAllMocks()
const updatedServiceZone = await service.updateServiceZones( const updatedServiceZone = await service.updateServiceZones(
createdServiceZone.id, createdServiceZone.id,
updateData updateData
) )
expect(updatedServiceZone.geo_zones).toHaveLength(3)
expect(updatedServiceZone).toEqual( expect(updatedServiceZone).toEqual(
expect.objectContaining({ expect.objectContaining({
id: createdServiceZone.id, id: createdServiceZone.id,
name: updateData.name, name: updateData.name,
geo_zones: expect.arrayContaining([ geo_zones: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: updateData.geo_zones[0].id, id: frGeoZone.id,
type: updateData.geo_zones[0].type, }),
country_code: updateData.geo_zones[0].country_code, expect.objectContaining({
id: usGeoZone.id,
type: GeoZoneType.COUNTRY,
country_code: "es",
}),
expect.objectContaining({
id: expect.any(String),
type: GeoZoneType.COUNTRY,
country_code: "ch",
}), }),
]), ]),
}) })
) )
const chGeoZone = updatedServiceZone.geo_zones.find(
(geoZone) => geoZone.country_code === "ch"
)!
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(4)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_deleted,
action: "deleted",
object: "geo_zone",
data: { id: ukGeoZone.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.service_zone_updated,
action: "updated",
object: "service_zone",
data: { id: updatedServiceZone.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_created,
action: "created",
object: "geo_zone",
data: { id: chGeoZone.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.geo_zone_updated,
action: "updated",
object: "geo_zone",
data: { id: usGeoZone.id },
}),
])
}) })
it("should fail on duplicated service zone name", async function () { it("should fail on duplicated service zone name", async function () {
@@ -413,12 +494,16 @@ moduleIntegrationTestRunner({
}) })
) )
jest.clearAllMocks()
const updatedServiceZones = await service.upsertServiceZones( const updatedServiceZones = await service.upsertServiceZones(
updateData updateData
) )
expect(updatedServiceZones).toHaveLength(2) expect(updatedServiceZones).toHaveLength(2)
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(
(serviceZone) => serviceZone.id === data_.id (serviceZone) => serviceZone.id === data_.id
@@ -3,12 +3,18 @@ import {
CreateShippingOptionDTO, CreateShippingOptionDTO,
IFulfillmentModuleService, IFulfillmentModuleService,
} from "@medusajs/types" } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import {
import { generateCreateShippingOptionsData } from "../../__fixtures__" MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import {
buildExpectedEventMessageShape,
generateCreateShippingOptionsData,
} from "../../__fixtures__"
import { resolve } from "path" import { resolve } from "path"
import { FulfillmentProviderService } from "@services" import { FulfillmentProviderService } from "@services"
import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers" import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers"
import { GeoZoneType } from "@medusajs/utils" import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import { UpdateShippingOptionDTO } from "@medusajs/types/src" import { UpdateShippingOptionDTO } from "@medusajs/types/src"
jest.setTimeout(100000) jest.setTimeout(100000)
@@ -34,10 +40,20 @@ const providerId = FulfillmentProviderService.getRegistrationIdentifier(
"test-provider" "test-provider"
) )
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
moduleOptions, moduleOptions,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => { describe("Fulfillment Module Service", () => {
describe("read", () => { describe("read", () => {
it("should list shipping options with a filter", async function () { it("should list shipping options with a filter", async function () {
@@ -432,6 +448,8 @@ moduleIntegrationTestRunner({
provider_id: providerId, provider_id: providerId,
}) })
jest.clearAllMocks()
const createdShippingOption = await service.createShippingOptions( const createdShippingOption = await service.createShippingOptions(
createData createData
) )
@@ -462,6 +480,28 @@ moduleIntegrationTestRunner({
]), ]),
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(3)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_created,
action: "created",
object: "shipping_option",
data: { id: createdShippingOption.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_type_created,
action: "created",
object: "shipping_option_type",
data: { id: createdShippingOption.type.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_created,
action: "created",
object: "shipping_option_rule",
data: { id: createdShippingOption.rules[0].id },
}),
])
}) })
it("should create multiple new shipping options", async function () { it("should create multiple new shipping options", async function () {
@@ -491,11 +531,14 @@ moduleIntegrationTestRunner({
}), }),
] ]
jest.clearAllMocks()
const createdShippingOptions = await service.createShippingOptions( const createdShippingOptions = await service.createShippingOptions(
createData createData
) )
expect(createdShippingOptions).toHaveLength(2) expect(createdShippingOptions).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(6)
let i = 0 let i = 0
for (const data_ of createData) { for (const data_ of createData) {
@@ -525,6 +568,30 @@ moduleIntegrationTestRunner({
]), ]),
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_created,
action: "created",
object: "shipping_option",
data: { id: createdShippingOptions[i].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_type_created,
action: "created",
object: "shipping_option_type",
data: { id: createdShippingOptions[i].type.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_created,
action: "created",
object: "shipping_option_rule",
data: { id: createdShippingOptions[i].rules[0].id },
}),
])
)
++i ++i
} }
}) })
@@ -623,6 +690,8 @@ moduleIntegrationTestRunner({
], ],
} }
jest.clearAllMocks()
const updatedShippingOption = await service.updateShippingOptions( const updatedShippingOption = await service.updateShippingOptions(
updateData.id!, updateData.id!,
updateData updateData
@@ -681,6 +750,42 @@ moduleIntegrationTestRunner({
label: updateData.type.label, label: updateData.type.label,
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(5)
expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_updated,
action: "updated",
object: "shipping_option",
data: { id: updatedShippingOption.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_type_deleted,
action: "deleted",
object: "shipping_option_type",
data: { id: shippingOption.type.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_type_created,
action: "created",
object: "shipping_option_type",
data: { id: updatedShippingOption.type.id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_created,
action: "created",
object: "shipping_option_rule",
data: { id: updatedShippingOption.rules[1].id },
}),
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_updated,
action: "updated",
object: "shipping_option_rule",
data: { id: updatedShippingOption.rules[0].id },
}),
])
)
}) })
it("should update a shipping option without updating the rules or the type", async () => { it("should update a shipping option without updating the rules or the type", async () => {
@@ -1092,6 +1197,8 @@ moduleIntegrationTestRunner({
shipping_option_id: shippingOption.id, shipping_option_id: shippingOption.id,
} }
jest.clearAllMocks()
const rule = await service.createShippingOptionRules(ruleData) const rule = await service.createShippingOptionRules(ruleData)
expect(rule).toEqual( expect(rule).toEqual(
@@ -1104,6 +1211,16 @@ moduleIntegrationTestRunner({
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_created,
action: "created",
object: "shipping_option_rule",
data: { id: rule.id },
}),
])
const rules = await service.listShippingOptionRules() const rules = await service.listShippingOptionRules()
expect(rules).toHaveLength(2) expect(rules).toHaveLength(2)
expect(rules).toEqual( expect(rules).toEqual(
@@ -1157,6 +1274,8 @@ moduleIntegrationTestRunner({
value: "updated-test", value: "updated-test",
} }
jest.clearAllMocks()
const updatedRule = await service.updateShippingOptionRules( const updatedRule = await service.updateShippingOptionRules(
updateData updateData
) )
@@ -1169,6 +1288,15 @@ moduleIntegrationTestRunner({
value: updateData.value, value: updateData.value,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_option_rule_updated,
action: "updated",
object: "shipping_option_rule",
data: { id: updatedRule.id },
}),
])
}) })
it("should fail to update a non-existent shipping option rule", async () => { it("should fail to update a non-existent shipping option rule", async () => {
@@ -3,13 +3,28 @@ import {
CreateShippingProfileDTO, CreateShippingProfileDTO,
IFulfillmentModuleService, IFulfillmentModuleService,
} from "@medusajs/types" } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils" import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { buildExpectedEventMessageShape } from "../../__fixtures__"
import { FulfillmentEvents } from "@medusajs/utils"
jest.setTimeout(100000) jest.setTimeout(100000)
moduleIntegrationTestRunner({ moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT, moduleName: Modules.FULFILLMENT,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => { testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => { describe("Fulfillment Module Service", () => {
describe("mutations", () => { describe("mutations", () => {
describe("on create", () => { describe("on create", () => {
@@ -29,6 +44,16 @@ moduleIntegrationTestRunner({
type: createData.type, type: createData.type,
}) })
) )
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(1)
expect(eventBusEmitSpy).toHaveBeenCalledWith([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_profile_created,
action: "created",
object: "shipping_profile",
data: { id: createdShippingProfile.id },
}),
])
}) })
it("should create multiple new shipping profiles", async function () { it("should create multiple new shipping profiles", async function () {
@@ -47,6 +72,7 @@ moduleIntegrationTestRunner({
await service.createShippingProfiles(createData) await service.createShippingProfiles(createData)
expect(createdShippingProfiles).toHaveLength(2) expect(createdShippingProfiles).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
let i = 0 let i = 0
for (const data_ of createData) { for (const data_ of createData) {
@@ -56,6 +82,18 @@ moduleIntegrationTestRunner({
type: data_.type, type: data_.type,
}) })
) )
expect(eventBusEmitSpy).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedEventMessageShape({
eventName: FulfillmentEvents.shipping_profile_created,
action: "created",
object: "shipping_profile",
data: { id: createdShippingProfiles[i].id },
}),
])
)
++i ++i
} }
}) })
@@ -15,15 +15,16 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { import {
arrayDifference, arrayDifference,
deepEqualObj,
EmitEvents, EmitEvents,
FulfillmentUtils,
getSetDifference, getSetDifference,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
isDefined,
isPresent,
isString, isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
Modules,
ModulesSdkUtils, ModulesSdkUtils,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
@@ -38,10 +39,18 @@ import {
ShippingOptionType, ShippingOptionType,
ShippingProfile, ShippingProfile,
} from "@models" } from "@models"
import { isContextValid, validateAndNormalizeRules } from "@utils" import {
buildCreatedFulfillmentEvents,
buildCreatedFulfillmentSetEvents,
buildCreatedServiceZoneEvents,
eventBuilders,
isContextValid,
validateAndNormalizeRules,
} from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { UpdateShippingOptionsInput } from "../types/service" import { UpdateShippingOptionsInput } from "../types/service"
import FulfillmentProviderService from "./fulfillment-provider" import FulfillmentProviderService from "./fulfillment-provider"
import { buildCreatedShippingOptionEvents } from "../utils/events"
const generateMethodForModels = [ const generateMethodForModels = [
ServiceZone, ServiceZone,
@@ -271,9 +280,13 @@ export default class FulfillmentModuleService<
> { > {
const createdFulfillmentSets = await this.create_(data, sharedContext) const createdFulfillmentSets = await this.create_(data, sharedContext)
const returnedFulfillmentSets = Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0]
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[] FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(createdFulfillmentSets) >(returnedFulfillmentSets)
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -282,7 +295,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateFulfillmentSetDTO | FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[], | FulfillmentTypes.CreateFulfillmentSetDTO[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TEntity | TEntity[]> { ): Promise<TEntity[]> {
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) { if (!data_.length) {
@@ -304,14 +317,12 @@ export default class FulfillmentModuleService<
sharedContext sharedContext
) )
this.aggregateFulfillmentSetCreatedEvents( buildCreatedFulfillmentSetEvents({
createdFulfillmentSets, fulfillmentSets: createdFulfillmentSets,
sharedContext sharedContext,
) })
return Array.isArray(data) return createdFulfillmentSets
? createdFulfillmentSets
: createdFulfillmentSets[0]
} }
createServiceZones( createServiceZones(
@@ -324,6 +335,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO> ): Promise<FulfillmentTypes.ServiceZoneDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createServiceZones( async createServiceZones(
data: data:
| FulfillmentTypes.CreateServiceZoneDTO[] | FulfillmentTypes.CreateServiceZoneDTO[]
@@ -339,7 +351,7 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(createdServiceZones) >(Array.isArray(data) ? createdServiceZones : createdServiceZones[0])
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -348,7 +360,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateServiceZoneDTO[] | FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO, | FulfillmentTypes.CreateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TServiceZoneEntity | TServiceZoneEntity[]> { ): Promise<TServiceZoneEntity[]> {
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) { if (!data_.length) {
@@ -368,7 +380,12 @@ export default class FulfillmentModuleService<
sharedContext sharedContext
) )
return Array.isArray(data) ? createdServiceZones : createdServiceZones[0] buildCreatedServiceZoneEvents({
serviceZones: createdServiceZones,
sharedContext,
})
return createdServiceZones
} }
createShippingOptions( createShippingOptions(
@@ -381,6 +398,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO> ): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createShippingOptions( async createShippingOptions(
data: data:
| FulfillmentTypes.CreateShippingOptionDTO[] | FulfillmentTypes.CreateShippingOptionDTO[]
@@ -396,7 +414,7 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[] FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
>(createdShippingOptions) >(Array.isArray(data) ? createdShippingOptions : createdShippingOptions[0])
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -405,7 +423,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingOptionDTO[] | FulfillmentTypes.CreateShippingOptionDTO[]
| FulfillmentTypes.CreateShippingOptionDTO, | FulfillmentTypes.CreateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionEntity | TShippingOptionEntity[]> { ): Promise<TShippingOptionEntity[]> {
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) { if (!data_.length) {
@@ -417,14 +435,17 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(rules as Record<string, unknown>[]) validateAndNormalizeRules(rules as Record<string, unknown>[])
} }
const createdShippingOptions = await this.shippingOptionService_.create( const createdSO = await this.shippingOptionService_.create(
data_, data_,
sharedContext sharedContext
) )
return Array.isArray(data) buildCreatedShippingOptionEvents({
? createdShippingOptions shippingOptions: createdSO,
: createdShippingOptions[0] sharedContext,
})
return createdSO
} }
createShippingProfiles( createShippingProfiles(
@@ -437,6 +458,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingProfileDTO> ): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@EmitEvents()
async createShippingProfiles( async createShippingProfiles(
data: data:
| FulfillmentTypes.CreateShippingProfileDTO[] | FulfillmentTypes.CreateShippingProfileDTO[]
@@ -450,10 +472,17 @@ 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[]
>(createdShippingProfiles) >(
Array.isArray(data) ? createdShippingProfiles : createdShippingProfiles[0]
)
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -462,21 +491,14 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingProfileDTO[] | FulfillmentTypes.CreateShippingProfileDTO[]
| FulfillmentTypes.CreateShippingProfileDTO, | FulfillmentTypes.CreateShippingProfileDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TShippingProfileEntity[] | TShippingProfileEntity> { ): Promise<TShippingProfileEntity[]> {
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) { if (!data_.length) {
return [] return []
} }
const createdShippingProfiles = await this.shippingProfileService_.create( return await this.shippingProfileService_.create(data_, sharedContext)
data_,
sharedContext
)
return Array.isArray(data)
? createdShippingProfiles
: createdShippingProfiles[0]
} }
createGeoZones( createGeoZones(
@@ -489,6 +511,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.GeoZoneDTO> ): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createGeoZones( async createGeoZones(
data: data:
| FulfillmentTypes.CreateGeoZoneDTO | FulfillmentTypes.CreateGeoZoneDTO
@@ -504,6 +527,11 @@ 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]
) )
@@ -519,6 +547,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionRuleDTO> ): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createShippingOptionRules( async createShippingOptionRules(
data: data:
| FulfillmentTypes.CreateShippingOptionRuleDTO[] | FulfillmentTypes.CreateShippingOptionRuleDTO[]
@@ -536,7 +565,11 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize< return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionRuleDTO | FulfillmentTypes.ShippingOptionRuleDTO
| FulfillmentTypes.ShippingOptionRuleDTO[] | FulfillmentTypes.ShippingOptionRuleDTO[]
>(createdShippingOptionRules) >(
Array.isArray(data)
? createdShippingOptionRules
: createdShippingOptionRules[0]
)
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -545,7 +578,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingOptionRuleDTO[] | FulfillmentTypes.CreateShippingOptionRuleDTO[]
| FulfillmentTypes.CreateShippingOptionRuleDTO, | FulfillmentTypes.CreateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionRuleEntity | TShippingOptionRuleEntity[]> { ): Promise<TShippingOptionRuleEntity[]> {
const data_ = Array.isArray(data) ? data : [data] const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) { if (!data_.length) {
@@ -554,15 +587,21 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(data_ as unknown as Record<string, unknown>[]) validateAndNormalizeRules(data_ as unknown as Record<string, unknown>[])
const createdShippingOptionRules = const createdSORules = await this.shippingOptionRuleService_.create(
await this.shippingOptionRuleService_.create(data_, sharedContext) data_,
sharedContext
)
return Array.isArray(data) eventBuilders.createdShippingOptionRule({
? createdShippingOptionRules data: createdSORules.map((sor) => ({ id: sor.id })),
: createdShippingOptionRules[0] sharedContext,
})
return createdSORules
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createFulfillment( async createFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO, data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -603,12 +642,18 @@ 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
) )
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async createReturnFulfillment( async createReturnFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO, data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -639,6 +684,11 @@ 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
) )
@@ -654,6 +704,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.FulfillmentSetDTO> ): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async update( async update(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO, data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -714,9 +765,11 @@ export default class FulfillmentModuleService<
fulfillmentSets.map((f) => [f.id, f]) fulfillmentSets.map((f) => [f.id, f])
) )
// find service zones to delete
const serviceZoneIdsToDelete: string[] = [] const serviceZoneIdsToDelete: string[] = []
const geoZoneIdsToDelete: string[] = [] const geoZoneIdsToDelete: string[] = []
const existingServiceZoneIds: string[] = []
const existingGeoZoneIds: string[] = []
data_.forEach((fulfillmentSet) => { data_.forEach((fulfillmentSet) => {
if (fulfillmentSet.service_zones) { if (fulfillmentSet.service_zones) {
/** /**
@@ -734,6 +787,7 @@ export default class FulfillmentModuleService<
.filter((id): id is string => !!id) .filter((id): id is string => !!id)
) )
) )
if (toDeleteServiceZoneIds.size) { if (toDeleteServiceZoneIds.size) {
serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds)) serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds))
geoZoneIdsToDelete.push( geoZoneIdsToDelete.push(
@@ -758,11 +812,13 @@ export default class FulfillmentModuleService<
.map((s) => "id" in s && s.id) .map((s) => "id" in s && s.id)
.filter((id): id is string => !!id) .filter((id): id is string => !!id)
) )
const expectedServiceZoneSet = new Set( const expectedServiceZoneSet = new Set(
fulfillmentSet.service_zones fulfillmentSet.service_zones
.map((s) => "id" in s && s.id) .map((s) => "id" in s && s.id)
.filter((id): id is string => !!id) .filter((id): id is string => !!id)
) )
const missingServiceZoneIds = getSetDifference( const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet, expectedServiceZoneSet,
serviceZonesSet serviceZonesSet
@@ -789,6 +845,16 @@ export default class FulfillmentModuleService<
} }
return serviceZone return serviceZone
} }
const existingServiceZone = serviceZonesMap.get(serviceZone.id)!
existingServiceZoneIds.push(existingServiceZone.id)
if (existingServiceZone.geo_zones.length) {
existingGeoZoneIds.push(
...existingServiceZone.geo_zones.map((g) => g.id)
)
}
return serviceZonesMap.get(serviceZone.id)! return serviceZonesMap.get(serviceZone.id)!
} }
) )
@@ -797,6 +863,15 @@ 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(
{ {
@@ -818,6 +893,32 @@ 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]
@@ -835,6 +936,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO[]> ): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async updateServiceZones( async updateServiceZones(
idOrSelector: string | FulfillmentTypes.FilterableServiceZoneProps, idOrSelector: string | FulfillmentTypes.FilterableServiceZoneProps,
data: FulfillmentTypes.UpdateServiceZoneDTO, data: FulfillmentTypes.UpdateServiceZoneDTO,
@@ -926,20 +1028,28 @@ export default class FulfillmentModuleService<
) )
const geoZoneIdsToDelete: string[] = [] const geoZoneIdsToDelete: string[] = []
const existingGeoZoneIds: string[] = []
const updatedGeoZoneIds: string[] = []
data_.forEach((serviceZone) => { data_.forEach((serviceZone) => {
if (serviceZone.geo_zones) { if (serviceZone.geo_zones) {
const existingServiceZone = serviceZoneMap.get(serviceZone.id!)! const existingServiceZone = serviceZoneMap.get(serviceZone.id!)!
const existingGeoZones = existingServiceZone.geo_zones const existingGeoZones = existingServiceZone.geo_zones
const updatedGeoZones = serviceZone.geo_zones const updatedGeoZones = serviceZone.geo_zones
const existingGeoZoneIdsForServiceZone = existingGeoZones.map(
(g) => g.id
)
const toDeleteGeoZoneIds = getSetDifference( const toDeleteGeoZoneIds = getSetDifference(
new Set(existingGeoZones.map((g) => g.id)), new Set(existingGeoZoneIdsForServiceZone),
new Set( new Set(
updatedGeoZones updatedGeoZones
.map((g) => "id" in g && g.id) .map((g) => "id" in g && g.id)
.filter((id): id is string => !!id) .filter((id): id is string => !!id)
) )
) )
existingGeoZoneIds.push(...existingGeoZoneIdsForServiceZone)
if (toDeleteGeoZoneIds.size) { if (toDeleteGeoZoneIds.size) {
geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds)) geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds))
} }
@@ -978,12 +1088,25 @@ export default class FulfillmentModuleService<
} }
const existing = geoZonesMap.get(geoZone.id)! const existing = geoZonesMap.get(geoZone.id)!
// If only the id is provided we dont consider it as an update
if (
Object.keys(geoZone).length > 1 &&
!deepEqualObj(existing, geoZone)
) {
updatedGeoZoneIds.push(geoZone.id)
}
return { ...existing, ...geoZone } return { ...existing, ...geoZone }
}) })
} }
}) })
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,
@@ -997,6 +1120,26 @@ 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]
} }
@@ -1010,11 +1153,12 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO[]> ): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async upsertServiceZones( async upsertServiceZones(
data: data:
| FulfillmentTypes.UpsertServiceZoneDTO | FulfillmentTypes.UpsertServiceZoneDTO
| FulfillmentTypes.UpsertServiceZoneDTO[], | FulfillmentTypes.UpsertServiceZoneDTO[],
sharedContext?: Context @MedusaContext() sharedContext: Context = {}
): Promise< ): Promise<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[] FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
> { > {
@@ -1055,9 +1199,11 @@ export default class FulfillmentModuleService<
forCreate, forCreate,
sharedContext sharedContext
) )
const toPush = Array.isArray(createdServiceZones) const toPush = Array.isArray(createdServiceZones)
? createdServiceZones ? createdServiceZones
: [createdServiceZones] : [createdServiceZones]
created.push(...toPush) created.push(...toPush)
} }
@@ -1066,9 +1212,11 @@ export default class FulfillmentModuleService<
forUpdate, forUpdate,
sharedContext sharedContext
) )
const toPush = Array.isArray(updatedServiceZones) const toPush = Array.isArray(updatedServiceZones)
? updatedServiceZones ? updatedServiceZones
: [updatedServiceZones] : [updatedServiceZones]
updated.push(...toPush) updated.push(...toPush)
} }
@@ -1087,6 +1235,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO[]> ): Promise<FulfillmentTypes.ShippingOptionDTO[]>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async updateShippingOptions( async updateShippingOptions(
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps, idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO, data: FulfillmentTypes.UpdateShippingOptionDTO,
@@ -1142,7 +1291,7 @@ export default class FulfillmentModuleService<
id: shippingOptionIds, id: shippingOptionIds,
}, },
{ {
relations: ["rules"], relations: ["rules", "type"],
take: shippingOptionIds.length, take: shippingOptionIds.length,
}, },
sharedContext sharedContext
@@ -1157,16 +1306,28 @@ export default class FulfillmentModuleService<
) )
const ruleIdsToDelete: string[] = [] const ruleIdsToDelete: string[] = []
const updatedRuleIds: string[] = []
const existingRuleIds: string[] = []
const optionTypeDeletedIds: string[] = []
dataArray.forEach((shippingOption) => { dataArray.forEach((shippingOption) => {
const existingShippingOption = existingShippingOptions.get(
shippingOption.id
)! // Garuantueed to exist since the validation above have been performed
if (shippingOption.type && !("id" in shippingOption.type)) {
optionTypeDeletedIds.push(existingShippingOption.type.id)
}
if (!shippingOption.rules) { if (!shippingOption.rules) {
return return
} }
const existingShippingOption = existingShippingOptions.get(
shippingOption.id
)! // Garuantueed to exist since the validation above have been performed
const existingRules = existingShippingOption.rules const existingRules = existingShippingOption.rules
existingRuleIds.push(...existingRules.map((r) => r.id))
FulfillmentModuleService.validateMissingShippingOptionRules( FulfillmentModuleService.validateMissingShippingOptionRules(
existingShippingOption, existingShippingOption,
shippingOption shippingOption
@@ -1183,6 +1344,10 @@ export default class FulfillmentModuleService<
const existingRule = (existingRulesMap.get(rule.id) ?? const existingRule = (existingRulesMap.get(rule.id) ??
{}) as FulfillmentTypes.UpdateShippingOptionRuleDTO {}) as FulfillmentTypes.UpdateShippingOptionRuleDTO
if (existingRulesMap.get(rule.id)) {
updatedRuleIds.push(rule.id)
}
const ruleData: FulfillmentTypes.UpdateShippingOptionRuleDTO = { const ruleData: FulfillmentTypes.UpdateShippingOptionRuleDTO = {
...existingRule, ...existingRule,
...rule, ...rule,
@@ -1198,8 +1363,6 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(updatedRules) validateAndNormalizeRules(updatedRules)
const updatedRuleIds = updatedRules.map((r) => "id" in r && r.id)
const toDeleteRuleIds = arrayDifference( const toDeleteRuleIds = arrayDifference(
updatedRuleIds, updatedRuleIds,
Array.from(existingRulesMap.keys()) Array.from(existingRulesMap.keys())
@@ -1219,6 +1382,11 @@ 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
@@ -1230,11 +1398,71 @@ 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 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
@@ -1245,6 +1473,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO> ): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async upsertShippingOptions( async upsertShippingOptions(
data: data:
| FulfillmentTypes.UpsertShippingOptionDTO[] | FulfillmentTypes.UpsertShippingOptionDTO[]
@@ -1329,6 +1558,7 @@ export default class FulfillmentModuleService<
): Promise< ): Promise<
FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[] FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[]
> { > {
// TODO: should we implement that or can we get rid of the profiles concept entirely and link to the so instead?
return [] return []
} }
@@ -1342,6 +1572,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.GeoZoneDTO> ): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async updateGeoZones( async updateGeoZones(
data: data:
| FulfillmentTypes.UpdateGeoZoneDTO | FulfillmentTypes.UpdateGeoZoneDTO
@@ -1357,10 +1588,15 @@ export default class FulfillmentModuleService<
FulfillmentModuleService.validateGeoZones(data_) FulfillmentModuleService.validateGeoZones(data_)
const updatedGeoZones = await this.geoZoneService_.update( const updatedGeoZones = await this.geoZoneService_.update(
data, data_,
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)
@@ -1378,6 +1614,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionRuleDTO> ): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async updateShippingOptionRules( async updateShippingOptionRules(
data: data:
| FulfillmentTypes.UpdateShippingOptionRuleDTO[] | FulfillmentTypes.UpdateShippingOptionRuleDTO[]
@@ -1416,31 +1653,140 @@ 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]
} }
@InjectTransactionManager("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async updateFulfillment( async updateFulfillment(
id: string, id: string,
data: FulfillmentTypes.UpdateFulfillmentDTO, data: FulfillmentTypes.UpdateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> { ): Promise<FulfillmentTypes.FulfillmentDTO> {
const fulfillment = await this.fulfillmentService_.update( const fulfillment = await this.updateFulfillment_(id, data, sharedContext)
{ id, ...data },
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
@InjectTransactionManager("baseRepository_")
protected async updateFulfillment_(
id: string,
data: FulfillmentTypes.UpdateFulfillmentDTO,
@MedusaContext() sharedContext: Context
): Promise<TFulfillmentEntity> {
const existingFulfillment: TFulfillmentEntity =
await this.fulfillmentService_.retrieve(
id,
{
relations: ["items", "labels"],
},
sharedContext sharedContext
) )
const serialized = const updatedLabelIds: string[] = []
await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>( let deletedLabelIds: string[] = []
fulfillment
const existingLabelIds = existingFulfillment.labels.map((label) => label.id)
/**
* @note
* Since the relation is a one to many, the deletion, update and creation of labels
* is handled b the orm. That means that we dont have to perform any manual deletions or update.
* For some reason we use to have upsert and replace handled manually but we could simplify all that just like
* we do below which will create the label, update some and delete the one that does not exists in the new data.
*
* There is a bit of logic as we need to reassign the data of those we want to keep
* and we also need to emit the events later on.
*/
if (isDefined(data.labels) && isPresent(data.labels)) {
const dataLabelIds: string[] = data.labels
.filter((label): label is { id: string } => "id" in label)
.map((label) => label.id)
deletedLabelIds = arrayDifference(existingLabelIds, dataLabelIds)
for (let label of data.labels) {
if (!("id" in label)) {
continue
}
const existingLabel = existingFulfillment.labels.find(
({ id }) => id === label.id
)!
if (
!existingLabel ||
Object.keys(label).length === 1 ||
deepEqualObj(existingLabel, label)
) {
continue
}
updatedLabelIds.push(label.id)
const labelData = { ...label }
Object.assign(label, existingLabel, labelData)
}
}
const [fulfillment] = await this.fulfillmentService_.update(
[{ id, ...data }],
sharedContext
) )
return Array.isArray(serialized) ? serialized[0] : serialized this.handleFulfillmentUpdateEvents(
fulfillment,
existingLabelIds,
updatedLabelIds,
deletedLabelIds,
sharedContext
)
return fulfillment
}
private handleFulfillmentUpdateEvents(
fulfillment: 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("baseRepository_") @InjectManager("baseRepository_")
@EmitEvents()
async cancelFulfillment( async cancelFulfillment(
id: string, id: string,
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
@@ -1473,6 +1819,11 @@ export default class FulfillmentModuleService<
}, },
sharedContext sharedContext
) )
eventBuilders.updatedFulfillment({
data: [{ id }],
sharedContext,
})
} }
const result = await this.baseRepository_.serialize(fulfillment) const result = await this.baseRepository_.serialize(fulfillment)
@@ -1791,60 +2142,4 @@ export default class FulfillmentModuleService<
return geoZoneConstraints return geoZoneConstraints
} }
protected aggregateFulfillmentSetCreatedEvents(
createdFulfillmentSets: TEntity[],
sharedContext: Context
): void {
const buildMessage = ({
eventName,
id,
object,
}: {
eventName: string
id: string
object: string
}) => {
return {
eventName,
metadata: {
object,
service: Modules.FULFILLMENT,
action: "created",
eventGroupId: sharedContext.eventGroupId,
},
data: { id },
}
}
for (const fulfillmentSet of createdFulfillmentSets) {
sharedContext.messageAggregator!.saveRawMessageData(
buildMessage({
eventName: FulfillmentUtils.FulfillmentEvents.created,
id: fulfillmentSet.id,
object: "fulfillment_set",
})
)
for (const serviceZone of fulfillmentSet.service_zones ?? []) {
sharedContext.messageAggregator!.saveRawMessageData(
buildMessage({
eventName: FulfillmentUtils.FulfillmentEvents.service_zone_created,
id: serviceZone.id,
object: "service_zone",
})
)
for (const geoZone of serviceZone.geo_zones ?? []) {
sharedContext.messageAggregator!.saveRawMessageData(
buildMessage({
eventName: FulfillmentUtils.FulfillmentEvents.geo_zone_created,
id: geoZone.id,
object: "geo_zone",
})
)
}
}
}
}
} }
@@ -0,0 +1,293 @@
import {
Fulfillment,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingOptionRule,
ShippingOptionType,
} from "@models"
import { Context } from "@medusajs/types"
import {
CommonEvents,
eventBuilderFactory,
FulfillmentEvents,
Modules,
} from "@medusajs/utils"
export const eventBuilders = {
createdFulfillment: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "fulfillment",
eventsEnum: FulfillmentEvents,
}),
updatedFulfillment: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "fulfillment",
eventsEnum: FulfillmentEvents,
}),
createdFulfillmentAddress: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "fulfillment_address",
eventsEnum: FulfillmentEvents,
}),
createdFulfillmentItem: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "fulfillment_item",
eventsEnum: FulfillmentEvents,
}),
createdFulfillmentLabel: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "fulfillment_label",
eventsEnum: FulfillmentEvents,
}),
updatedFulfillmentLabel: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "fulfillment_label",
eventsEnum: FulfillmentEvents,
}),
deletedFulfillmentLabel: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "fulfillment_label",
eventsEnum: FulfillmentEvents,
}),
createdShippingProfile: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "shipping_profile",
eventsEnum: FulfillmentEvents,
}),
createdShippingOptionType: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "shipping_option_type",
eventsEnum: FulfillmentEvents,
}),
updatedShippingOptionType: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "shipping_option_type",
eventsEnum: FulfillmentEvents,
}),
deletedShippingOptionType: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "shipping_option_type",
eventsEnum: FulfillmentEvents,
}),
createdShippingOptionRule: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "shipping_option_rule",
eventsEnum: FulfillmentEvents,
}),
updatedShippingOptionRule: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "shipping_option_rule",
eventsEnum: FulfillmentEvents,
}),
deletedShippingOptionRule: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "shipping_option_rule",
eventsEnum: FulfillmentEvents,
}),
createdShippingOption: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "shipping_option",
eventsEnum: FulfillmentEvents,
}),
updatedShippingOption: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "shipping_option",
eventsEnum: FulfillmentEvents,
}),
createdFulfillmentSet: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "fulfillment_set",
isMainEntity: true,
eventsEnum: FulfillmentEvents,
}),
updatedFulfillmentSet: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "fulfillment_set",
isMainEntity: true,
eventsEnum: FulfillmentEvents,
}),
deletedFulfillmentSet: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "fulfillment_set",
isMainEntity: true,
eventsEnum: FulfillmentEvents,
}),
createdServiceZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "service_zone",
eventsEnum: FulfillmentEvents,
}),
updatedServiceZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "service_zone",
eventsEnum: FulfillmentEvents,
}),
deletedServiceZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "service_zone",
eventsEnum: FulfillmentEvents,
}),
createdGeoZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.CREATED,
object: "geo_zone",
eventsEnum: FulfillmentEvents,
}),
updatedGeoZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.UPDATED,
object: "geo_zone",
eventsEnum: FulfillmentEvents,
}),
deletedGeoZone: eventBuilderFactory({
service: Modules.FULFILLMENT,
action: CommonEvents.DELETED,
object: "geo_zone",
eventsEnum: FulfillmentEvents,
}),
}
export function buildCreatedFulfillmentEvents({
fulfillments,
sharedContext,
}: {
fulfillments: Fulfillment[]
sharedContext: Context
}) {
if (!fulfillments.length) {
return
}
const fulfillments_: { id: string }[] = []
const addresses: { id: string }[] = []
const items: { id: string }[] = []
const labels: { id: string }[] = []
fulfillments.forEach((fulfillment) => {
fulfillments_.push({ id: fulfillment.id })
if (fulfillment.delivery_address) {
addresses.push({ id: fulfillment.delivery_address.id })
}
if (fulfillment.items) {
items.push(...fulfillment.items)
}
if (fulfillment.labels) {
labels.push(...fulfillment.labels)
}
})
eventBuilders.createdFulfillment({ data: fulfillments_, sharedContext })
eventBuilders.createdFulfillmentAddress({ data: addresses, sharedContext })
eventBuilders.createdFulfillmentItem({ data: items, sharedContext })
eventBuilders.createdFulfillmentLabel({ data: labels, sharedContext })
}
export function buildCreatedShippingOptionEvents({
shippingOptions,
sharedContext,
}: {
shippingOptions: ShippingOption[]
sharedContext: Context
}) {
if (!shippingOptions.length) {
return
}
const options: { id: string }[] = []
const types: ShippingOptionType[] = []
const rules: ShippingOptionRule[] = []
shippingOptions.forEach((shippingOption) => {
options.push({ id: shippingOption.id })
if (shippingOption.type) {
types.push(shippingOption.type)
}
if (shippingOption.rules) {
rules.push(...shippingOption.rules)
}
})
eventBuilders.createdShippingOption({ data: options, sharedContext })
eventBuilders.createdShippingOptionType({ data: types, sharedContext })
eventBuilders.createdShippingOptionRule({ data: rules, sharedContext })
}
export function buildCreatedFulfillmentSetEvents({
fulfillmentSets,
sharedContext,
}: {
fulfillmentSets: FulfillmentSet[]
sharedContext: Context
}): void {
if (!fulfillmentSets.length) {
return
}
const serviceZones: ServiceZone[] = []
fulfillmentSets.forEach((fulfillmentSet) => {
if (!fulfillmentSet.service_zones?.length) {
return
}
serviceZones.push(...fulfillmentSet.service_zones)
})
eventBuilders.createdFulfillmentSet({ data: fulfillmentSets, sharedContext })
buildCreatedServiceZoneEvents({ serviceZones, sharedContext })
}
export function buildCreatedServiceZoneEvents({
serviceZones,
sharedContext,
}: {
serviceZones: ServiceZone[]
sharedContext: Context
}): void {
if (!serviceZones.length) {
return
}
const geoZones: GeoZone[] = []
serviceZones.forEach((serviceZone) => {
if (!serviceZone.geo_zones.length) {
return
}
geoZones.push(...serviceZone.geo_zones)
})
eventBuilders.createdServiceZone({ data: serviceZones, sharedContext })
eventBuilders.createdGeoZone({ data: geoZones, sharedContext })
}
@@ -1 +1,2 @@
export * from './utils' export * from "./utils"
export * from "./events"
@@ -15,11 +15,11 @@ import {
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
InventoryEvents, InventoryEvents,
isDefined,
isString,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
ModulesSdkUtils, ModulesSdkUtils,
isDefined,
isString,
partitionArray, partitionArray,
promiseAll, promiseAll,
} from "@medusajs/utils" } from "@medusajs/utils"
@@ -198,11 +198,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
created.map((reservationItem) => ({ created.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_created, eventName: InventoryEvents.reservation_item_created,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
object: "reservation-item", object: "reservation-item",
}, context,
data: { id: reservationItem.id }, data: { id: reservationItem.id },
})) }))
) )
@@ -297,11 +296,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
result.map((inventoryItem) => ({ result.map((inventoryItem) => ({
eventName: InventoryEvents.created, eventName: InventoryEvents.created,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
object: "inventory-item", object: "inventory-item",
}, context,
data: { id: inventoryItem.id }, data: { id: inventoryItem.id },
})) }))
) )
@@ -349,11 +347,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
created.map((inventoryLevel) => ({ created.map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_created, eventName: InventoryEvents.inventory_level_created,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
object: "inventory-level", object: "inventory-level",
}, context,
data: { id: inventoryLevel.id }, data: { id: inventoryLevel.id },
})) }))
) )
@@ -408,11 +405,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
result.map((inventoryItem) => ({ result.map((inventoryItem) => ({
eventName: InventoryEvents.updated, eventName: InventoryEvents.updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "inventory-item", object: "inventory-item",
}, context,
data: { id: inventoryItem.id }, data: { id: inventoryItem.id },
})) }))
) )
@@ -448,11 +444,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
result[0].map((inventoryLevel) => ({ result[0].map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_deleted, eventName: InventoryEvents.inventory_level_deleted,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.DELETED, action: CommonEvents.DELETED,
object: "inventory-level", object: "inventory-level",
}, context,
data: { id: inventoryLevel.id }, data: { id: inventoryLevel.id },
})) }))
) )
@@ -480,11 +475,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData({ context.messageAggregator?.saveRawMessageData({
eventName: InventoryEvents.inventory_level_deleted, eventName: InventoryEvents.inventory_level_deleted,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.DELETED, action: CommonEvents.DELETED,
object: "inventory-level", object: "inventory-level",
}, context,
data: { id: inventoryLevel.id }, data: { id: inventoryLevel.id },
}) })
@@ -521,11 +515,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
levels.map((inventoryLevel) => ({ levels.map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_updated, eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "inventory-level", object: "inventory-level",
}, context,
data: { id: inventoryLevel.id }, data: { id: inventoryLevel.id },
})) }))
) )
@@ -606,11 +599,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
result.map((reservationItem) => ({ result.map((reservationItem) => ({
eventName: InventoryEvents.inventory_level_updated, eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "reservation-item", object: "reservation-item",
}, context,
data: { id: reservationItem.id }, data: { id: reservationItem.id },
})) }))
) )
@@ -738,11 +730,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
reservations.map((reservationItem) => ({ reservations.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_deleted, eventName: InventoryEvents.reservation_item_deleted,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.DELETED, action: CommonEvents.DELETED,
object: "reservation-item", object: "reservation-item",
}, context,
data: { id: reservationItem.id }, data: { id: reservationItem.id },
})) }))
) )
@@ -781,11 +772,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData( context.messageAggregator?.saveRawMessageData(
reservations.map((reservationItem) => ({ reservations.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_deleted, eventName: InventoryEvents.reservation_item_deleted,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.DELETED, action: CommonEvents.DELETED,
object: "reservation-item", object: "reservation-item",
}, context,
data: { id: reservationItem.id }, data: { id: reservationItem.id },
})) }))
) )
@@ -851,11 +841,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData({ context.messageAggregator?.saveRawMessageData({
eventName: InventoryEvents.inventory_level_updated, eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "inventory-level", object: "inventory-level",
}, context,
data: { id: result.id }, data: { id: result.id },
}) })
} }
@@ -1,19 +1,19 @@
import { import {
Context, Context,
DAL, DAL,
IEventBusModuleService,
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleJoinerConfig, ModuleJoinerConfig,
UserTypes,
ModulesSdkTypes, ModulesSdkTypes,
IEventBusModuleService, UserTypes,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
CommonEvents,
EmitEvents, EmitEvents,
InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
ModulesSdkUtils, ModulesSdkUtils,
InjectManager,
CommonEvents,
UserEvents, UserEvents,
} from "@medusajs/utils" } from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -94,11 +94,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({ invites.map((invite) => ({
eventName: UserEvents.invite_token_generated, eventName: UserEvents.invite_token_generated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: "token_generated", action: "token_generated",
object: "invite", object: "invite",
}, context: sharedContext,
data: { id: invite.id }, data: { id: invite.id },
})) }))
) )
@@ -150,11 +149,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
users.map((user) => ({ users.map((user) => ({
eventName: UserEvents.created, eventName: UserEvents.created,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
object: "user", object: "user",
}, context: sharedContext,
data: { id: user.id }, data: { id: user.id },
})) }))
) )
@@ -190,11 +188,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
updatedUsers.map((user) => ({ updatedUsers.map((user) => ({
eventName: UserEvents.updated, eventName: UserEvents.updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "user", object: "user",
}, context: sharedContext,
data: { id: user.id }, data: { id: user.id },
})) }))
) )
@@ -230,11 +227,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({ invites.map((invite) => ({
eventName: UserEvents.invite_created, eventName: UserEvents.invite_created,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.CREATED, action: CommonEvents.CREATED,
object: "invite", object: "invite",
}, context: sharedContext,
data: { id: invite.id }, data: { id: invite.id },
})) }))
) )
@@ -242,11 +238,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({ invites.map((invite) => ({
eventName: UserEvents.invite_token_generated, eventName: UserEvents.invite_token_generated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: "token_generated", action: "token_generated",
object: "invite", object: "invite",
}, context: sharedContext,
data: { id: invite.id }, data: { id: invite.id },
})) }))
) )
@@ -301,11 +296,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData( sharedContext.messageAggregator?.saveRawMessageData(
serializedInvites.map((invite) => ({ serializedInvites.map((invite) => ({
eventName: UserEvents.invite_updated, eventName: UserEvents.invite_updated,
metadata: {
service: this.constructor.name, service: this.constructor.name,
action: CommonEvents.UPDATED, action: CommonEvents.UPDATED,
object: "invite", object: "invite",
}, context: sharedContext,
data: { id: invite.id }, data: { id: invite.id },
})) }))
) )