Files
medusa-store/packages/modules/fulfillment/src/services/fulfillment-module-service.ts
Adrien de Peretti e8822f3e69 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>
2025-09-10 14:37:38 +02:00

2347 lines
68 KiB
TypeScript

import {
CalculatedShippingOptionPrice,
Context,
DAL,
FilterableFulfillmentSetProps,
FindConfig,
FulfillmentDTO,
FulfillmentOption,
FulfillmentTypes,
IFulfillmentModuleService,
InferEntityType,
InternalModuleDeclaration,
Logger,
ModuleJoinerConfig,
ModulesSdkTypes,
ShippingOptionDTO,
SoftDeleteReturn,
UpdateFulfillmentSetDTO,
UpdateServiceZoneDTO,
ValidateFulfillmentDataContext,
} from "@medusajs/framework/types"
import {
arrayDifference,
deepCopy,
deepEqualObj,
EmitEvents,
getSetDifference,
InjectManager,
InjectTransactionManager,
isDefined,
isPresent,
isString,
MedusaContext,
MedusaError,
ModulesSdkUtils,
promiseAll,
} from "@medusajs/framework/utils"
import { isObject } from "@medusajs/utils"
import {
Fulfillment,
FulfillmentProvider,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingOptionRule,
ShippingOptionType,
ShippingProfile,
} from "@models"
import { isContextValid, Rule, validateAndNormalizeRules } from "@utils"
import { joinerConfig } from "../joiner-config"
import { UpdateShippingOptionsInput } from "../types/service"
import FulfillmentProviderService from "./fulfillment-provider"
const generateMethodForModels = {
FulfillmentSet,
ServiceZone,
ShippingOption,
GeoZone,
ShippingProfile,
ShippingOptionRule,
ShippingOptionType,
FulfillmentProvider,
// Not adding Fulfillment to not auto generate the methods under the hood and only provide the methods we want to expose
}
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
fulfillmentAddressService: ModulesSdkTypes.IMedusaInternalService<any>
fulfillmentSetService: ModulesSdkTypes.IMedusaInternalService<any>
serviceZoneService: ModulesSdkTypes.IMedusaInternalService<any>
geoZoneService: ModulesSdkTypes.IMedusaInternalService<any>
shippingProfileService: ModulesSdkTypes.IMedusaInternalService<any>
shippingOptionService: ModulesSdkTypes.IMedusaInternalService<any>
shippingOptionRuleService: ModulesSdkTypes.IMedusaInternalService<any>
shippingOptionTypeService: ModulesSdkTypes.IMedusaInternalService<any>
fulfillmentProviderService: FulfillmentProviderService
fulfillmentService: ModulesSdkTypes.IMedusaInternalService<any>
logger?: Logger
}
export default class FulfillmentModuleService
extends ModulesSdkUtils.MedusaService<{
FulfillmentSet: { dto: FulfillmentTypes.FulfillmentSetDTO }
ServiceZone: { dto: FulfillmentTypes.ServiceZoneDTO }
ShippingOption: { dto: FulfillmentTypes.ShippingOptionDTO }
GeoZone: { dto: FulfillmentTypes.GeoZoneDTO }
ShippingProfile: { dto: FulfillmentTypes.ShippingProfileDTO }
ShippingOptionRule: { dto: FulfillmentTypes.ShippingOptionRuleDTO }
ShippingOptionType: { dto: FulfillmentTypes.ShippingOptionTypeDTO }
FulfillmentProvider: { dto: FulfillmentTypes.FulfillmentProviderDTO }
}>(generateMethodForModels)
implements IFulfillmentModuleService
{
protected baseRepository_: DAL.RepositoryService
protected readonly fulfillmentSetService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof FulfillmentSet>
>
protected readonly serviceZoneService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ServiceZone>
>
protected readonly geoZoneService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof GeoZone>
>
protected readonly shippingProfileService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ShippingProfile>
>
protected readonly shippingOptionService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ShippingOption>
>
protected readonly shippingOptionRuleService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ShippingOptionRule>
>
protected readonly shippingOptionTypeService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof ShippingOptionType>
>
protected readonly fulfillmentProviderService_: FulfillmentProviderService
protected readonly fulfillmentService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Fulfillment>
>
constructor(
{
baseRepository,
fulfillmentSetService,
serviceZoneService,
geoZoneService,
shippingProfileService,
shippingOptionService,
shippingOptionRuleService,
shippingOptionTypeService,
fulfillmentProviderService,
fulfillmentService,
fulfillmentAddressService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.baseRepository_ = baseRepository
this.fulfillmentSetService_ = fulfillmentSetService
this.serviceZoneService_ = serviceZoneService
this.geoZoneService_ = geoZoneService
this.shippingProfileService_ = shippingProfileService
this.shippingOptionService_ = shippingOptionService
this.shippingOptionRuleService_ = shippingOptionRuleService
this.shippingOptionTypeService_ = shippingOptionTypeService
this.fulfillmentProviderService_ = fulfillmentProviderService
this.fulfillmentService_ = fulfillmentService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
@InjectManager()
// @ts-ignore
async listShippingOptions(
filters: FulfillmentTypes.FilterableShippingOptionForContextProps = {},
config: FindConfig<FulfillmentTypes.ShippingOptionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
// Eventually, we could call normalizeListShippingOptionsForContextParams to translate the address and make a and condition with the other filters
// In that case we could remote the address check below
if (filters?.context || filters?.address) {
return await this.listShippingOptionsForContext(
filters,
config,
sharedContext
)
}
return await super.listShippingOptions(filters, config, sharedContext)
}
@InjectManager()
async listShippingOptionsForContext(
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
config: FindConfig<ShippingOptionDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
const {
context,
config: normalizedConfig,
filters: normalizedFilters,
} = FulfillmentModuleService.normalizeListShippingOptionsForContextParams(
filters,
config
)
let shippingOptions = await this.shippingOptionService_.list(
normalizedFilters,
normalizedConfig,
sharedContext
)
if (context) {
shippingOptions = shippingOptions.filter((shippingOption) => {
if (!shippingOption.rules?.length) {
return true
}
return isContextValid(
context,
shippingOption.rules.map((r) => r) as unknown as Rule[]
)
})
}
return await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO[]
>(shippingOptions)
}
@InjectManager()
async retrieveFulfillment(
id: string,
config: FindConfig<FulfillmentTypes.FulfillmentDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> {
const fulfillment = await this.fulfillmentService_.retrieve(
id,
config,
sharedContext
)
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
@InjectManager()
async listFulfillments(
filters: FulfillmentTypes.FilterableFulfillmentProps = {},
config: FindConfig<FulfillmentTypes.FulfillmentDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO[]> {
const fulfillments = await this.fulfillmentService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentDTO[]
>(fulfillments)
}
@InjectManager()
async listAndCountFulfillments(
filters?: FilterableFulfillmentSetProps,
config?: FindConfig<FulfillmentDTO>,
@MedusaContext() sharedContext: Context = {}
): Promise<[FulfillmentDTO[], number]> {
const [fulfillments, count] = await this.fulfillmentService_.listAndCount(
filters,
config,
sharedContext
)
return [
await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO[]>(
fulfillments
),
count,
]
}
// @ts-expect-error
createFulfillmentSets(
data: FulfillmentTypes.CreateFulfillmentSetDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO[]>
// @ts-expect-error
createFulfillmentSets(
data: FulfillmentTypes.CreateFulfillmentSetDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createFulfillmentSets(
data:
| FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
> {
const createdFulfillmentSets = await this.createFulfillmentSets_(
data,
sharedContext
)
const returnedFulfillmentSets = Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0]
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(returnedFulfillmentSets)
}
@InjectTransactionManager()
protected async createFulfillmentSets_(
data:
| FulfillmentTypes.CreateFulfillmentSetDTO
| FulfillmentTypes.CreateFulfillmentSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof FulfillmentSet>[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
for (const fulfillmentSet of data_) {
if (fulfillmentSet.service_zones?.length) {
for (const serviceZone of fulfillmentSet.service_zones) {
if (serviceZone.geo_zones?.length) {
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
}
}
}
}
const createdFulfillmentSets = await this.fulfillmentSetService_.create(
data_,
sharedContext
)
return createdFulfillmentSets
}
// @ts-ignore
createServiceZones(
data: FulfillmentTypes.CreateServiceZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO[]>
// @ts-expect-error
createServiceZones(
data: FulfillmentTypes.CreateServiceZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createServiceZones(
data:
| FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
> {
const createdServiceZones = await this.createServiceZones_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(Array.isArray(data) ? createdServiceZones : createdServiceZones[0])
}
@InjectTransactionManager()
protected async createServiceZones_(
data:
| FulfillmentTypes.CreateServiceZoneDTO[]
| FulfillmentTypes.CreateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ServiceZone>[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
for (const serviceZone of data_) {
if (serviceZone.geo_zones?.length) {
if (serviceZone.geo_zones?.length) {
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
}
}
}
const createdServiceZones = await this.serviceZoneService_.create(
data_,
sharedContext
)
return createdServiceZones
}
// @ts-ignore
createShippingOptions(
data: FulfillmentTypes.CreateShippingOptionDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
// @ts-expect-error
createShippingOptions(
data: FulfillmentTypes.CreateShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createShippingOptions(
data:
| FulfillmentTypes.CreateShippingOptionDTO[]
| FulfillmentTypes.CreateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
> {
const createdShippingOptions = await this.createShippingOptions_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
>(Array.isArray(data) ? createdShippingOptions : createdShippingOptions[0])
}
@InjectTransactionManager()
async createShippingOptions_(
data:
| FulfillmentTypes.CreateShippingOptionDTO[]
| FulfillmentTypes.CreateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ShippingOption>[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const rules = data_.flatMap((d) => d.rules).filter(Boolean)
if (rules.length) {
validateAndNormalizeRules(rules as Record<string, unknown>[])
}
const createdSO = await this.shippingOptionService_.create(
data_,
sharedContext
)
return createdSO
}
// @ts-ignore
createShippingProfiles(
data: FulfillmentTypes.CreateShippingProfileDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO[]>
// @ts-expect-error
createShippingProfiles(
data: FulfillmentTypes.CreateShippingProfileDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createShippingProfiles(
data:
| FulfillmentTypes.CreateShippingProfileDTO[]
| FulfillmentTypes.CreateShippingProfileDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[]
> {
const createdShippingProfiles = await this.createShippingProfiles_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingProfileDTO
| FulfillmentTypes.ShippingProfileDTO[]
>(
Array.isArray(data) ? createdShippingProfiles : createdShippingProfiles[0]
)
}
@InjectTransactionManager()
async createShippingProfiles_(
data:
| FulfillmentTypes.CreateShippingProfileDTO[]
| FulfillmentTypes.CreateShippingProfileDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ShippingProfile>[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
return await this.shippingProfileService_.create(data_, sharedContext)
}
// @ts-expect-error
createGeoZones(
data: FulfillmentTypes.CreateGeoZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO[]>
// @ts-expect-error
createGeoZones(
data: FulfillmentTypes.CreateGeoZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createGeoZones(
data:
| FulfillmentTypes.CreateGeoZoneDTO
| FulfillmentTypes.CreateGeoZoneDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
const data_ = Array.isArray(data) ? data : [data]
FulfillmentModuleService.validateGeoZones(data_)
const createdGeoZones = await this.geoZoneService_.create(
data_,
sharedContext
)
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
Array.isArray(data) ? createdGeoZones : createdGeoZones[0]
)
}
// @ts-expect-error
async createShippingOptionRules(
data: FulfillmentTypes.CreateShippingOptionRuleDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionRuleDTO[]>
// @ts-expect-error
async createShippingOptionRules(
data: FulfillmentTypes.CreateShippingOptionRuleDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createShippingOptionRules(
data:
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
| FulfillmentTypes.CreateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| FulfillmentTypes.ShippingOptionRuleDTO
| FulfillmentTypes.ShippingOptionRuleDTO[]
> {
const createdShippingOptionRules = await this.createShippingOptionRules_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionRuleDTO
| FulfillmentTypes.ShippingOptionRuleDTO[]
>(
Array.isArray(data)
? createdShippingOptionRules
: createdShippingOptionRules[0]
)
}
@InjectTransactionManager()
async createShippingOptionRules_(
data:
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
| FulfillmentTypes.CreateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ShippingOptionRule>[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
validateAndNormalizeRules(data_ as unknown as Record<string, unknown>[])
const createdSORules = await this.shippingOptionRuleService_.create(
data_,
sharedContext
)
return createdSORules
}
@InjectManager()
@EmitEvents()
async createFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> {
const { order, ...fulfillmentDataToCreate } = data
const fulfillment = await this.fulfillmentService_.create(
fulfillmentDataToCreate,
sharedContext
)
const {
items,
data: fulfillmentData,
provider_id,
...fulfillmentRest
} = fulfillment
try {
const providerResult =
await this.fulfillmentProviderService_.createFulfillment(
provider_id!, // TODO: should we add a runtime check on provider_id being provided?
fulfillmentData || {},
items.map((i) => i),
order,
fulfillmentRest as unknown as Partial<FulfillmentDTO>
)
await this.fulfillmentService_.update(
{
id: fulfillment.id,
data: providerResult.data ?? {},
labels: providerResult.labels ?? [],
},
sharedContext
)
} catch (error) {
await this.fulfillmentService_.delete(fulfillment.id, sharedContext)
throw error
}
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
@InjectManager()
@EmitEvents()
async deleteFulfillment(
id: string,
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const fulfillment = await this.fulfillmentService_.retrieve(
id,
{},
sharedContext
)
if (!isPresent(fulfillment.canceled_at)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Fulfillment with id ${fulfillment.id} needs to be canceled first before deleting`
)
}
await this.fulfillmentService_.delete(id, sharedContext)
}
@InjectManager()
@EmitEvents()
async createReturnFulfillment(
data: FulfillmentTypes.CreateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> {
const { order, ...fulfillmentDataToCreate } = data
const fulfillment = await this.fulfillmentService_.create(
fulfillmentDataToCreate,
sharedContext
)
const shippingOption = await this.shippingOptionService_.retrieve(
fulfillment.shipping_option_id!,
{
select: ["id", "name", "data", "metadata"],
},
sharedContext
)
try {
const providerResult =
await this.fulfillmentProviderService_.createReturn(
fulfillment.provider_id!, // TODO: should we add a runtime check on provider_id being provided?,
{
...fulfillment,
shipping_option: shippingOption,
} as Record<any, any>
)
await this.fulfillmentService_.update(
{
id: fulfillment.id,
data: providerResult.data ?? {},
labels: providerResult.labels ?? [],
},
sharedContext
)
} catch (error) {
await this.fulfillmentService_.delete(fulfillment.id, sharedContext)
throw error
}
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
// @ts-expect-error
updateFulfillmentSets(
data: FulfillmentTypes.UpdateFulfillmentSetDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO[]>
// @ts-expect-error
updateFulfillmentSets(
data: FulfillmentTypes.UpdateFulfillmentSetDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.FulfillmentSetDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateFulfillmentSets(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.FulfillmentSetDTO[] | FulfillmentTypes.FulfillmentSetDTO
> {
const updatedFulfillmentSets = await this.updateFulfillmentSets_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
FulfillmentTypes.FulfillmentSetDTO | FulfillmentTypes.FulfillmentSetDTO[]
>(updatedFulfillmentSets)
}
@InjectTransactionManager()
protected async updateFulfillmentSets_(
data: UpdateFulfillmentSetDTO[] | UpdateFulfillmentSetDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| InferEntityType<typeof FulfillmentSet>[]
| InferEntityType<typeof FulfillmentSet>
> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const fulfillmentSetIds = data_.map((f) => f.id)
if (!fulfillmentSetIds.length) {
return []
}
const fulfillmentSets = await this.fulfillmentSetService_.list(
{
id: fulfillmentSetIds,
},
{
relations: ["service_zones", "service_zones.geo_zones"],
take: fulfillmentSetIds.length,
},
sharedContext
)
const fulfillmentSetSet = new Set(fulfillmentSets.map((f) => f.id))
const expectedFulfillmentSetSet = new Set(data_.map((f) => f.id))
const missingFulfillmentSetIds = getSetDifference(
expectedFulfillmentSetSet,
fulfillmentSetSet
)
if (missingFulfillmentSetIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following fulfillment sets does not exists: ${Array.from(
missingFulfillmentSetIds
).join(", ")}`
)
}
const fulfillmentSetMap = new Map<
string,
InferEntityType<typeof FulfillmentSet>
>(fulfillmentSets.map((f) => [f.id, f]))
const serviceZoneIdsToDelete: string[] = []
const geoZoneIdsToDelete: string[] = []
const existingServiceZoneIds: string[] = []
const existingGeoZoneIds: string[] = []
data_.forEach((fulfillmentSet) => {
if (fulfillmentSet.service_zones) {
/**
* Detect and delete service zones that are not in the updated
*/
const existingFulfillmentSet = fulfillmentSetMap.get(fulfillmentSet.id)!
const existingServiceZones = existingFulfillmentSet.service_zones
const updatedServiceZones = fulfillmentSet.service_zones
const toDeleteServiceZoneIds = getSetDifference(
new Set(existingServiceZones.map((s) => s.id)),
new Set(
updatedServiceZones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
)
if (toDeleteServiceZoneIds.size) {
serviceZoneIdsToDelete.push(...Array.from(toDeleteServiceZoneIds))
geoZoneIdsToDelete.push(
...existingServiceZones
.filter((s) => toDeleteServiceZoneIds.has(s.id))
.flatMap((s) => s.geo_zones.map((g) => g.id))
)
}
/**
* Detect and re assign service zones to the fulfillment set that are still present
*/
const serviceZonesMap = new Map(
existingFulfillmentSet.service_zones.map((serviceZone) => [
serviceZone.id,
serviceZone,
])
)
const serviceZonesSet = new Set(
existingServiceZones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const expectedServiceZoneSet = new Set(
fulfillmentSet.service_zones
.map((s) => "id" in s && s.id)
.filter((id): id is string => !!id)
)
const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet,
serviceZonesSet
)
if (missingServiceZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following service zones does not exists: ${Array.from(
missingServiceZoneIds
).join(", ")}`
)
}
// re assign service zones to the fulfillment set
if (fulfillmentSet.service_zones) {
fulfillmentSet.service_zones = fulfillmentSet.service_zones.map(
(serviceZone) => {
if (!("id" in serviceZone)) {
if (serviceZone.geo_zones?.length) {
FulfillmentModuleService.validateGeoZones(
serviceZone.geo_zones
)
}
return serviceZone
}
const existingServiceZone = serviceZonesMap.get(serviceZone.id)!
existingServiceZoneIds.push(existingServiceZone.id)
if (existingServiceZone.geo_zones.length) {
existingGeoZoneIds.push(
...existingServiceZone.geo_zones.map((g) => g.id)
)
}
return serviceZonesMap.get(serviceZone.id)!
}
)
}
}
})
if (serviceZoneIdsToDelete.length) {
await promiseAll([
this.geoZoneService_.delete(
{
id: geoZoneIdsToDelete,
},
sharedContext
),
this.serviceZoneService_.delete(
{
id: serviceZoneIdsToDelete,
},
sharedContext
),
])
}
const updatedFulfillmentSets = await this.fulfillmentSetService_.update(
data_,
sharedContext
)
return Array.isArray(data)
? updatedFulfillmentSets
: updatedFulfillmentSets[0]
}
// @ts-ignore
updateServiceZones(
id: string,
data: FulfillmentTypes.UpdateServiceZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO>
// @ts-expect-error
updateServiceZones(
selector: FulfillmentTypes.FilterableServiceZoneProps,
data: FulfillmentTypes.UpdateServiceZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateServiceZones(
idOrSelector: string | FulfillmentTypes.FilterableServiceZoneProps,
data: FulfillmentTypes.UpdateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO
> {
const normalizedInput: UpdateServiceZoneDTO[] = []
if (isString(idOrSelector)) {
normalizedInput.push({ id: idOrSelector, ...data })
} else {
const serviceZones = await this.serviceZoneService_.list(
{ ...idOrSelector },
{},
sharedContext
)
if (!serviceZones.length) {
return []
}
for (const serviceZone of serviceZones) {
normalizedInput.push({ id: serviceZone.id, ...data })
}
}
const updatedServiceZones = await this.updateServiceZones_(
normalizedInput,
sharedContext
)
const toReturn = isString(idOrSelector)
? updatedServiceZones[0]
: updatedServiceZones
return await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
>(toReturn)
}
@InjectTransactionManager()
protected async updateServiceZones_(
data:
| FulfillmentTypes.UpdateServiceZoneDTO[]
| FulfillmentTypes.UpdateServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
InferEntityType<typeof ServiceZone> | InferEntityType<typeof ServiceZone>[]
> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
const serviceZoneIds = data_.map((s) => s.id)
if (!serviceZoneIds.length) {
return []
}
const serviceZones = await this.serviceZoneService_.list(
{
id: serviceZoneIds,
},
{
relations: ["geo_zones"],
take: serviceZoneIds.length,
},
sharedContext
)
const serviceZoneSet = new Set(serviceZones.map((s) => s.id))
const expectedServiceZoneSet = new Set(data_.map((s) => s.id))
const missingServiceZoneIds = getSetDifference(
expectedServiceZoneSet,
serviceZoneSet
)
if (missingServiceZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following service zones does not exists: ${Array.from(
missingServiceZoneIds
).join(", ")}`
)
}
const serviceZoneMap = new Map<string, InferEntityType<typeof ServiceZone>>(
serviceZones.map((s) => [s.id, s])
)
const geoZoneIdsToDelete: string[] = []
const existingGeoZoneIds: string[] = []
const updatedGeoZoneIds: string[] = []
data_.forEach((serviceZone) => {
if (serviceZone.geo_zones) {
const existingServiceZone = serviceZoneMap.get(serviceZone.id!)!
const existingGeoZones = existingServiceZone.geo_zones
const updatedGeoZones = serviceZone.geo_zones
const existingGeoZoneIdsForServiceZone = existingGeoZones.map(
(g) => g.id
)
const toDeleteGeoZoneIds = getSetDifference(
new Set(existingGeoZoneIdsForServiceZone),
new Set(
updatedGeoZones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
)
existingGeoZoneIds.push(...existingGeoZoneIdsForServiceZone)
if (toDeleteGeoZoneIds.size) {
geoZoneIdsToDelete.push(...Array.from(toDeleteGeoZoneIds))
}
const geoZonesMap = new Map(
existingServiceZone.geo_zones.map((geoZone) => [geoZone.id, geoZone])
)
const geoZonesSet = new Set(
existingGeoZones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
const expectedGeoZoneSet = new Set(
serviceZone.geo_zones
.map((g) => "id" in g && g.id)
.filter((id): id is string => !!id)
)
const missingGeoZoneIds = getSetDifference(
expectedGeoZoneSet,
geoZonesSet
)
if (missingGeoZoneIds.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following geo zones does not exists: ${Array.from(
missingGeoZoneIds
).join(", ")}`
)
}
serviceZone.geo_zones = serviceZone.geo_zones.map((geoZone) => {
if (!("id" in geoZone)) {
FulfillmentModuleService.validateGeoZones([geoZone])
return geoZone
}
const existing = geoZonesMap.get(geoZone.id)!
// If only the id is provided we dont consider it as an update
if (
Object.keys(geoZone).length > 1 &&
!deepEqualObj(existing, geoZone)
) {
updatedGeoZoneIds.push(geoZone.id)
}
return { ...existing, ...geoZone }
})
}
})
if (geoZoneIdsToDelete.length) {
await this.geoZoneService_.delete(
{
id: geoZoneIdsToDelete,
},
sharedContext
)
}
const updatedServiceZones = await this.serviceZoneService_.update(
data_,
sharedContext
)
return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0]
}
upsertServiceZones(
data: FulfillmentTypes.UpsertServiceZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO>
upsertServiceZones(
data: FulfillmentTypes.UpsertServiceZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ServiceZoneDTO[]>
@InjectManager()
@EmitEvents()
async upsertServiceZones(
data:
| FulfillmentTypes.UpsertServiceZoneDTO
| FulfillmentTypes.UpsertServiceZoneDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ServiceZoneDTO | FulfillmentTypes.ServiceZoneDTO[]
> {
const upsertServiceZones = await this.upsertServiceZones_(
data,
sharedContext
)
const allServiceZones = await this.baseRepository_.serialize<
FulfillmentTypes.ServiceZoneDTO[] | FulfillmentTypes.ServiceZoneDTO
>(upsertServiceZones)
return Array.isArray(data) ? allServiceZones : allServiceZones[0]
}
@InjectTransactionManager()
async upsertServiceZones_(
data:
| FulfillmentTypes.UpsertServiceZoneDTO[]
| FulfillmentTypes.UpsertServiceZoneDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
InferEntityType<typeof ServiceZone>[] | InferEntityType<typeof ServiceZone>
> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(serviceZone): serviceZone is FulfillmentTypes.UpdateServiceZoneDTO =>
!!serviceZone.id
)
const forCreate = input.filter(
(serviceZone): serviceZone is FulfillmentTypes.CreateServiceZoneDTO =>
!serviceZone.id
)
const created: InferEntityType<typeof ServiceZone>[] = []
const updated: InferEntityType<typeof ServiceZone>[] = []
if (forCreate.length) {
const createdServiceZones = await this.createServiceZones_(
forCreate,
sharedContext
)
const toPush = Array.isArray(createdServiceZones)
? createdServiceZones
: [createdServiceZones]
created.push(...toPush)
}
if (forUpdate.length) {
const updatedServiceZones = await this.updateServiceZones_(
forUpdate,
sharedContext
)
const toPush = Array.isArray(updatedServiceZones)
? updatedServiceZones
: [updatedServiceZones]
updated.push(...toPush)
}
return [...created, ...updated]
}
// @ts-ignore
updateShippingOptions(
id: string,
data: FulfillmentTypes.UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO>
// @ts-expect-error
updateShippingOptions(
selector: FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateShippingOptions(
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
> {
const normalizedInput: UpdateShippingOptionsInput[] = []
if (isString(idOrSelector)) {
normalizedInput.push({ id: idOrSelector, ...data })
} else {
const shippingOptions = await this.shippingOptionService_.list(
idOrSelector,
{},
sharedContext
)
shippingOptions.forEach((shippingOption) => {
normalizedInput.push({ id: shippingOption.id, ...data })
})
}
const updatedShippingOptions = await this.updateShippingOptions_(
normalizedInput,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
>(updatedShippingOptions)
return isString(idOrSelector) ? serialized[0] : serialized
}
@InjectTransactionManager()
async updateShippingOptions_(
data: UpdateShippingOptionsInput[] | UpdateShippingOptionsInput,
@MedusaContext() sharedContext: Context = {}
): Promise<
| InferEntityType<typeof ShippingOption>
| InferEntityType<typeof ShippingOption>[]
> {
const dataArray = Array.isArray(data)
? data.map((d) => deepCopy(d))
: [deepCopy(data)]
if (!dataArray.length) {
return []
}
const shippingOptionIds = dataArray.map((s) => s.id)
if (!shippingOptionIds.length) {
return []
}
const shippingOptions = await this.shippingOptionService_.list(
{
id: shippingOptionIds,
},
{
relations: ["rules", "type"],
take: shippingOptionIds.length,
},
sharedContext
)
const existingShippingOptions = new Map(
shippingOptions.map((s) => [s.id, s])
)
FulfillmentModuleService.validateMissingShippingOptions_(
shippingOptions,
dataArray
)
const ruleIdsToDelete: string[] = []
const updatedRuleIds: string[] = []
const existingRuleIds: string[] = []
const optionTypeDeletedIds: string[] = []
dataArray.forEach((shippingOption) => {
const existingShippingOption = existingShippingOptions.get(
shippingOption.id
)! // Guaranteed to exist since the validation above have been performed
if (isObject(shippingOption.type) && !("id" in shippingOption.type)) {
optionTypeDeletedIds.push(existingShippingOption.type.id)
}
if (!shippingOption.rules) {
return
}
const existingRules = existingShippingOption.rules
existingRuleIds.push(...existingRules.map((r) => r.id))
FulfillmentModuleService.validateMissingShippingOptionRules(
existingShippingOption,
shippingOption
)
const existingRulesMap: Map<
string,
| FulfillmentTypes.UpdateShippingOptionRuleDTO
| InferEntityType<typeof ShippingOptionRule>
> = new Map(existingRules.map((rule) => [rule.id, rule]))
const updatedRules = shippingOption.rules
.map((rule) => {
if ("id" in rule) {
const existingRule = (existingRulesMap.get(rule.id) ??
{}) as FulfillmentTypes.UpdateShippingOptionRuleDTO
if (existingRulesMap.get(rule.id)) {
updatedRuleIds.push(rule.id)
}
// @ts-ignore
delete rule.created_at
// @ts-ignore
delete rule.updated_at
// @ts-ignore
delete rule.deleted_at
const ruleData: FulfillmentTypes.UpdateShippingOptionRuleDTO = {
...existingRule,
...rule,
}
existingRulesMap.set(rule.id, ruleData)
return ruleData
}
return
})
.filter(Boolean) as FulfillmentTypes.UpdateShippingOptionRuleDTO[]
validateAndNormalizeRules(updatedRules)
const toDeleteRuleIds = arrayDifference(
updatedRuleIds,
Array.from(existingRulesMap.keys())
) as string[]
if (toDeleteRuleIds.length) {
ruleIdsToDelete.push(...toDeleteRuleIds)
}
shippingOption.rules = shippingOption.rules.map((rule) => {
if (!("id" in rule)) {
validateAndNormalizeRules([rule])
return rule
}
return existingRulesMap.get(rule.id)!
})
})
if (ruleIdsToDelete.length) {
await this.shippingOptionRuleService_.delete(
ruleIdsToDelete,
sharedContext
)
}
const updatedShippingOptions = await this.shippingOptionService_.update(
dataArray,
sharedContext
)
return Array.isArray(data)
? updatedShippingOptions
: updatedShippingOptions[0]
}
async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager()
@EmitEvents()
async upsertShippingOptions(
data:
| FulfillmentTypes.UpsertShippingOptionDTO[]
| FulfillmentTypes.UpsertShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
> {
const upsertedShippingOptions = await this.upsertShippingOptions_(
data,
sharedContext
)
const allShippingOptions = await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
>(upsertedShippingOptions)
return Array.isArray(data) ? allShippingOptions : allShippingOptions[0]
}
@InjectTransactionManager()
async upsertShippingOptions_(
data:
| FulfillmentTypes.UpsertShippingOptionDTO[]
| FulfillmentTypes.UpsertShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof ShippingOption>[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(shippingOption): shippingOption is UpdateShippingOptionsInput =>
!!shippingOption.id
)
const forCreate = input.filter(
(
shippingOption
): shippingOption is FulfillmentTypes.CreateShippingOptionDTO =>
!shippingOption.id
)
let created: InferEntityType<typeof ShippingOption>[] = []
let updated: InferEntityType<typeof ShippingOption>[] = []
if (forCreate.length) {
const createdShippingOptions = await this.createShippingOptions_(
forCreate,
sharedContext
)
const toPush = Array.isArray(createdShippingOptions)
? createdShippingOptions
: [createdShippingOptions]
created.push(...toPush)
}
if (forUpdate.length) {
const updatedShippingOptions = await this.updateShippingOptions_(
forUpdate,
sharedContext
)
const toPush = Array.isArray(updatedShippingOptions)
? updatedShippingOptions
: [updatedShippingOptions]
updated.push(...toPush)
}
return [...created, ...updated]
}
async upsertShippingOptionTypes(
data: FulfillmentTypes.UpsertShippingOptionTypeDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionTypeDTO[]>
async upsertShippingOptionTypes(
data: FulfillmentTypes.UpsertShippingOptionTypeDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionTypeDTO>
@InjectManager()
@EmitEvents()
async upsertShippingOptionTypes(
data:
| FulfillmentTypes.UpsertShippingOptionTypeDTO[]
| FulfillmentTypes.UpsertShippingOptionTypeDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| FulfillmentTypes.ShippingOptionTypeDTO[]
| FulfillmentTypes.ShippingOptionTypeDTO
> {
const results = await this.updateShippingOptionTypes_(data, sharedContext)
const allTypes = await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionTypeDTO[]
| FulfillmentTypes.ShippingOptionTypeDTO
>(results)
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,
data: FulfillmentTypes.UpdateShippingOptionTypeDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionTypeDTO>
// @ts-expect-error
updateShippingOptionTypes(
selector: FulfillmentTypes.FilterableShippingOptionTypeProps,
data: FulfillmentTypes.UpdateShippingOptionTypeDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionTypeDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateShippingOptionTypes(
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionTypeProps,
data: FulfillmentTypes.UpdateShippingOptionTypeDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| FulfillmentTypes.ShippingOptionTypeDTO[]
| FulfillmentTypes.ShippingOptionTypeDTO
> {
let normalizedInput: FulfillmentTypes.UpdateShippingOptionTypeDTO[] = []
if (isString(idOrSelector)) {
// Check if the type exists in the first place
await this.shippingOptionTypeService_.retrieve(
idOrSelector,
{},
sharedContext
)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const types = await this.shippingOptionTypeService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = types.map((type) => ({
id: type.id,
...data,
}))
}
const types = await this.shippingOptionTypeService_.update(
normalizedInput,
sharedContext
)
const updatedTypes = await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionTypeDTO[]
>(types)
return isString(idOrSelector) ? updatedTypes[0] : updatedTypes
}
// @ts-expect-error
updateShippingProfiles(
selector: FulfillmentTypes.FilterableShippingProfileProps,
data: FulfillmentTypes.UpdateShippingProfileDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO[]>
// @ts-expect-error
updateShippingProfiles(
id: string,
data: FulfillmentTypes.UpdateShippingProfileDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateShippingProfiles(
idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps,
data: FulfillmentTypes.UpdateShippingProfileDTO,
@MedusaContext() sharedContext: Context = {}
): 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)[] = []
if (isString(idOrSelector)) {
await this.shippingProfileService_.retrieve(
idOrSelector,
{},
sharedContext
)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const profiles = await this.shippingProfileService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = profiles.map((profile) => ({
id: profile.id,
...data,
}))
}
const profiles = await this.shippingProfileService_.update(
normalizedInput,
sharedContext
)
return profiles
}
async upsertShippingProfiles(
data: FulfillmentTypes.UpsertShippingProfileDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO[]>
async upsertShippingProfiles(
data: FulfillmentTypes.UpsertShippingProfileDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingProfileDTO>
@InjectManager()
@EmitEvents()
async upsertShippingProfiles(
data:
| FulfillmentTypes.UpsertShippingProfileDTO[]
| FulfillmentTypes.UpsertShippingProfileDTO,
@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)
const forCreate = input.filter(
(prof): prof is FulfillmentTypes.CreateShippingProfileDTO => !prof.id
)
let created: InferEntityType<typeof ShippingProfile>[] = []
let updated: InferEntityType<typeof ShippingProfile>[] = []
if (forCreate.length) {
created = await this.shippingProfileService_.create(
forCreate,
sharedContext
)
}
if (forUpdate.length) {
updated = await this.shippingProfileService_.update(
forUpdate,
sharedContext
)
}
return [...created, ...updated]
}
// @ts-expect-error
updateGeoZones(
data: FulfillmentTypes.UpdateGeoZoneDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO[]>
// @ts-expect-error
updateGeoZones(
data: FulfillmentTypes.UpdateGeoZoneDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.GeoZoneDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateGeoZones(
data:
| FulfillmentTypes.UpdateGeoZoneDTO
| FulfillmentTypes.UpdateGeoZoneDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
FulfillmentModuleService.validateGeoZones(data_)
const updatedGeoZones = await this.geoZoneService_.update(
data_,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.GeoZoneDTO[]
>(updatedGeoZones)
return Array.isArray(data) ? serialized : serialized[0]
}
// @ts-expect-error
updateShippingOptionRules(
data: FulfillmentTypes.UpdateShippingOptionRuleDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionRuleDTO[]>
// @ts-expect-error
updateShippingOptionRules(
data: FulfillmentTypes.UpdateShippingOptionRuleDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updateShippingOptionRules(
data:
| FulfillmentTypes.UpdateShippingOptionRuleDTO[]
| FulfillmentTypes.UpdateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| FulfillmentTypes.ShippingOptionRuleDTO[]
| FulfillmentTypes.ShippingOptionRuleDTO
> {
const updatedShippingOptionRules = await this.updateShippingOptionRules_(
data,
sharedContext
)
return await this.baseRepository_.serialize<
| FulfillmentTypes.ShippingOptionRuleDTO
| FulfillmentTypes.ShippingOptionRuleDTO[]
>(updatedShippingOptionRules)
}
@InjectTransactionManager()
async updateShippingOptionRules_(
data:
| FulfillmentTypes.UpdateShippingOptionRuleDTO[]
| FulfillmentTypes.UpdateShippingOptionRuleDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
| InferEntityType<typeof ShippingOptionRule>
| InferEntityType<typeof ShippingOptionRule>[]
> {
const data_ = Array.isArray(data) ? data : [data]
if (!data_.length) {
return []
}
validateAndNormalizeRules(data_ as unknown as Record<string, unknown>[])
const updatedShippingOptionRules =
await this.shippingOptionRuleService_.update(data_, sharedContext)
return Array.isArray(data)
? updatedShippingOptionRules
: updatedShippingOptionRules[0]
}
@InjectManager()
@EmitEvents()
async updateFulfillment(
id: string,
data: FulfillmentTypes.UpdateFulfillmentDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentTypes.FulfillmentDTO> {
const fulfillment = await this.updateFulfillment_(id, data, sharedContext)
return await this.baseRepository_.serialize<FulfillmentTypes.FulfillmentDTO>(
fulfillment
)
}
@InjectTransactionManager()
protected async updateFulfillment_(
id: string,
data: FulfillmentTypes.UpdateFulfillmentDTO,
@MedusaContext() sharedContext: Context
): Promise<InferEntityType<typeof Fulfillment>> {
const existingFulfillment: InferEntityType<typeof Fulfillment> =
await this.fulfillmentService_.retrieve(
id,
{
relations: ["items", "labels"],
},
sharedContext
)
const updatedLabelIds: string[] = []
/**
* @note
* Since the relation is a one to many, the deletion, update and creation of labels
* is handled b the orm. That means that we dont have to perform any manual deletions or update.
* For some reason we use to have upsert and replace handled manually but we could simplify all that just like
* we do below which will create the label, update some and delete the one that does not exists in the new data.
*
* There is a bit of logic as we need to reassign the data of those we want to keep
* and we also need to emit the events later on.
*/
if (isDefined(data.labels) && isPresent(data.labels)) {
for (let label of data.labels) {
if (!("id" in label)) {
continue
}
const existingLabel = existingFulfillment.labels.find(
({ id }) => id === label.id
)!
if (
!existingLabel ||
Object.keys(label).length === 1 ||
deepEqualObj(existingLabel, label)
) {
continue
}
updatedLabelIds.push(label.id)
const labelData = { ...label }
Object.assign(label, existingLabel, labelData)
}
}
const [fulfillment] = await this.fulfillmentService_.update(
[{ id, ...data }],
sharedContext
)
return fulfillment
}
@InjectManager()
@EmitEvents()
async cancelFulfillment(
id: string,
@MedusaContext() sharedContext: Context = {}
): Promise<FulfillmentDTO> {
const canceledAt = new Date()
let fulfillment = await this.fulfillmentService_.retrieve(
id,
{},
sharedContext
)
FulfillmentModuleService.canCancelFulfillmentOrThrow(fulfillment)
// Make this action idempotent
if (!fulfillment.canceled_at) {
try {
await this.fulfillmentProviderService_.cancelFulfillment(
fulfillment.provider_id!, // TODO: should we add a runtime check on provider_id being provided?,
fulfillment.data ?? {}
)
} catch (error) {
throw error
}
fulfillment = await this.fulfillmentService_.update(
{
id,
canceled_at: canceledAt,
},
sharedContext
)
}
const result = await this.baseRepository_.serialize<FulfillmentDTO>(
fulfillment
)
return result
}
async retrieveFulfillmentOptions(
providerId: string
): Promise<FulfillmentOption[]> {
return await this.fulfillmentProviderService_.getFulfillmentOptions(
providerId
)
}
async validateFulfillmentData(
providerId: string,
optionData: Record<string, unknown>,
data: Record<string, unknown>,
context: ValidateFulfillmentDataContext
): Promise<Record<string, unknown>> {
return await this.fulfillmentProviderService_.validateFulfillmentData(
providerId,
optionData,
data,
context
)
}
// TODO: seems not to be used, what is the purpose of this method?
async validateFulfillmentOption(
providerId: string,
data: Record<string, unknown>
): Promise<boolean> {
return await this.fulfillmentProviderService_.validateOption(
providerId,
data
)
}
@InjectManager()
async validateShippingOption(
shippingOptionId: string,
context: Record<string, unknown> = {},
@MedusaContext() sharedContext: Context = {}
) {
const shippingOptions = await this.listShippingOptionsForContext(
{ id: shippingOptionId, context },
{
relations: ["rules"],
},
sharedContext
)
return !!shippingOptions.length
}
@InjectManager()
async validateShippingOptionsForPriceCalculation(
shippingOptionsData: FulfillmentTypes.CreateShippingOptionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<boolean[]> {
const nonCalculatedOptions = shippingOptionsData.filter(
(option) => option.price_type !== "calculated"
)
if (nonCalculatedOptions.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot calculate price for non-calculated shipping options: ${nonCalculatedOptions
.map((o) => o.name)
.join(", ")}`
)
}
const promises = shippingOptionsData.map((option) =>
this.fulfillmentProviderService_.canCalculate(option.provider_id, option)
)
return await promiseAll(promises)
}
async calculateShippingOptionsPrices(
shippingOptionsData: FulfillmentTypes.CalculateShippingOptionPriceDTO[]
): Promise<CalculatedShippingOptionPrice[]> {
const promises = shippingOptionsData.map((data) =>
this.fulfillmentProviderService_.calculatePrice(
data.provider_id,
data.optionData,
data.data,
data.context
)
)
return await promiseAll(promises)
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async deleteShippingProfiles(
ids: string | string[],
@MedusaContext() sharedContext: Context = {}
) {
const shippingProfileIds = Array.isArray(ids) ? ids : [ids]
await this.validateShippingProfileDeletion(
shippingProfileIds,
sharedContext
)
return await super.deleteShippingProfiles(shippingProfileIds, sharedContext)
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async softDeleteShippingProfiles<
TReturnableLinkableKeys extends string = string
>(
ids: string[],
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
@MedusaContext() sharedContext: Context = {}
): Promise<Record<string, string[]> | void> {
await this.validateShippingProfileDeletion(ids, sharedContext)
return await super.softDeleteShippingProfiles(ids, config, sharedContext)
}
protected async validateShippingProfileDeletion(
ids: string[],
sharedContext: Context
) {
const shippingProfileIds = Array.isArray(ids) ? ids : [ids]
const shippingProfiles = await this.shippingProfileService_.list(
{ id: shippingProfileIds },
{
relations: ["shipping_options.id"],
},
sharedContext
)
const undeletableShippingProfiles = shippingProfiles.filter(
(profile) => profile.shipping_options.length > 0
)
if (undeletableShippingProfiles.length) {
const undeletableShippingProfileIds = undeletableShippingProfiles.map(
(profile) => profile.id
)
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot delete Shipping Profiles ${undeletableShippingProfileIds} with associated Shipping Options. Delete Shipping Options first and try again.`
)
}
}
protected static canCancelFulfillmentOrThrow(
fulfillment: InferEntityType<typeof Fulfillment>
) {
if (fulfillment.shipped_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Fulfillment with id ${fulfillment.id} already shipped`
)
}
if (fulfillment.delivered_at) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Fulfillment with id ${fulfillment.id} already delivered`
)
}
return true
}
protected static validateMissingShippingOptions_(
shippingOptions: InferEntityType<typeof ShippingOption>[],
shippingOptionsData: UpdateShippingOptionsInput[]
) {
const missingShippingOptionIds = arrayDifference(
shippingOptionsData.map((s) => s.id),
shippingOptions.map((s) => s.id)
)
if (missingShippingOptionIds.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following shipping options do not exist: ${Array.from(
missingShippingOptionIds
).join(", ")}`
)
}
}
protected static validateMissingShippingOptionRules(
shippingOption: InferEntityType<typeof ShippingOption>,
shippingOptionUpdateData: FulfillmentTypes.UpdateShippingOptionDTO
) {
if (!shippingOptionUpdateData.rules) {
return
}
const existingRules = shippingOption.rules
const rulesSet = new Set(existingRules.map((r) => r.id))
// Only validate the rules that have an id to validate that they really exists in the shipping option
const expectedRuleSet = new Set(
shippingOptionUpdateData.rules
.map((r) => "id" in r && r.id)
.filter((id): id is string => !!id)
)
const nonAlreadyExistingRules = getSetDifference(expectedRuleSet, rulesSet)
if (nonAlreadyExistingRules.size) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`The following rules does not exists: ${Array.from(
nonAlreadyExistingRules
).join(", ")} on shipping option ${shippingOptionUpdateData.id}`
)
}
}
protected static validateGeoZones(
geoZones: (
| (Partial<FulfillmentTypes.CreateGeoZoneDTO> & { type: string })
| (Partial<FulfillmentTypes.UpdateGeoZoneDTO> & { type: string })
)[]
) {
const requirePropForType = {
country: ["country_code"],
province: ["country_code", "province_code"],
city: ["country_code", "province_code", "city"],
zip: ["country_code", "province_code", "city", "postal_expression"],
}
for (const geoZone of geoZones) {
if (!requirePropForType[geoZone.type]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Invalid geo zone type: ${geoZone.type}`
)
}
for (const prop of requirePropForType[geoZone.type]) {
if (!geoZone[prop]) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Missing required property ${prop} for geo zone type ${geoZone.type}`
)
}
}
}
}
protected static normalizeListShippingOptionsForContextParams(
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
config: FindConfig<ShippingOptionDTO> = {}
) {
let {
fulfillment_set_id,
fulfillment_set_type,
address,
context,
...where
} = filters
const normalizedConfig = { ...config }
normalizedConfig.relations = [
"rules",
"type",
"shipping_profile",
"provider",
...(normalizedConfig.relations ?? []),
]
normalizedConfig.take =
normalizedConfig.take ?? (context ? null : undefined)
let normalizedFilters = { ...where }
if (fulfillment_set_id || fulfillment_set_type) {
const fulfillmentSetConstraints = {}
if (fulfillment_set_id) {
fulfillmentSetConstraints["id"] = fulfillment_set_id
}
if (fulfillment_set_type) {
fulfillmentSetConstraints["type"] = fulfillment_set_type
}
normalizedFilters = {
...normalizedFilters,
service_zone: {
...(normalizedFilters.service_zone ?? {}),
fulfillment_set: {
...(normalizedFilters.service_zone?.fulfillment_set ?? {}),
...fulfillmentSetConstraints,
},
},
}
normalizedConfig.relations.push("service_zone.fulfillment_set")
}
if (address) {
const geoZoneConstraints =
FulfillmentModuleService.buildGeoZoneConstraintsFromAddress(address)
if (geoZoneConstraints.length) {
normalizedFilters = {
...normalizedFilters,
service_zone: {
...(normalizedFilters.service_zone ?? {}),
geo_zones: {
$or: geoZoneConstraints.map((geoZoneConstraint) => ({
// Apply eventually provided constraints on the geo zone along side the address constraints
...(normalizedFilters.service_zone?.geo_zones ?? {}),
...geoZoneConstraint,
})),
},
},
}
normalizedConfig.relations.push("service_zone.geo_zones")
}
}
normalizedConfig.relations = Array.from(new Set(normalizedConfig.relations))
return {
filters: normalizedFilters,
config: normalizedConfig,
context,
}
}
/**
* Build the constraints for the geo zones based on the address properties
* available and the hierarchy of required properties.
* We build a OR constraint from the narrowest to the broadest
* e.g. if we have a postal expression we build a constraint for the postal expression require props of type zip
* and a constraint for the city required props of type city
* and a constraint for the province code required props of type province
* and a constraint for the country code required props of type country
* example:
* {
* $or: [
* {
* type: "zip",
* country_code: "SE",
* province_code: "AB",
* city: "Stockholm",
* postal_expression: "12345"
* },
* {
* type: "city",
* country_code: "SE",
* province_code: "AB",
* city: "Stockholm"
* },
* {
* type: "province",
* country_code: "SE",
* province_code: "AB"
* },
* {
* type: "country",
* country_code: "SE"
* }
* ]
* }
*/
protected static buildGeoZoneConstraintsFromAddress(
address: FulfillmentTypes.FilterableShippingOptionForContextProps["address"]
) {
/**
* Define the hierarchy of required properties for the geo zones.
*/
const geoZoneRequirePropertyHierarchy = {
postal_expression: {
props: ["country_code", "province_code", "city", "postal_expression"],
type: "zip",
},
city: {
props: ["country_code", "province_code", "city"],
type: "city",
},
province_code: {
props: ["country_code", "province_code"],
type: "province",
},
country_code: {
props: ["country_code"],
type: "country",
},
}
/**
* The following changes assume that the lowest level check (e.g postal expression) can't exist multiple times in the higher level (e.g country)
* In case we encounter situations where it is possible to have multiple postal expressions for the same country we need to change the logic back
* to this pr https://github.com/medusajs/medusa/pull/8066
*/
const geoZoneConstraints = Object.entries(geoZoneRequirePropertyHierarchy)
.map(([prop, { props, type }]) => {
if (address![prop]) {
return {
type,
...props.reduce((geoZoneConstraint, prop) => {
if (isPresent(address![prop])) {
geoZoneConstraint[prop] = address![prop]
}
return geoZoneConstraint
}, {} as Record<string, string | undefined>),
}
}
return null
})
.filter((v) => !!v)
return geoZoneConstraints
}
}