@medusajs/fulfillment
Version:
Medusa Fulfillment module
1,006 lines (1,005 loc) • 67.5 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("@medusajs/framework/types");
const utils_1 = require("@medusajs/framework/utils");
const _models_1 = require("../models");
const _utils_1 = require("../utils");
const joiner_config_1 = require("../joiner-config");
const events_1 = require("../utils/events");
const generateMethodForModels = {
FulfillmentSet: _models_1.FulfillmentSet,
ServiceZone: _models_1.ServiceZone,
ShippingOption: _models_1.ShippingOption,
GeoZone: _models_1.GeoZone,
ShippingProfile: _models_1.ShippingProfile,
ShippingOptionRule: _models_1.ShippingOptionRule,
ShippingOptionType: _models_1.ShippingOptionType,
FulfillmentProvider: _models_1.FulfillmentProvider,
// Not adding Fulfillment to not auto generate the methods under the hood and only provide the methods we want to expose8
};
class FulfillmentModuleService extends utils_1.ModulesSdkUtils.MedusaService(generateMethodForModels) {
constructor({ baseRepository, fulfillmentSetService, serviceZoneService, geoZoneService, shippingProfileService, shippingOptionService, shippingOptionRuleService, shippingOptionTypeService, fulfillmentProviderService, fulfillmentService, fulfillmentAddressService, }, moduleDeclaration) {
// @ts-ignore
super(...arguments);
this.moduleDeclaration = moduleDeclaration;
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() {
return joiner_config_1.joinerConfig;
}
// @ts-ignore
async listShippingOptions(filters = {}, config = {}, sharedContext = {}) {
// 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);
}
async listShippingOptionsForContext(filters, config = {}, sharedContext = {}) {
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 (0, _utils_1.isContextValid)(context, shippingOption.rules.map((r) => r));
});
}
return await this.baseRepository_.serialize(shippingOptions);
}
async retrieveFulfillment(id, config = {}, sharedContext = {}) {
const fulfillment = await this.fulfillmentService_.retrieve(id, config, sharedContext);
return await this.baseRepository_.serialize(fulfillment);
}
async listFulfillments(filters = {}, config = {}, sharedContext = {}) {
const fulfillments = await this.fulfillmentService_.list(filters, config, sharedContext);
return await this.baseRepository_.serialize(fulfillments);
}
async listAndCountFulfillments(filters, config, sharedContext = {}) {
const [fulfillments, count] = await this.fulfillmentService_.listAndCount(filters, config, sharedContext);
return [
await this.baseRepository_.serialize(fulfillments),
count,
];
}
// @ts-expect-error
async createFulfillmentSets(data, sharedContext = {}) {
const createdFulfillmentSets = await this.createFulfillmentSets_(data, sharedContext);
const returnedFulfillmentSets = Array.isArray(data)
? createdFulfillmentSets
: createdFulfillmentSets[0];
return await this.baseRepository_.serialize(returnedFulfillmentSets);
}
async createFulfillmentSets_(data, sharedContext = {}) {
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);
(0, _utils_1.buildCreatedFulfillmentSetEvents)({
fulfillmentSets: createdFulfillmentSets,
sharedContext,
});
return createdFulfillmentSets;
}
// @ts-expect-error
async createServiceZones(data, sharedContext = {}) {
const createdServiceZones = await this.createServiceZones_(data, sharedContext);
return await this.baseRepository_.serialize(Array.isArray(data) ? createdServiceZones : createdServiceZones[0]);
}
async createServiceZones_(data, sharedContext = {}) {
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);
(0, _utils_1.buildCreatedServiceZoneEvents)({
serviceZones: createdServiceZones,
sharedContext,
});
return createdServiceZones;
}
// @ts-expect-error
async createShippingOptions(data, sharedContext = {}) {
const createdShippingOptions = await this.createShippingOptions_(data, sharedContext);
return await this.baseRepository_.serialize(Array.isArray(data) ? createdShippingOptions : createdShippingOptions[0]);
}
async createShippingOptions_(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
if (!data_.length) {
return [];
}
const rules = data_.flatMap((d) => d.rules).filter(Boolean);
if (rules.length) {
(0, _utils_1.validateAndNormalizeRules)(rules);
}
const createdSO = await this.shippingOptionService_.create(data_, sharedContext);
(0, events_1.buildCreatedShippingOptionEvents)({
shippingOptions: createdSO,
sharedContext,
});
return createdSO;
}
// @ts-expect-error
async createShippingProfiles(data, sharedContext = {}) {
const createdShippingProfiles = await this.createShippingProfiles_(data, sharedContext);
_utils_1.eventBuilders.createdShippingProfile({
data: createdShippingProfiles,
sharedContext,
});
return await this.baseRepository_.serialize(Array.isArray(data) ? createdShippingProfiles : createdShippingProfiles[0]);
}
async createShippingProfiles_(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
if (!data_.length) {
return [];
}
return await this.shippingProfileService_.create(data_, sharedContext);
}
// @ts-expect-error
async createGeoZones(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
FulfillmentModuleService.validateGeoZones(data_);
const createdGeoZones = await this.geoZoneService_.create(data_, sharedContext);
_utils_1.eventBuilders.createdGeoZone({
data: createdGeoZones,
sharedContext,
});
return await this.baseRepository_.serialize(Array.isArray(data) ? createdGeoZones : createdGeoZones[0]);
}
// @ts-expect-error
async createShippingOptionRules(data, sharedContext = {}) {
const createdShippingOptionRules = await this.createShippingOptionRules_(data, sharedContext);
return await this.baseRepository_.serialize(Array.isArray(data)
? createdShippingOptionRules
: createdShippingOptionRules[0]);
}
async createShippingOptionRules_(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
if (!data_.length) {
return [];
}
(0, _utils_1.validateAndNormalizeRules)(data_);
const createdSORules = await this.shippingOptionRuleService_.create(data_, sharedContext);
_utils_1.eventBuilders.createdShippingOptionRule({
data: createdSORules.map((sor) => ({ id: sor.id })),
sharedContext,
});
return createdSORules;
}
async createFulfillment(data, sharedContext = {}) {
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);
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;
}
(0, _utils_1.buildCreatedFulfillmentEvents)({
fulfillments: [fulfillment],
sharedContext,
});
return await this.baseRepository_.serialize(fulfillment);
}
async deleteFulfillment(id, sharedContext = {}) {
const fulfillment = await this.fulfillmentService_.retrieve(id, {}, sharedContext);
if (!(0, utils_1.isPresent)(fulfillment.canceled_at)) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Fulfillment with id ${fulfillment.id} needs to be canceled first before deleting`);
}
await this.fulfillmentService_.delete(id, sharedContext);
}
async createReturnFulfillment(data, sharedContext = {}) {
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,
});
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;
}
(0, _utils_1.buildCreatedFulfillmentEvents)({
fulfillments: [fulfillment],
sharedContext,
});
return await this.baseRepository_.serialize(fulfillment);
}
// @ts-expect-error
async updateFulfillmentSets(data, sharedContext = {}) {
const updatedFulfillmentSets = await this.updateFulfillmentSets_(data, sharedContext);
return await this.baseRepository_.serialize(updatedFulfillmentSets);
}
async updateFulfillmentSets_(data, sharedContext = {}) {
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 = (0, utils_1.getSetDifference)(expectedFulfillmentSetSet, fulfillmentSetSet);
if (missingFulfillmentSetIds.size) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `The following fulfillment sets does not exists: ${Array.from(missingFulfillmentSetIds).join(", ")}`);
}
const fulfillmentSetMap = new Map(fulfillmentSets.map((f) => [f.id, f]));
const serviceZoneIdsToDelete = [];
const geoZoneIdsToDelete = [];
const existingServiceZoneIds = [];
const existingGeoZoneIds = [];
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 = (0, utils_1.getSetDifference)(new Set(existingServiceZones.map((s) => s.id)), new Set(updatedServiceZones
.map((s) => "id" in s && s.id)
.filter((id) => !!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));
const expectedServiceZoneSet = new Set(fulfillmentSet.service_zones
.map((s) => "id" in s && s.id)
.filter((id) => !!id));
const missingServiceZoneIds = (0, utils_1.getSetDifference)(expectedServiceZoneSet, serviceZonesSet);
if (missingServiceZoneIds.size) {
throw new utils_1.MedusaError(utils_1.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) {
_utils_1.eventBuilders.deletedServiceZone({
data: serviceZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
});
_utils_1.eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
});
await (0, utils_1.promiseAll)([
this.geoZoneService_.delete({
id: geoZoneIdsToDelete,
}, sharedContext),
this.serviceZoneService_.delete({
id: serviceZoneIdsToDelete,
}, sharedContext),
]);
}
const updatedFulfillmentSets = await this.fulfillmentSetService_.update(data_, sharedContext);
_utils_1.eventBuilders.updatedFulfillmentSet({
data: updatedFulfillmentSets,
sharedContext,
});
const createdServiceZoneIds = [];
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));
_utils_1.eventBuilders.createdServiceZone({
data: createdServiceZoneIds.map((id) => ({ id })),
sharedContext,
});
_utils_1.eventBuilders.createdGeoZone({
data: createdGeoZoneIds.map((id) => ({ id })),
sharedContext,
});
return Array.isArray(data)
? updatedFulfillmentSets
: updatedFulfillmentSets[0];
}
// @ts-expect-error
async updateServiceZones(idOrSelector, data, sharedContext = {}) {
const normalizedInput = [];
if ((0, utils_1.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 = (0, utils_1.isString)(idOrSelector)
? updatedServiceZones[0]
: updatedServiceZones;
return await this.baseRepository_.serialize(toReturn);
}
async updateServiceZones_(data, sharedContext = {}) {
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 = (0, utils_1.getSetDifference)(expectedServiceZoneSet, serviceZoneSet);
if (missingServiceZoneIds.size) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `The following service zones does not exists: ${Array.from(missingServiceZoneIds).join(", ")}`);
}
const serviceZoneMap = new Map(serviceZones.map((s) => [s.id, s]));
const geoZoneIdsToDelete = [];
const existingGeoZoneIds = [];
const updatedGeoZoneIds = [];
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 = (0, utils_1.getSetDifference)(new Set(existingGeoZoneIdsForServiceZone), new Set(updatedGeoZones
.map((g) => "id" in g && g.id)
.filter((id) => !!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));
const expectedGeoZoneSet = new Set(serviceZone.geo_zones
.map((g) => "id" in g && g.id)
.filter((id) => !!id));
const missingGeoZoneIds = (0, utils_1.getSetDifference)(expectedGeoZoneSet, geoZonesSet);
if (missingGeoZoneIds.size) {
throw new utils_1.MedusaError(utils_1.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 &&
!(0, utils_1.deepEqualObj)(existing, geoZone)) {
updatedGeoZoneIds.push(geoZone.id);
}
return { ...existing, ...geoZone };
});
}
});
if (geoZoneIdsToDelete.length) {
_utils_1.eventBuilders.deletedGeoZone({
data: geoZoneIdsToDelete.map((id) => ({ id })),
sharedContext,
});
await this.geoZoneService_.delete({
id: geoZoneIdsToDelete,
}, sharedContext);
}
const updatedServiceZones = await this.serviceZoneService_.update(data_, sharedContext);
_utils_1.eventBuilders.updatedServiceZone({
data: updatedServiceZones,
sharedContext,
});
const createdGeoZoneIds = updatedServiceZones
.flatMap((serviceZone) => {
return serviceZone.geo_zones.map((g) => g.id);
})
.filter((id) => !existingGeoZoneIds.includes(id));
_utils_1.eventBuilders.createdGeoZone({
data: createdGeoZoneIds.map((id) => ({ id })),
sharedContext,
});
_utils_1.eventBuilders.updatedGeoZone({
data: updatedGeoZoneIds.map((id) => ({ id })),
sharedContext,
});
return Array.isArray(data) ? updatedServiceZones : updatedServiceZones[0];
}
async upsertServiceZones(data, sharedContext = {}) {
const upsertServiceZones = await this.upsertServiceZones_(data, sharedContext);
const allServiceZones = await this.baseRepository_.serialize(upsertServiceZones);
return Array.isArray(data) ? allServiceZones : allServiceZones[0];
}
async upsertServiceZones_(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const forUpdate = input.filter((serviceZone) => !!serviceZone.id);
const forCreate = input.filter((serviceZone) => !serviceZone.id);
const created = [];
const updated = [];
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-expect-error
async updateShippingOptions(idOrSelector, data, sharedContext = {}) {
const normalizedInput = [];
if ((0, utils_1.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(updatedShippingOptions);
return (0, utils_1.isString)(idOrSelector) ? serialized[0] : serialized;
}
async updateShippingOptions_(data, sharedContext = {}) {
const dataArray = Array.isArray(data)
? data.map((d) => (0, utils_1.deepCopy)(d))
: [(0, utils_1.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 = [];
const updatedRuleIds = [];
const existingRuleIds = [];
const optionTypeDeletedIds = [];
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 = new Map(existingRules.map((rule) => [rule.id, rule]));
const updatedRules = shippingOption.rules
.map((rule) => {
if ("id" in rule) {
const existingRule = (existingRulesMap.get(rule.id) ??
{});
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 = {
...existingRule,
...rule,
};
existingRulesMap.set(rule.id, ruleData);
return ruleData;
}
return;
})
.filter(Boolean);
(0, _utils_1.validateAndNormalizeRules)(updatedRules);
const toDeleteRuleIds = (0, utils_1.arrayDifference)(updatedRuleIds, Array.from(existingRulesMap.keys()));
if (toDeleteRuleIds.length) {
ruleIdsToDelete.push(...toDeleteRuleIds);
}
shippingOption.rules = shippingOption.rules.map((rule) => {
if (!("id" in rule)) {
(0, _utils_1.validateAndNormalizeRules)([rule]);
return rule;
}
return existingRulesMap.get(rule.id);
});
});
if (ruleIdsToDelete.length) {
_utils_1.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];
}
handleShippingOptionUpdateEvents({ shippingOptionsData, updatedShippingOptions, optionTypeDeletedIds, updatedRuleIds, existingRuleIds, sharedContext, }) {
_utils_1.eventBuilders.updatedShippingOption({
data: updatedShippingOptions,
sharedContext,
});
_utils_1.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);
_utils_1.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);
_utils_1.eventBuilders.createdShippingOptionRule({
data: createdRuleIds.map((id) => ({ id })),
sharedContext,
});
_utils_1.eventBuilders.updatedShippingOptionRule({
data: updatedRuleIds.map((id) => ({ id })),
sharedContext,
});
}
async upsertShippingOptions(data, sharedContext = {}) {
const upsertedShippingOptions = await this.upsertShippingOptions_(data, sharedContext);
const allShippingOptions = await this.baseRepository_.serialize(upsertedShippingOptions);
return Array.isArray(data) ? allShippingOptions : allShippingOptions[0];
}
async upsertShippingOptions_(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const forUpdate = input.filter((shippingOption) => !!shippingOption.id);
const forCreate = input.filter((shippingOption) => !shippingOption.id);
let created = [];
let updated = [];
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
async updateShippingProfiles(idOrSelector, data, sharedContext = {}) {
let normalizedInput = [];
if ((0, utils_1.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(profiles);
return (0, utils_1.isString)(idOrSelector) ? updatedProfiles[0] : updatedProfiles;
}
async upsertShippingProfiles(data, sharedContext = {}) {
const input = Array.isArray(data) ? data : [data];
const forUpdate = input.filter((prof) => !!prof.id);
const forCreate = input.filter((prof) => !prof.id);
let created = [];
let updated = [];
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(result);
return Array.isArray(data) ? allProfiles : allProfiles[0];
}
// @ts-expect-error
async updateGeoZones(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
if (!data_.length) {
return [];
}
FulfillmentModuleService.validateGeoZones(data_);
const updatedGeoZones = await this.geoZoneService_.update(data_, sharedContext);
_utils_1.eventBuilders.updatedGeoZone({
data: updatedGeoZones,
sharedContext,
});
const serialized = await this.baseRepository_.serialize(updatedGeoZones);
return Array.isArray(data) ? serialized : serialized[0];
}
// @ts-expect-error
async updateShippingOptionRules(data, sharedContext = {}) {
const updatedShippingOptionRules = await this.updateShippingOptionRules_(data, sharedContext);
return await this.baseRepository_.serialize(updatedShippingOptionRules);
}
async updateShippingOptionRules_(data, sharedContext = {}) {
const data_ = Array.isArray(data) ? data : [data];
if (!data_.length) {
return [];
}
(0, _utils_1.validateAndNormalizeRules)(data_);
const updatedShippingOptionRules = await this.shippingOptionRuleService_.update(data_, sharedContext);
_utils_1.eventBuilders.updatedShippingOptionRule({
data: updatedShippingOptionRules.map((rule) => ({ id: rule.id })),
sharedContext,
});
return Array.isArray(data)
? updatedShippingOptionRules
: updatedShippingOptionRules[0];
}
async updateFulfillment(id, data, sharedContext = {}) {
const fulfillment = await this.updateFulfillment_(id, data, sharedContext);
return await this.baseRepository_.serialize(fulfillment);
}
async updateFulfillment_(id, data, sharedContext) {
const existingFulfillment = await this.fulfillmentService_.retrieve(id, {
relations: ["items", "labels"],
}, sharedContext);
const updatedLabelIds = [];
let deletedLabelIds = [];
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 ((0, utils_1.isDefined)(data.labels) && (0, utils_1.isPresent)(data.labels)) {
const dataLabelIds = data.labels
.filter((label) => "id" in label)
.map((label) => label.id);
deletedLabelIds = (0, utils_1.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 ||
(0, utils_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;
}
handleFulfillmentUpdateEvents(fulfillment, existingLabelIds, updatedLabelIds, deletedLabelIds, sharedContext) {
_utils_1.eventBuilders.updatedFulfillment({
data: [{ id: fulfillment.id }],
sharedContext,
});
_utils_1.eventBuilders.deletedFulfillmentLabel({
data: deletedLabelIds.map((id) => ({ id })),
sharedContext,
});
_utils_1.eventBuilders.updatedFulfillmentLabel({
data: updatedLabelIds.map((id) => ({ id })),
sharedContext,
});
const createdLabels = fulfillment.labels.filter((label) => {
return !existingLabelIds.includes(label.id);
});
_utils_1.eventBuilders.createdFulfillmentLabel({
data: createdLabels.map((label) => ({ id: label.id })),
sharedContext,
});
}
async cancelFulfillment(id, sharedContext = {}) {
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);
_utils_1.eventBuilders.updatedFulfillment({
data: [{ id }],
sharedContext,
});
}
const result = await this.baseRepository_.serialize(fulfillment);
return result;
}
async retrieveFulfillmentOptions(providerId) {
return await this.fulfillmentProviderService_.getFulfillmentOptions(providerId);
}
async validateFulfillmentData(providerId, optionData, data, context) {
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, data) {
return await this.fulfillmentProviderService_.validateOption(providerId, data);
}
async validateShippingOption(shippingOptionId, context = {}, sharedContext = {}) {
const shippingOptions = await this.listShippingOptionsForContext({ id: shippingOptionId, context }, {
relations: ["rules"],
}, sharedContext);
return !!shippingOptions.length;
}
async validateShippingOptionsForPriceCalculation(shippingOptionsData, sharedContext = {}) {
const nonCalculatedOptions = shippingOptionsData.filter((option) => option.price_type !== "calculated");
if (nonCalculatedOptions.length) {
throw new utils_1.MedusaError(utils_1.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 (0, utils_1.promiseAll)(promises);
}
async calculateShippingOptionsPrices(shippingOptionsData) {
const promises = shippingOptionsData.map((data) => this.fulfillmentProviderService_.calculatePrice(data.provider_id, data.optionData, data.data, data.context));
return await (0, utils_1.promiseAll)(promises);
}
// @ts-expect-error
async deleteShippingProfiles(ids, sharedContext = {}) {
const shippingProfileIds = Array.isArray(ids) ? ids : [ids];
await this.validateShippingProfileDeletion(shippingProfileIds, sharedContext);
return await super.deleteShippingProfiles(shippingProfileIds, sharedContext);
}
// @ts-expect-error
async softDeleteShippingProfiles(ids, config, sharedContext = {}) {
await this.validateShippingProfileDeletion(ids, sharedContext);
return await super.softDeleteShippingProfiles(ids, config, sharedContext);
}
async validateShippingProfileDeletion(ids, sharedContext) {
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 utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot delete Shipping Profiles ${undeletableShippingProfileIds} with associated Shipping Options. Delete Shipping Options first and try again.`);
}
}
static canCancelFulfillmentOrThrow(fulfillment) {
if (fulfillment.shipped_at) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Fulfillment with id ${fulfillment.id} already shipped`);
}
if (fulfillment.delivered_at) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Fulfillment with id ${fulfillment.id} already delivered`);
}
return true;
}
static validateMissingShippingOptions_(shippingOptions, shippingOptionsData) {
const missingShippingOptionIds = (0, utils_1.arrayDifference)(shippingOptionsData.map((s) => s.id), shippingOptions.map((s) => s.id));
if (missingShippingOptionIds.length) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `The following shipping options do not exist: ${Array.from(missingShippingOptionIds).join(", ")}`);
}
}
static validateMissingShippingOptionRules(shippingOption, shippingOptionUpdateData) {
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));
const nonAlreadyExistingRules = (0, utils_1.getSetDifference)(expectedRuleSet, rulesSet);
if (nonAlreadyExistingRules.size) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `The following rules does not exists: ${Array.from(nonAlreadyExistingRules).join(", ")} on shipping option ${shippingOptionUpdateData.id}`);
}
}
static validateGeoZones(geoZones) {
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 utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Invalid geo zone type: ${geoZone.type}`);
}
for (const prop of requirePropForType[geoZone.type]) {
if (!geoZone[prop]) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Missing required property ${prop} for geo zone type ${geoZone.type}`);
}
}
}
}
static normalizeListShippingOptionsForContextParams(filters, config = {}) {
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: {