chore(): Module Internal Events (#13296)

* chore(): Ensure the product module emits all necessary events

* chore(): Ensure the product module emits all necessary events

* Update events tests

* more events and fixes

* more tests and category fixes

* more tests and category fixes

* Add todo

* update updateProduct_ event emitting and adjust test

* Adjust update products implementation to rely on already computed events

* rm unnecessary update variants events

* Fix formatting in changeset for product events

* refactor: Manage event emitting automatically (WIP)

* refactor: Manage event emitting automatically (WIP)

* chore(api-key): Add missing emit events and refactoring

* chore(cart): Add missing emit events and refactoring

* chore(customer): Add missing emit events and refactoring

* chore(fufillment, utils): Add missing emit events and refactoring

* chore(fufillment, utils): Add missing emit events and refactoring

* chore(inventory): Add missing emit events and refactoring

* chore(notification): Add missing emit events and refactoring

* chore(utils): Remove medusa service event handling legacy

* chore(product): Add missing emit events and refactoring

* chore(order): Add missing emit events and refactoring

* chore(payment): Add missing emit events and refactoring

* chore(pricing, util): Add missing emit events and refactoring, fix internal service upsertWithReplace event dispatching

* chore(promotions): Add missing emit events and refactoring

* chore(region): Add missing emit events and refactoring

* chore(sales-channel): Add missing emit events and refactoring

* chore(settings): Add missing emit events and refactoring

* chore(stock-location): Add missing emit events and refactoring

* chore(store): Add missing emit events and refactoring

* chore(taxes): Add missing emit events and refactoring

* chore(user): Add missing emit events and refactoring

* fix unit tests

* rm changeset for regeneration

* Create changeset for Medusa.js patch updates

Add a changeset for patch updates to multiple Medusa.js modules.

* rm unused product event builders

* address feedback

* remove old changeset

* fix event action for token generated

* fix user module events

* fix import

* fix promotion events

* add new module integration tests shard

* fix medusa service

* revert shard

* fix event action

* fix pipeline

* fix pipeline

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de PerettiandOli Juhl authored and GitHub committed 2025-09-10 14:37:38 +02:00
1 parent afe21741c4
commit e8822f3e69
55 files changed
+3614 -2353

No files matched your search

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