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
2024-06-03 10:29:35 +02:00
committed by GitHub
parent 28d2a5347a
commit 9608bf06ef
29 changed files with 1716 additions and 348 deletions

View File

@@ -60,11 +60,29 @@ export type UpdateStockLocationSalesChannelsReq = {
}
export type CreateFulfillmentSetReq = CreateFulfillmentSetDTO
export type CreateServiceZoneReq = CreateServiceZoneDTO
export type UpdateServiceZoneReq = UpdateServiceZoneDTO
export type UpdateServiceZoneReq =
| UpdateServiceZoneDTO
// Shipping Options
export type CreateShippingOptionReq = CreateShippingOptionDTO
export type UpdateShippingOptionReq = UpdateShippingOptionDTO
// Shipping Options
| { region_id: string; amount: number }
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
export type CreateShippingProfileReq = CreateShippingProfileDTO

View File

@@ -14,7 +14,7 @@ export interface SuiteOptions<TService = unknown> {
}
}
export function moduleIntegrationTestRunner({
export function moduleIntegrationTestRunner<TService = any>({
moduleName,
moduleModels,
moduleOptions = {},
@@ -34,7 +34,7 @@ export function moduleIntegrationTestRunner({
injectedDependencies?: Record<string, any>
resolve?: string
debug?: boolean
testSuite: <TService = unknown>(options: SuiteOptions<TService>) => void
testSuite: (options: SuiteOptions<TService>) => void
}) {
const moduleSdkImports = require("@medusajs/modules-sdk")
@@ -111,7 +111,7 @@ export function moduleIntegrationTestRunner({
schema,
clientUrl: dbConfig.clientUrl,
},
} as SuiteOptions
} as SuiteOptions<TService>
const beforeEach_ = async () => {
await MikroOrmWrapper.setupDatabase()

View File

@@ -1,3 +1,5 @@
import { Context } from "../shared-context"
export type Subscriber<T = unknown> = (
data: T,
eventName: string
@@ -39,13 +41,12 @@ export type Message<T = unknown> = {
options?: Record<string, unknown>
}
export type MessageFormat<T = unknown> = {
export type RawMessageFormat<T = any> = {
eventName: string
metadata: {
service: string
action: string
object: string
eventGroupId?: string
}
data: T | T[]
data: T
service: string
object: string
action?: string
context?: Pick<Context, "eventGroupId">
options?: Record<string, any>
}

View File

@@ -110,5 +110,8 @@ export interface UpdateFulfillmentDTO {
/**
* The labels associated with the fulfillment.
*/
labels?: Omit<CreateFulfillmentLabelDTO, "fulfillment_id">[]
labels?: (
| Omit<CreateFulfillmentLabelDTO, "fulfillment_id">
| { id: string }
)[]
}

View File

@@ -3,6 +3,10 @@ import {
CreateCountryGeoZoneDTO,
CreateProvinceGeoZoneDTO,
CreateZipGeoZoneDTO,
UpdateCityGeoZoneDTO,
UpdateCountryGeoZoneDTO,
UpdateProvinceGeoZoneDTO,
UpdateZipGeoZoneDTO,
} from "./geo-zone"
/**
@@ -52,6 +56,10 @@ export interface UpdateServiceZoneDTO {
| Omit<CreateProvinceGeoZoneDTO, "service_zone_id">
| Omit<CreateCityGeoZoneDTO, "service_zone_id">
| Omit<CreateZipGeoZoneDTO, "service_zone_id">
| UpdateCountryGeoZoneDTO
| UpdateProvinceGeoZoneDTO
| UpdateCityGeoZoneDTO
| UpdateZipGeoZoneDTO
| {
/**
* The ID of the geo zone.

View File

@@ -46,14 +46,6 @@ export interface CreateShippingOptionDTO {
* The shipping option rules associated with the shipping option.
*/
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
}
)[]
/**
* The shipping option pricing
*/
prices: (
| { currency_code: string; amount: number; id?: string }
| { region_id: string; amount: number; id?: string }
)[]
}
/**

View File

@@ -30,8 +30,8 @@ export interface IMessageAggregator {
clearMessages(): void
saveRawMessageData<T>(
messageData:
| EventBusTypes.MessageFormat<T>
| EventBusTypes.MessageFormat<T>[],
| EventBusTypes.RawMessageFormat<T>
| EventBusTypes.RawMessageFormat<T>[],
options?: Record<string, unknown>
): void
}

View File

@@ -1,39 +1,4 @@
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
@@ -48,27 +13,29 @@ export function composeMessage(
{
data,
service,
entity,
object,
action,
context = {},
options,
}: {
data: unknown
data: any
service: string
entity: string
object: string
action?: string
context?: Context
options?: Record<string, unknown>
options?: Record<string, any>
}
): EventBusTypes.Message {
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")
}
const metadata: EventBusTypes.MessageBody["metadata"] = {
service,
object: entity,
object,
action: act!,
}

View File

@@ -1,11 +1,12 @@
import {
Context,
EventBusTypes,
IMessageAggregator,
Message,
MessageAggregatorFormat,
} from "@medusajs/types"
import { buildEventMessages } from "./build-event-messages"
import { composeMessage } from "./build-event-messages"
export class MessageAggregator implements IMessageAggregator {
private messages: Message[]
@@ -28,11 +29,25 @@ export class MessageAggregator implements IMessageAggregator {
saveRawMessageData<T>(
messageData:
| EventBusTypes.MessageFormat<T>
| EventBusTypes.MessageFormat<T>[],
options?: Record<string, unknown>
| EventBusTypes.RawMessageFormat<T>
| EventBusTypes.RawMessageFormat<T>[],
{
options,
sharedContext,
}: { options?: Record<string, unknown>; sharedContext?: Context } = {}
): 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): {

View File

@@ -58,7 +58,8 @@ type ReturnType<TNames extends string[]> = TNames extends [
* @param names
*/
export function buildEventNamesFromEntityName<TNames extends string[]>(
names: TNames
names: TNames,
prefix?: string
): ReturnType<TNames> {
const events = {}
@@ -69,16 +70,15 @@ export function buildEventNamesFromEntityName<TNames extends string[]>(
if (i === 0) {
for (const event of Object.values(CommonEvents) as string[]) {
events[event] = `${kebabCaseName}.${event}`
events[event] = `${prefix ? prefix + "." : ""}${kebabCaseName}.${event}`
}
continue
}
for (const event of Object.values(CommonEvents) as string[]) {
events[`${snakedCaseName}_${event}`] =
`${kebabCaseName}.${event}` as `${KebabCase<
typeof name
>}.${typeof event}`
events[`${snakedCaseName}_${event}`] = `${
prefix ? prefix + "." : ""
}${kebabCaseName}.${event}` as `${KebabCase<typeof name>}.${typeof event}`
}
}

View File

@@ -1,21 +1,33 @@
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
const eventBaseNames: [
"fulfillmentSet",
"serviceZone",
"geoZone",
"shippingOption",
"shippingOptionType",
"shippingProfile",
"shippingOptionRule",
"fulfillment"
"fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel"
] = [
"fulfillmentSet",
"serviceZone",
"geoZone",
"shippingOption",
"shippingOptionType",
"shippingProfile",
"shippingOptionRule",
"fulfillment",
"fulfillmentAddress",
"fulfillmentItem",
"fulfillmentLabel",
]
export const FulfillmentEvents = buildEventNamesFromEntityName(eventBaseNames)
export const FulfillmentEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.FULFILLMENT
)

View File

@@ -1,14 +1,13 @@
import { CommonEvents } from "../event-bus"
import { buildEventNamesFromEntityName } from "../event-bus"
import { Modules } from "../modules-sdk"
export const InventoryEvents = {
created: "inventory-item." + CommonEvents.CREATED,
updated: "inventory-item." + CommonEvents.UPDATED,
deleted: "inventory-item." + CommonEvents.DELETED,
restored: "inventory-item." + CommonEvents.RESTORED,
reservation_item_created: "reservation-item." + CommonEvents.CREATED,
reservation_item_updated: "reservation-item." + CommonEvents.UPDATED,
reservation_item_deleted: "reservation-item." + CommonEvents.DELETED,
inventory_level_deleted: "inventory-level." + CommonEvents.DELETED,
inventory_level_created: "inventory-level." + CommonEvents.CREATED,
inventory_level_updated: "inventory-level." + CommonEvents.UPDATED,
}
const eventBaseNames: ["inventoryItem", "reservationItem", "inventoryLevel"] = [
"inventoryItem",
"reservationItem",
"inventoryLevel",
]
export const InventoryEvents = buildEventNamesFromEntityName(
eventBaseNames,
Modules.INVENTORY
)

View File

@@ -1,16 +1,17 @@
import { MessageAggregator } from "../../event-bus"
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 (
target: any,
propertyKey: string | symbol,
descriptor: any
): void {
const aggregator = new MessageAggregator()
InjectIntoContext({
messageAggregator: () => aggregator,
messageAggregator: () => new MessageAggregator(),
})(target, propertyKey, descriptor)
const original = descriptor.value
@@ -18,6 +19,17 @@ export function EmitEvents(options: MessageAggregatorFormat = {} as MessageAggre
descriptor.value = async function (...args: any[]) {
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)])
aggregator.clearMessages()

View File

@@ -1,5 +1,3 @@
import { MessageAggregator } from "../../event-bus"
export function InjectIntoContext(
properties: Record<string, unknown | Function>
): MethodDecorator {

View File

@@ -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)
}
}

View File

@@ -9,3 +9,4 @@ export * from "./migration-scripts"
export * from "./internal-module-service-factory"
export * from "./abstract-module-service-factory"
export * from "./definition"
export * from "./event-builder-factory"

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 = {
created: "user." + CommonEvents.CREATED,
updated: "user." + CommonEvents.UPDATED,
invite_created: "invite." + CommonEvents.CREATED,
invite_updated: "invite." + CommonEvents.UPDATED,
invite_token_generated: "invite.token_generated",
...buildEventNamesFromEntityName(eventBaseNames, Modules.USER),
invite_token_generated: `${Modules.USER}.user.invite.token_generated`,
}

View File

@@ -7,15 +7,15 @@ import {
UpdateFulfillmentSetDTO,
} from "@medusajs/types"
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 { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000)
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => {
testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
@@ -625,6 +625,15 @@ moduleIntegrationTestRunner({
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 () {
@@ -655,6 +664,7 @@ moduleIntegrationTestRunner({
})
expect(updatedFulfillmentSets).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(2)
for (const data_ of updateData) {
const currentFullfillmentSet = fullfillmentSets.find(
@@ -668,6 +678,17 @@ moduleIntegrationTestRunner({
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,
})
)
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 () {
@@ -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 () {
@@ -897,11 +988,16 @@ moduleIntegrationTestRunner({
const updatedFulfillmentSets = await service.update(updateData)
expect(updatedFulfillmentSets).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(10)
for (const data_ of updateData) {
const expectedFulfillmentSet = updatedFulfillmentSets.find(
(f) => f.id === data_.id
)
const originalFulfillmentSet = createdFulfillmentSets.find(
(f) => f.id === data_.id
)
expect(expectedFulfillmentSet).toEqual(
expect.objectContaining({
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()
@@ -1002,6 +1139,7 @@ moduleIntegrationTestRunner({
const updatedFulfillmentSets = await service.update(updateData)
expect(updatedFulfillmentSets).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[1][0]).toHaveLength(6)
for (const data_ of updateData) {
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()

View File

@@ -1,11 +1,19 @@
import { resolve } from "path"
import { Modules } from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import {
IFulfillmentModuleService,
UpdateFulfillmentDTO,
} from "@medusajs/types"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import {
buildExpectedEventMessageShape,
generateCreateFulfillmentData,
generateCreateShippingOptionsData,
} from "../../__fixtures__"
import { FulfillmentEvents } from "@medusajs/utils"
jest.setTimeout(100000)
@@ -27,10 +35,20 @@ const moduleOptions = {
const providerId = "fixtures-fulfillment-provider_test-provider"
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
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("read", () => {
it("should list fulfillment", async () => {
@@ -103,6 +121,8 @@ moduleIntegrationTestRunner({
})
)
jest.clearAllMocks()
const fulfillment = await service.createFulfillment(
generateCreateFulfillmentData({
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 () => {
@@ -160,6 +208,8 @@ moduleIntegrationTestRunner({
})
)
jest.clearAllMocks()
const fulfillment = await service.createReturnFulfillment(
generateCreateFulfillmentData({
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,
})
)
jest.clearAllMocks()
})
it("should cancel a fulfillment successfully", async () => {
@@ -239,6 +454,16 @@ moduleIntegrationTestRunner({
expect(result.canceled_at).not.toBeNull()
expect(idempotentResult.canceled_at).not.toBeNull()
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 () => {

View File

@@ -4,14 +4,28 @@ import {
IFulfillmentModuleService,
UpdateGeoZoneDTO,
} from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000)
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
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("read", () => {
it("should list geo zones with a filter", async function () {
@@ -81,6 +95,8 @@ moduleIntegrationTestRunner({
country_code: "fr",
}
jest.clearAllMocks()
const geoZone = await service.createGeoZones(data)
expect(geoZone).toEqual(
@@ -90,6 +106,16 @@ moduleIntegrationTestRunner({
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 () {
@@ -115,9 +141,12 @@ moduleIntegrationTestRunner({
},
]
jest.clearAllMocks()
const geoZones = await service.createGeoZones(data)
expect(geoZones).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
let i = 0
for (const data_ of data) {
@@ -128,6 +157,18 @@ moduleIntegrationTestRunner({
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
}
})
@@ -213,6 +254,8 @@ moduleIntegrationTestRunner({
country_code: "us",
}
jest.clearAllMocks()
const updatedGeoZone = await service.updateGeoZones(updateData)
expect(updatedGeoZone).toEqual(
@@ -222,6 +265,15 @@ moduleIntegrationTestRunner({
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 () {
@@ -258,14 +310,18 @@ moduleIntegrationTestRunner({
})
)
jest.clearAllMocks()
const updatedGeoZones = await service.updateGeoZones(updateData)
expect(updatedGeoZones).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
for (const data_ of updateData) {
const expectedGeoZone = updatedGeoZones.find(
(geoZone) => geoZone.id === data_.id
)
)!
expect(expectedGeoZone).toEqual(
expect.objectContaining({
id: data_.id,
@@ -273,6 +329,17 @@ moduleIntegrationTestRunner({
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 },
}),
])
)
}
})
})

View File

@@ -5,14 +5,28 @@ import {
IFulfillmentModuleService,
UpdateServiceZoneDTO,
} from "@medusajs/types"
import { GeoZoneType } from "@medusajs/utils"
import { SuiteOptions, moduleIntegrationTestRunner } from "medusa-test-utils"
import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import { buildExpectedEventMessageShape } from "../../__fixtures__"
jest.setTimeout(100000)
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
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("read", () => {
it("should list service zones with a filter", async function () {
@@ -276,6 +290,14 @@ moduleIntegrationTestRunner({
type: GeoZoneType.COUNTRY,
country_code: "fr",
},
{
type: GeoZoneType.COUNTRY,
country_code: "us",
},
{
type: GeoZoneType.COUNTRY,
country_code: "uk",
},
],
}
@@ -283,35 +305,94 @@ moduleIntegrationTestRunner({
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",
geo_zones: [
{
id: createdServiceZone.geo_zones[0].id,
id: usGeoZone.id,
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(
createdServiceZone.id,
updateData
)
expect(updatedServiceZone.geo_zones).toHaveLength(3)
expect(updatedServiceZone).toEqual(
expect.objectContaining({
id: createdServiceZone.id,
name: updateData.name,
geo_zones: expect.arrayContaining([
expect.objectContaining({
id: updateData.geo_zones[0].id,
type: updateData.geo_zones[0].type,
country_code: updateData.geo_zones[0].country_code,
id: frGeoZone.id,
}),
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 () {
@@ -413,12 +494,16 @@ moduleIntegrationTestRunner({
})
)
jest.clearAllMocks()
const updatedServiceZones = await service.upsertServiceZones(
updateData
)
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) {
const expectedServiceZone = updatedServiceZones.find(
(serviceZone) => serviceZone.id === data_.id

View File

@@ -3,12 +3,18 @@ import {
CreateShippingOptionDTO,
IFulfillmentModuleService,
} from "@medusajs/types"
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
import { generateCreateShippingOptionsData } from "../../__fixtures__"
import {
MockEventBusService,
moduleIntegrationTestRunner,
} from "medusa-test-utils"
import {
buildExpectedEventMessageShape,
generateCreateShippingOptionsData,
} from "../../__fixtures__"
import { resolve } from "path"
import { FulfillmentProviderService } from "@services"
import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers"
import { GeoZoneType } from "@medusajs/utils"
import { FulfillmentEvents, GeoZoneType } from "@medusajs/utils"
import { UpdateShippingOptionDTO } from "@medusajs/types/src"
jest.setTimeout(100000)
@@ -34,10 +40,20 @@ const providerId = FulfillmentProviderService.getRegistrationIdentifier(
"test-provider"
)
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
moduleName: Modules.FULFILLMENT,
moduleOptions,
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => {
testSuite: ({ service }) => {
let eventBusEmitSpy
beforeEach(() => {
eventBusEmitSpy = jest.spyOn(MockEventBusService.prototype, "emit")
})
afterEach(() => {
jest.clearAllMocks()
})
describe("Fulfillment Module Service", () => {
describe("read", () => {
it("should list shipping options with a filter", async function () {
@@ -432,6 +448,8 @@ moduleIntegrationTestRunner({
provider_id: providerId,
})
jest.clearAllMocks()
const createdShippingOption = await service.createShippingOptions(
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 () {
@@ -491,11 +531,14 @@ moduleIntegrationTestRunner({
}),
]
jest.clearAllMocks()
const createdShippingOptions = await service.createShippingOptions(
createData
)
expect(createdShippingOptions).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(6)
let i = 0
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
}
})
@@ -623,6 +690,8 @@ moduleIntegrationTestRunner({
],
}
jest.clearAllMocks()
const updatedShippingOption = await service.updateShippingOptions(
updateData.id!,
updateData
@@ -681,6 +750,42 @@ moduleIntegrationTestRunner({
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 () => {
@@ -1092,6 +1197,8 @@ moduleIntegrationTestRunner({
shipping_option_id: shippingOption.id,
}
jest.clearAllMocks()
const rule = await service.createShippingOptionRules(ruleData)
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()
expect(rules).toHaveLength(2)
expect(rules).toEqual(
@@ -1157,6 +1274,8 @@ moduleIntegrationTestRunner({
value: "updated-test",
}
jest.clearAllMocks()
const updatedRule = await service.updateShippingOptionRules(
updateData
)
@@ -1169,6 +1288,15 @@ moduleIntegrationTestRunner({
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 () => {

View File

@@ -3,13 +3,28 @@ import {
CreateShippingProfileDTO,
IFulfillmentModuleService,
} 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)
moduleIntegrationTestRunner({
moduleIntegrationTestRunner<IFulfillmentModuleService>({
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("mutations", () => {
describe("on create", () => {
@@ -29,6 +44,16 @@ moduleIntegrationTestRunner({
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 () {
@@ -47,6 +72,7 @@ moduleIntegrationTestRunner({
await service.createShippingProfiles(createData)
expect(createdShippingProfiles).toHaveLength(2)
expect(eventBusEmitSpy.mock.calls[0][0]).toHaveLength(2)
let i = 0
for (const data_ of createData) {
@@ -56,6 +82,18 @@ moduleIntegrationTestRunner({
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
}
})

View File

@@ -115,7 +115,7 @@ export default class GeoZone {
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, " fgz")
this.id = generateEntityId(this.id, "fgz")
this.service_zone_id ??= this.service_zone?.id
}

View File

@@ -15,15 +15,16 @@ import {
} from "@medusajs/types"
import {
arrayDifference,
deepEqualObj,
EmitEvents,
FulfillmentUtils,
getSetDifference,
InjectManager,
InjectTransactionManager,
isDefined,
isPresent,
isString,
MedusaContext,
MedusaError,
Modules,
ModulesSdkUtils,
promiseAll,
} from "@medusajs/utils"
@@ -38,10 +39,18 @@ import {
ShippingOptionType,
ShippingProfile,
} from "@models"
import { isContextValid, validateAndNormalizeRules } from "@utils"
import {
buildCreatedFulfillmentEvents,
buildCreatedFulfillmentSetEvents,
buildCreatedServiceZoneEvents,
eventBuilders,
isContextValid,
validateAndNormalizeRules,
} from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import { UpdateShippingOptionsInput } from "../types/service"
import FulfillmentProviderService from "./fulfillment-provider"
import { buildCreatedShippingOptionEvents } from "../utils/events"
const generateMethodForModels = [
ServiceZone,
@@ -271,9 +280,13 @@ export default class FulfillmentModuleService<
> {
const createdFulfillmentSets = await this.create_(data, sharedContext)
const returnedFulfillmentSets = Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0]
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(createdFulfillmentSets)
>(returnedFulfillmentSets)
}
@InjectTransactionManager("baseRepository_")
@@ -282,7 +295,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity | TEntity[]> {
): Promise<TEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
@@ -304,14 +317,12 @@ export default class FulfillmentModuleService<
sharedContext
)
this.aggregateFulfillmentSetCreatedEvents(
createdFulfillmentSets,
sharedContext
)
buildCreatedFulfillmentSetEvents({
fulfillmentSets: createdFulfillmentSets,
sharedContext,
})
return Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0]
return createdFulfillmentSets
}
createServiceZones(
@@ -324,6 +335,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async createServiceZones(
data:
| FulfillmentTypes.CreateServiceZoneDTO[]
@@ -339,7 +351,7 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(createdServiceZones)
>(Array.isArray(data) ? createdServiceZones : createdServiceZones[0])
}
@InjectTransactionManager("baseRepository_")
@@ -348,7 +360,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TServiceZoneEntity | TServiceZoneEntity[]> {
): Promise<TServiceZoneEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
@@ -368,7 +380,12 @@ export default class FulfillmentModuleService<
sharedContext
)
return Array.isArray(data) ? createdServiceZones : createdServiceZones[0]
buildCreatedServiceZoneEvents({
serviceZones: createdServiceZones,
sharedContext,
})
return createdServiceZones
}
createShippingOptions(
@@ -381,6 +398,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async createShippingOptions(
data:
| FulfillmentTypes.CreateShippingOptionDTO[]
@@ -396,7 +414,7 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
>(createdShippingOptions)
>(Array.isArray(data) ? createdShippingOptions : createdShippingOptions[0])
}
@InjectTransactionManager("baseRepository_")
@@ -405,7 +423,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingOptionDTO[]
| FulfillmentTypes.CreateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionEntity | TShippingOptionEntity[]> {
): Promise<TShippingOptionEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
@@ -417,14 +435,17 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(rules as Record<string, unknown>[])
}
const createdShippingOptions = await this.shippingOptionService_.create(
const createdSO = await this.shippingOptionService_.create(
data_,
sharedContext
)
return Array.isArray(data)
? createdShippingOptions
: createdShippingOptions[0]
buildCreatedShippingOptionEvents({
shippingOptions: createdSO,
sharedContext,
})
return createdSO
}
createShippingProfiles(
@@ -437,6 +458,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectTransactionManager("baseRepository_")
@EmitEvents()
async createShippingProfiles(
data:
| FulfillmentTypes.CreateShippingProfileDTO[]
@@ -450,10 +472,17 @@ export default class FulfillmentModuleService<
sharedContext
)
eventBuilders.createdShippingProfile({
data: createdShippingProfiles,
sharedContext,
})
return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingProfileDTO
| FulfillmentTypes.ShippingProfileDTO[]
>(createdShippingProfiles)
>(
Array.isArray(data) ? createdShippingProfiles : createdShippingProfiles[0]
)
}
@InjectTransactionManager("baseRepository_")
@@ -462,21 +491,14 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingProfileDTO[]
| FulfillmentTypes.CreateShippingProfileDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TShippingProfileEntity[] | TShippingProfileEntity> {
): Promise<TShippingProfileEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const createdShippingProfiles = await this.shippingProfileService_.create(
data_,
sharedContext
)
return Array.isArray(data)
? createdShippingProfiles
: createdShippingProfiles[0]
return await this.shippingProfileService_.create(data_, sharedContext)
}
createGeoZones(
@@ -489,6 +511,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async createGeoZones(
data:
| FulfillmentTypes.CreateGeoZoneDTO
@@ -504,6 +527,11 @@ export default class FulfillmentModuleService<
sharedContext
)
eventBuilders.createdGeoZone({
data: createdGeoZones,
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
Array.isArray(data) ? createdGeoZones : createdGeoZones[0]
)
@@ -519,6 +547,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async createShippingOptionRules(
data:
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
@@ -536,7 +565,11 @@ export default class FulfillmentModuleService<
return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionRuleDTO
| FulfillmentTypes.ShippingOptionRuleDTO[]
>(createdShippingOptionRules)
>(
Array.isArray(data)
? createdShippingOptionRules
: createdShippingOptionRules[0]
)
}
@InjectTransactionManager("baseRepository_")
@@ -545,7 +578,7 @@ export default class FulfillmentModuleService<
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
| FulfillmentTypes.CreateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionRuleEntity | TShippingOptionRuleEntity[]> {
): Promise<TShippingOptionRuleEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
@@ -554,15 +587,21 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(data_ as unknown as Record<string, unknown>[])
const createdShippingOptionRules =
await this.shippingOptionRuleService_.create(data_, sharedContext)
const createdSORules = await this.shippingOptionRuleService_.create(
data_,
sharedContext
)
return Array.isArray(data)
? createdShippingOptionRules
: createdShippingOptionRules[0]
eventBuilders.createdShippingOptionRule({
data: createdSORules.map((sor) => ({ id: sor.id })),
sharedContext,
})
return createdSORules
}
@InjectManager("baseRepository_")
@EmitEvents()
async createFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
@@ -603,12 +642,18 @@ export default class FulfillmentModuleService<
throw error
}
buildCreatedFulfillmentEvents({
fulfillments: [fulfillment],
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
@InjectManager("baseRepository_")
@EmitEvents()
async createReturnFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
@@ -639,6 +684,11 @@ export default class FulfillmentModuleService<
throw error
}
buildCreatedFulfillmentEvents({
fulfillments: [fulfillment],
sharedContext,
})
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
@@ -654,6 +704,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async update(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
@MedusaContext() sharedContext: Context = {}
@@ -714,9 +765,11 @@ export default class FulfillmentModuleService<
fulfillmentSets.map((f) => [f.id, f])
)
// find service zones to delete
const serviceZoneIdsToDelete: string[] = []
const geoZoneIdsToDelete: string[] = []
const existingServiceZoneIds: string[] = []
const existingGeoZoneIds: string[] = []
data_.forEach((fulfillmentSet) => {
if (fulfillmentSet.service_zones) {
/**
@@ -734,6 +787,7 @@ export default class FulfillmentModuleService<
.filter((id): id is string => !!id)
)
)
if (toDeleteServiceZoneIds.size) {
serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds))
geoZoneIdsToDelete.push(
@@ -758,11 +812,13 @@ export default class FulfillmentModuleService<
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const expectedServiceZoneSet = new Set(
fulfillmentSet.service_zones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet,
serviceZonesSet
@@ -789,6 +845,16 @@ export default class FulfillmentModuleService<
}
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)!
}
)
@@ -797,6 +863,15 @@ export default class FulfillmentModuleService<
})
if (serviceZoneIdsToDelete.length) {
eventBuilders.deletedServiceZone({
data: serviceZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await promiseAll([
this.geoZoneService_.delete(
{
@@ -818,6 +893,32 @@ export default class FulfillmentModuleService<
sharedContext
)
eventBuilders.updatedFulfillmentSet({
data: updatedFulfillmentSets,
sharedContext,
})
const createdServiceZoneIds: string[] = []
const createdGeoZoneIds = updatedFulfillmentSets
.flatMap((f) =>
[...f.service_zones].flatMap((serviceZone) => {
if (!existingServiceZoneIds.includes(serviceZone.id)) {
createdServiceZoneIds.push(serviceZone.id)
}
return serviceZone.geo_zones.map((g) => g.id)
})
)
.filter((id) => !existingGeoZoneIds.includes(id))
eventBuilders.createdServiceZone({
data: createdServiceZoneIds.map((id) => ({ id })),
sharedContext,
})
eventBuilders.createdGeoZone({
data: createdGeoZoneIds.map((id) => ({ id })),
sharedContext,
})
return Array.isArray(data)
? updatedFulfillmentSets
: updatedFulfillmentSets[0]
@@ -835,6 +936,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager("baseRepository_")
@EmitEvents()
async updateServiceZones(
idOrSelector: string | FulfillmentTypes.FilterableServiceZoneProps,
data: FulfillmentTypes.UpdateServiceZoneDTO,
@@ -926,20 +1028,28 @@ export default class FulfillmentModuleService<
)
const geoZoneIdsToDelete: string[] = []
const existingGeoZoneIds: string[] = []
const updatedGeoZoneIds: string[] = []
data_.forEach((serviceZone) => {
if (serviceZone.geo_zones) {
const existingServiceZone = serviceZoneMap.get(serviceZone.id!)!
const existingGeoZones = existingServiceZone.geo_zones
const updatedGeoZones = serviceZone.geo_zones
const existingGeoZoneIdsForServiceZone = existingGeoZones.map(
(g) => g.id
)
const toDeleteGeoZoneIds = getSetDifference(
new Set(existingGeoZones.map((g) => g.id)),
new Set(existingGeoZoneIdsForServiceZone),
new Set(
updatedGeoZones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
)
existingGeoZoneIds.push(...existingGeoZoneIdsForServiceZone)
if (toDeleteGeoZoneIds.size) {
geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds))
}
@@ -978,12 +1088,25 @@ export default class FulfillmentModuleService<
}
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 }
})
}
})
if (geoZoneIdsToDelete.length) {
eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await this.geoZoneService_.delete(
{
id: geoZoneIdsToDelete,
@@ -997,6 +1120,26 @@ export default class FulfillmentModuleService<
sharedContext
)
eventBuilders.updatedServiceZone({
data: updatedServiceZones,
sharedContext,
})
const createdGeoZoneIds = updatedServiceZones
.flatMap((serviceZone) => {
return serviceZone.geo_zones.map((g) => g.id)
})
.filter((id) => !existingGeoZoneIds.includes(id))
eventBuilders.createdGeoZone({
data: createdGeoZoneIds.map((id) => ({ id })),
sharedContext,
})
eventBuilders.updatedGeoZone({
data: updatedGeoZoneIds.map((id) => ({ id })),
sharedContext,
})
return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0]
}
@@ -1010,11 +1153,12 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager("baseRepository_")
@EmitEvents()
async upsertServiceZones(
data:
| FulfillmentTypes.UpsertServiceZoneDTO
| FulfillmentTypes.UpsertServiceZoneDTO[],
sharedContext?: Context
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
> {
@@ -1055,9 +1199,11 @@ export default class FulfillmentModuleService<
forCreate,
sharedContext
)
const toPush = Array.isArray(createdServiceZones)
? createdServiceZones
: [createdServiceZones]
created.push(...toPush)
}
@@ -1066,9 +1212,11 @@ export default class FulfillmentModuleService<
forUpdate,
sharedContext
)
const toPush = Array.isArray(updatedServiceZones)
? updatedServiceZones
: [updatedServiceZones]
updated.push(...toPush)
}
@@ -1087,6 +1235,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
@InjectManager("baseRepository_")
@EmitEvents()
async updateShippingOptions(
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO,
@@ -1142,7 +1291,7 @@ export default class FulfillmentModuleService<
id: shippingOptionIds,
},
{
relations: ["rules"],
relations: ["rules", "type"],
take: shippingOptionIds.length,
},
sharedContext
@@ -1157,16 +1306,28 @@ export default class FulfillmentModuleService<
)
const ruleIdsToDelete: string[] = []
const updatedRuleIds: string[] = []
const existingRuleIds: string[] = []
const optionTypeDeletedIds: string[] = []
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) {
return
}
const existingShippingOption = existingShippingOptions.get(
shippingOption.id
)! // Garuantueed to exist since the validation above have been performed
const existingRules = existingShippingOption.rules
existingRuleIds.push(...existingRules.map((r) => r.id))
FulfillmentModuleService.validateMissingShippingOptionRules(
existingShippingOption,
shippingOption
@@ -1183,6 +1344,10 @@ export default class FulfillmentModuleService<
const existingRule = (existingRulesMap.get(rule.id) ??
{}) as FulfillmentTypes.UpdateShippingOptionRuleDTO
if (existingRulesMap.get(rule.id)) {
updatedRuleIds.push(rule.id)
}
const ruleData: FulfillmentTypes.UpdateShippingOptionRuleDTO = {
...existingRule,
...rule,
@@ -1198,8 +1363,6 @@ export default class FulfillmentModuleService<
validateAndNormalizeRules(updatedRules)
const updatedRuleIds = updatedRules.map((r) => "id" in r && r.id)
const toDeleteRuleIds = arrayDifference(
updatedRuleIds,
Array.from(existingRulesMap.keys())
@@ -1219,6 +1382,11 @@ export default class FulfillmentModuleService<
})
if (ruleIdsToDelete.length) {
eventBuilders.deletedShippingOptionRule({
data: ruleIdsToDelete.map((id) => ({ id })),
sharedContext,
})
await this.shippingOptionRuleService_.delete(
ruleIdsToDelete,
sharedContext
@@ -1230,11 +1398,71 @@ export default class FulfillmentModuleService<
sharedContext
)
this.handleShippingOptionUpdateEvents({
shippingOptionsData: dataArray,
updatedShippingOptions,
optionTypeDeletedIds,
updatedRuleIds,
existingRuleIds,
sharedContext,
})
return Array.isArray(data)
? updatedShippingOptions
: updatedShippingOptions[0]
}
private handleShippingOptionUpdateEvents({
shippingOptionsData,
updatedShippingOptions,
optionTypeDeletedIds,
updatedRuleIds,
existingRuleIds,
sharedContext,
}) {
eventBuilders.updatedShippingOption({
data: updatedShippingOptions,
sharedContext,
})
eventBuilders.deletedShippingOptionType({
data: optionTypeDeletedIds.map((id) => ({ id })),
sharedContext,
})
const createdOptionTypeIds = updatedShippingOptions
.filter((so) => {
const updateData = shippingOptionsData.find((sod) => sod.id === so.id)
return updateData?.type && !("id" in updateData.type)
})
.map((so) => so.type.id)
eventBuilders.createdShippingOptionType({
data: createdOptionTypeIds.map((id) => ({ id })),
sharedContext,
})
const createdRuleIds = updatedShippingOptions
.flatMap((so) =>
[...so.rules].map((rule) => {
if (existingRuleIds.includes(rule.id)) {
return
}
return rule.id
})
)
.filter((id): id is string => !!id)
eventBuilders.createdShippingOptionRule({
data: createdRuleIds.map((id) => ({ id })),
sharedContext,
})
eventBuilders.updatedShippingOptionRule({
data: updatedRuleIds.map((id) => ({ id })),
sharedContext,
})
}
async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO[],
sharedContext?: Context
@@ -1245,6 +1473,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async upsertShippingOptions(
data:
| FulfillmentTypes.UpsertShippingOptionDTO[]
@@ -1329,6 +1558,7 @@ export default class FulfillmentModuleService<
): Promise<
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 []
}
@@ -1342,6 +1572,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async updateGeoZones(
data:
| FulfillmentTypes.UpdateGeoZoneDTO
@@ -1357,10 +1588,15 @@ export default class FulfillmentModuleService<
FulfillmentModuleService.validateGeoZones(data_)
const updatedGeoZones = await this.geoZoneService_.update(
data,
data_,
sharedContext
)
eventBuilders.updatedGeoZone({
data: updatedGeoZones,
sharedContext,
})
const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.GeoZoneDTO[]
>(updatedGeoZones)
@@ -1378,6 +1614,7 @@ export default class FulfillmentModuleService<
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async updateShippingOptionRules(
data:
| FulfillmentTypes.UpdateShippingOptionRuleDTO[]
@@ -1416,31 +1653,140 @@ export default class FulfillmentModuleService<
const updatedShippingOptionRules =
await this.shippingOptionRuleService_.update(data_, sharedContext)
eventBuilders.updatedShippingOptionRule({
data: updatedShippingOptionRules.map((rule) => ({ id: rule.id })),
sharedContext,
})
return Array.isArray(data)
? updatedShippingOptionRules
: updatedShippingOptionRules[0]
}
@InjectTransactionManager("baseRepository_")
@InjectManager("baseRepository_")
@EmitEvents()
async updateFulfillment(
id: string,
data: FulfillmentTypes.UpdateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> {
const fulfillment = await this.fulfillmentService_.update(
{ id, ...data },
const fulfillment = await this.updateFulfillment_(id, data, sharedContext)
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
)
const updatedLabelIds: string[] = []
let deletedLabelIds: string[] = []
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
)
const serialized =
await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
this.handleFulfillmentUpdateEvents(
fulfillment,
existingLabelIds,
updatedLabelIds,
deletedLabelIds,
sharedContext
)
return Array.isArray(serialized) ? serialized[0] : serialized
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_")
@EmitEvents()
async cancelFulfillment(
id: string,
@MedusaContext() sharedContext: Context = {}
@@ -1473,6 +1819,11 @@ export default class FulfillmentModuleService<
},
sharedContext
)
eventBuilders.updatedFulfillment({
data: [{ id }],
sharedContext,
})
}
const result = await this.baseRepository_.serialize(fulfillment)
@@ -1791,60 +2142,4 @@ export default class FulfillmentModuleService<
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",
})
)
}
}
}
}
}

View File

@@ -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 })
}

View File

@@ -1 +1,2 @@
export * from './utils'
export * from "./utils"
export * from "./events"

View File

@@ -15,11 +15,11 @@ import {
InjectManager,
InjectTransactionManager,
InventoryEvents,
isDefined,
isString,
MedusaContext,
MedusaError,
ModulesSdkUtils,
isDefined,
isString,
partitionArray,
promiseAll,
} from "@medusajs/utils"
@@ -198,11 +198,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
created.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_created,
metadata: {
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "reservation-item",
},
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "reservation-item",
context,
data: { id: reservationItem.id },
}))
)
@@ -297,11 +296,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
result.map((inventoryItem) => ({
eventName: InventoryEvents.created,
metadata: {
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "inventory-item",
},
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "inventory-item",
context,
data: { id: inventoryItem.id },
}))
)
@@ -349,11 +347,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
created.map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_created,
metadata: {
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "inventory-level",
},
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "inventory-level",
context,
data: { id: inventoryLevel.id },
}))
)
@@ -408,11 +405,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
result.map((inventoryItem) => ({
eventName: InventoryEvents.updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-item",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-item",
context,
data: { id: inventoryItem.id },
}))
)
@@ -448,11 +444,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
result[0].map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_deleted,
metadata: {
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "inventory-level",
},
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "inventory-level",
context,
data: { id: inventoryLevel.id },
}))
)
@@ -480,11 +475,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData({
eventName: InventoryEvents.inventory_level_deleted,
metadata: {
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "inventory-level",
},
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "inventory-level",
context,
data: { id: inventoryLevel.id },
})
@@ -521,11 +515,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
levels.map((inventoryLevel) => ({
eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-level",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-level",
context,
data: { id: inventoryLevel.id },
}))
)
@@ -606,11 +599,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
result.map((reservationItem) => ({
eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "reservation-item",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "reservation-item",
context,
data: { id: reservationItem.id },
}))
)
@@ -738,11 +730,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
reservations.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_deleted,
metadata: {
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "reservation-item",
},
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "reservation-item",
context,
data: { id: reservationItem.id },
}))
)
@@ -781,11 +772,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData(
reservations.map((reservationItem) => ({
eventName: InventoryEvents.reservation_item_deleted,
metadata: {
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "reservation-item",
},
service: this.constructor.name,
action: CommonEvents.DELETED,
object: "reservation-item",
context,
data: { id: reservationItem.id },
}))
)
@@ -851,11 +841,10 @@ export default class InventoryModuleService<
context.messageAggregator?.saveRawMessageData({
eventName: InventoryEvents.inventory_level_updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-level",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "inventory-level",
context,
data: { id: result.id },
})
}

View File

@@ -1,19 +1,19 @@
import {
Context,
DAL,
IEventBusModuleService,
InternalModuleDeclaration,
ModuleJoinerConfig,
UserTypes,
ModulesSdkTypes,
IEventBusModuleService,
UserTypes,
} from "@medusajs/types"
import {
CommonEvents,
EmitEvents,
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
InjectManager,
CommonEvents,
UserEvents,
} from "@medusajs/utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
@@ -94,11 +94,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({
eventName: UserEvents.invite_token_generated,
metadata: {
service: this.constructor.name,
action: "token_generated",
object: "invite",
},
service: this.constructor.name,
action: "token_generated",
object: "invite",
context: sharedContext,
data: { id: invite.id },
}))
)
@@ -150,11 +149,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
users.map((user) => ({
eventName: UserEvents.created,
metadata: {
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "user",
},
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "user",
context: sharedContext,
data: { id: user.id },
}))
)
@@ -190,11 +188,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
updatedUsers.map((user) => ({
eventName: UserEvents.updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "user",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "user",
context: sharedContext,
data: { id: user.id },
}))
)
@@ -230,11 +227,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({
eventName: UserEvents.invite_created,
metadata: {
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "invite",
},
service: this.constructor.name,
action: CommonEvents.CREATED,
object: "invite",
context: sharedContext,
data: { id: invite.id },
}))
)
@@ -242,11 +238,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
invites.map((invite) => ({
eventName: UserEvents.invite_token_generated,
metadata: {
service: this.constructor.name,
action: "token_generated",
object: "invite",
},
service: this.constructor.name,
action: "token_generated",
object: "invite",
context: sharedContext,
data: { id: invite.id },
}))
)
@@ -301,11 +296,10 @@ export default class UserModuleService<
sharedContext.messageAggregator?.saveRawMessageData(
serializedInvites.map((invite) => ({
eventName: UserEvents.invite_updated,
metadata: {
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "invite",
},
service: this.constructor.name,
action: CommonEvents.UPDATED,
object: "invite",
context: sharedContext,
data: { id: invite.id },
}))
)