ab634a14ba
FIXES SUP-1824 **What** The medusa internal service update should always return the data in the expected shape described by the interface. The medusa service should not have to handle the reshapre
2438 lines
70 KiB
TypeScript
2438 lines
70 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 {
|
|
Fulfillment,
|
|
FulfillmentProvider,
|
|
FulfillmentSet,
|
|
GeoZone,
|
|
ServiceZone,
|
|
ShippingOption,
|
|
ShippingOptionRule,
|
|
ShippingOptionType,
|
|
ShippingProfile,
|
|
} from "@models"
|
|
import {
|
|
buildCreatedFulfillmentEvents,
|
|
buildCreatedFulfillmentSetEvents,
|
|
buildCreatedServiceZoneEvents,
|
|
eventBuilders,
|
|
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 = {
|
|
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 expose8
|
|
}
|
|
|
|
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
|
|
)
|
|
|
|
buildCreatedFulfillmentSetEvents({
|
|
fulfillmentSets: createdFulfillmentSets,
|
|
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
|
|
)
|
|
|
|
buildCreatedServiceZoneEvents({
|
|
serviceZones: createdServiceZones,
|
|
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
|
|
)
|
|
|
|
buildCreatedShippingOptionEvents({
|
|
shippingOptions: createdSO,
|
|
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>
|
|
|
|
@InjectTransactionManager()
|
|
@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
|
|
)
|
|
|
|
eventBuilders.createdShippingProfile({
|
|
data: createdShippingProfiles,
|
|
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
|
|
)
|
|
|
|
eventBuilders.createdGeoZone({
|
|
data: createdGeoZones,
|
|
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
|
|
)
|
|
|
|
eventBuilders.createdShippingOptionRule({
|
|
data: createdSORules.map((sor) => ({ id: sor.id })),
|
|
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
|
|
}
|
|
|
|
buildCreatedFulfillmentEvents({
|
|
fulfillments: [fulfillment],
|
|
sharedContext,
|
|
})
|
|
|
|
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
|
|
}
|
|
|
|
buildCreatedFulfillmentEvents({
|
|
fulfillments: [fulfillment],
|
|
sharedContext,
|
|
})
|
|
|
|
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) {
|
|
eventBuilders.deletedServiceZone({
|
|
data: serviceZoneIdsToDelete.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
eventBuilders.deletedGeoZone({
|
|
data: geoZoneIdsToDelete.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
|
|
await promiseAll([
|
|
this.geoZoneService_.delete(
|
|
{
|
|
id: geoZoneIdsToDelete,
|
|
},
|
|
sharedContext
|
|
),
|
|
this.serviceZoneService_.delete(
|
|
{
|
|
id: serviceZoneIdsToDelete,
|
|
},
|
|
sharedContext
|
|
),
|
|
])
|
|
}
|
|
|
|
const updatedFulfillmentSets = await this.fulfillmentSetService_.update(
|
|
data_,
|
|
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]
|
|
}
|
|
|
|
// @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) {
|
|
eventBuilders.deletedGeoZone({
|
|
data: geoZoneIdsToDelete.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
|
|
await this.geoZoneService_.delete(
|
|
{
|
|
id: geoZoneIdsToDelete,
|
|
},
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
const updatedServiceZones = await this.serviceZoneService_.update(
|
|
data_,
|
|
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]
|
|
}
|
|
|
|
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
|
|
)! // Garuantueed to exist since the validation above have been performed
|
|
|
|
if (shippingOption.type && !("id" in shippingOption.type)) {
|
|
optionTypeDeletedIds.push(existingShippingOption.type.id)
|
|
}
|
|
|
|
if (!shippingOption.rules) {
|
|
return
|
|
}
|
|
|
|
const 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) {
|
|
eventBuilders.deletedShippingOptionRule({
|
|
data: ruleIdsToDelete.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
|
|
await this.shippingOptionRuleService_.delete(
|
|
ruleIdsToDelete,
|
|
sharedContext
|
|
)
|
|
}
|
|
|
|
const updatedShippingOptions = await this.shippingOptionService_.update(
|
|
dataArray,
|
|
sharedContext
|
|
)
|
|
|
|
this.handleShippingOptionUpdateEvents({
|
|
shippingOptionsData: dataArray,
|
|
updatedShippingOptions,
|
|
optionTypeDeletedIds,
|
|
updatedRuleIds,
|
|
existingRuleIds,
|
|
sharedContext,
|
|
})
|
|
|
|
return Array.isArray(data)
|
|
? updatedShippingOptions
|
|
: updatedShippingOptions[0]
|
|
}
|
|
|
|
private handleShippingOptionUpdateEvents({
|
|
shippingOptionsData,
|
|
updatedShippingOptions,
|
|
optionTypeDeletedIds,
|
|
updatedRuleIds,
|
|
existingRuleIds,
|
|
sharedContext,
|
|
}) {
|
|
eventBuilders.updatedShippingOption({
|
|
data: updatedShippingOptions,
|
|
sharedContext,
|
|
})
|
|
eventBuilders.deletedShippingOptionType({
|
|
data: optionTypeDeletedIds.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
|
|
const createdOptionTypeIds = updatedShippingOptions
|
|
.filter((so) => {
|
|
const updateData = shippingOptionsData.find((sod) => sod.id === so.id)
|
|
return updateData?.type && !("id" in updateData.type)
|
|
})
|
|
.map((so) => so.type.id)
|
|
|
|
eventBuilders.createdShippingOptionType({
|
|
data: createdOptionTypeIds.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
|
|
const createdRuleIds = updatedShippingOptions
|
|
.flatMap((so) =>
|
|
[...so.rules].map((rule) => {
|
|
if (existingRuleIds.includes(rule.id)) {
|
|
return
|
|
}
|
|
|
|
return rule.id
|
|
})
|
|
)
|
|
.filter((id): id is string => !!id)
|
|
|
|
eventBuilders.createdShippingOptionRule({
|
|
data: createdRuleIds.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
eventBuilders.updatedShippingOptionRule({
|
|
data: updatedRuleIds.map((id) => ({ id })),
|
|
sharedContext,
|
|
})
|
|
}
|
|
|
|
async upsertShippingOptions(
|
|
data: FulfillmentTypes.UpsertShippingOptionDTO[],
|
|
sharedContext?: Context
|
|
): 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]
|
|
}
|
|
|
|
// @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>
|
|
|
|
@InjectTransactionManager()
|
|
// @ts-expect-error
|
|
async updateShippingProfiles(
|
|
idOrSelector: string | FulfillmentTypes.FilterableShippingProfileProps,
|
|
data: FulfillmentTypes.UpdateShippingProfileDTO,
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<
|
|
FulfillmentTypes.ShippingProfileDTO | FulfillmentTypes.ShippingProfileDTO[]
|
|
> {
|
|
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
|
|
)
|
|
|
|
const updatedProfiles = await this.baseRepository_.serialize<
|
|
FulfillmentTypes.ShippingProfileDTO[]
|
|
>(profiles)
|
|
|
|
return isString(idOrSelector) ? updatedProfiles[0] : updatedProfiles
|
|
}
|
|
|
|
async upsertShippingProfiles(
|
|
data: FulfillmentTypes.UpsertShippingProfileDTO[],
|
|
sharedContext?: Context
|
|
): Promise<FulfillmentTypes.ShippingProfileDTO[]>
|
|
async upsertShippingProfiles(
|
|
data: FulfillmentTypes.UpsertShippingProfileDTO,
|
|
sharedContext?: Context
|
|
): Promise<FulfillmentTypes.ShippingProfileDTO>
|
|
|
|
@InjectTransactionManager()
|
|
async upsertShippingProfiles(
|
|
data:
|
|
| FulfillmentTypes.UpsertShippingProfileDTO[]
|
|
| FulfillmentTypes.UpsertShippingProfileDTO,
|
|
@MedusaContext() sharedContext: Context = {}
|
|
): Promise<
|
|
FulfillmentTypes.ShippingProfileDTO[] | FulfillmentTypes.ShippingProfileDTO
|
|
> {
|
|
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
|
|
)
|
|
}
|
|
|
|
const result = [...created, ...updated]
|
|
const allProfiles = await this.baseRepository_.serialize<
|
|
| FulfillmentTypes.ShippingProfileDTO[]
|
|
| FulfillmentTypes.ShippingProfileDTO
|
|
>(result)
|
|
|
|
return Array.isArray(data) ? allProfiles : allProfiles[0]
|
|
}
|
|
|
|
// @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
|
|
)
|
|
|
|
eventBuilders.updatedGeoZone({
|
|
data: updatedGeoZones,
|
|
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)
|
|
|
|
eventBuilders.updatedShippingOptionRule({
|
|
data: updatedShippingOptionRules.map((rule) => ({ id: rule.id })),
|
|
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[] = []
|
|
let deletedLabelIds: string[] = []
|
|
|
|
const existingLabelIds = existingFulfillment.labels.map((label) => label.id)
|
|
|
|
/**
|
|
* @note
|
|
* Since the relation is a one to many, the deletion, update and creation of labels
|
|
* is handled b the orm. That means that we dont have to perform any manual deletions or update.
|
|
* For some reason we use to have upsert and replace handled manually but we could simplify all that just like
|
|
* we do below which will create the label, update some and delete the one that does not exists in the new data.
|
|
*
|
|
* There is a bit of logic as we need to reassign the data of those we want to keep
|
|
* and we also need to emit the events later on.
|
|
*/
|
|
if (isDefined(data.labels) && isPresent(data.labels)) {
|
|
const dataLabelIds: string[] = data.labels
|
|
.filter((label): label is { id: string } => "id" in label)
|
|
.map((label) => label.id)
|
|
|
|
deletedLabelIds = arrayDifference(existingLabelIds, dataLabelIds)
|
|
|
|
for (let label of data.labels) {
|
|
if (!("id" in label)) {
|
|
continue
|
|
}
|
|
|
|
const existingLabel = existingFulfillment.labels.find(
|
|
({ id }) => id === label.id
|
|
)!
|
|
|
|
if (
|
|
!existingLabel ||
|
|
Object.keys(label).length === 1 ||
|
|
deepEqualObj(existingLabel, label)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
updatedLabelIds.push(label.id)
|
|
const labelData = { ...label }
|
|
Object.assign(label, existingLabel, labelData)
|
|
}
|
|
}
|
|
|
|
const [fulfillment] = await this.fulfillmentService_.update(
|
|
[{ id, ...data }],
|
|
sharedContext
|
|
)
|
|
|
|
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(
|
|
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
|
|
)
|
|
|
|
eventBuilders.updatedFulfillment({
|
|
data: [{ id }],
|
|
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()
|
|
// @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()
|
|
// @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: [
|
|
"country_code",
|
|
"province_code",
|
|
"city",
|
|
"postal_expression",
|
|
],
|
|
city: ["country_code", "province_code", "city"],
|
|
province_code: ["country_code", "province_code"],
|
|
country_code: ["country_code"],
|
|
}
|
|
|
|
/**
|
|
* 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, requiredProps]) => {
|
|
if (address![prop]) {
|
|
return requiredProps.reduce((geoZoneConstraint, prop) => {
|
|
if (isPresent(address![prop])) {
|
|
geoZoneConstraint[prop] = address![prop]
|
|
}
|
|
return geoZoneConstraint
|
|
}, {} as Record<string, string | undefined>)
|
|
}
|
|
return null
|
|
})
|
|
.filter((v): v is Record<string, any> => !!v)
|
|
|
|
return geoZoneConstraints
|
|
}
|
|
}
|