@medusajs/core-flows
Version:
Set of workflow definitions for Medusa
371 lines • 15.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createOrderFulfillmentWorkflow = exports.createOrderFulfillmentWorkflowId = exports.createFulfillmentValidateOrder = void 0;
const utils_1 = require("@medusajs/framework/utils");
const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
const common_1 = require("../../common");
const fulfillment_1 = require("../../fulfillment");
const inventory_1 = require("../../inventory");
const reservation_1 = require("../../reservation");
const steps_1 = require("../steps");
const build_reservations_map_1 = require("../utils/build-reservations-map");
const order_validation_1 = require("../utils/order-validation");
/**
* This step validates that a fulfillment can be created for an order. If the order
* is canceled, the items don't exist in the order, or the items aren't grouped by
* shipping requirement, the step throws an error.
*
* :::note
*
* You can retrieve an order's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
*
* :::
*
* @example
* const data = createFulfillmentValidateOrder({
* order: {
* id: "order_123",
* // other order details...
* },
* inputItems: [
* {
* id: "orli_123",
* quantity: 1,
* }
* ]
* })
*/
exports.createFulfillmentValidateOrder = (0, workflows_sdk_1.createStep)("create-fulfillment-validate-order", ({ order, inputItems }) => {
if (!inputItems.length) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "No items to fulfill");
}
(0, order_validation_1.throwIfOrderIsCancelled)({ order });
(0, order_validation_1.throwIfItemsDoesNotExistsInOrder)({ order, inputItems });
(0, order_validation_1.throwIfItemsAreNotGroupedByShippingRequirement)({ order, inputItems });
});
function prepareRegisterOrderFulfillmentData({ order, fulfillment, input, inputItemsMap, itemsList, }) {
return {
order_id: order.id,
reference: utils_1.Modules.FULFILLMENT,
reference_id: fulfillment.id,
created_by: input.created_by,
items: (itemsList ?? order.items).map((i) => {
const inputQuantity = inputItemsMap[i.id]?.quantity;
return {
id: i.id,
quantity: inputQuantity ?? i.quantity,
};
}),
};
}
function prepareFulfillmentData({ order, input, shippingOption, shippingMethod, reservations, itemsList, }) {
const fulfillableItems = input.items;
const orderItemsMap = new Map((itemsList ?? order.items).map((i) => [i.id, i]));
const reservationItemMap = (0, build_reservations_map_1.buildReservationsMap)(reservations);
// Note: If any of the items require shipping, we enable fulfillment
// unless explicitly set to not require shipping by the item in the request
const someItemsRequireShipping = fulfillableItems.length
? fulfillableItems.some((item) => {
const orderItem = orderItemsMap.get(item.id);
return orderItem.requires_shipping;
})
: true;
const fulfillmentItems = fulfillableItems
.map((i) => {
const orderItem = orderItemsMap.get(i.id);
const reservations = reservationItemMap.get(i.id);
if (orderItem.requires_shipping &&
orderItem.variant?.product &&
orderItem.variant?.product.shipping_profile?.id !==
shippingOption.shipping_profile_id) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Shipping profile ${shippingOption.shipping_profile_id} does not match the shipping profile of the order item ${orderItem.id}`);
}
if (!reservations?.length) {
return [
{
line_item_id: i.id,
inventory_item_id: undefined,
quantity: i.quantity,
title: orderItem.variant_title ?? orderItem.title,
sku: orderItem.variant_sku || "",
barcode: orderItem.variant_barcode || "",
},
];
}
// if line item is from a managed variant, create a fulfillment item for each reservation item
return reservations.map((r) => {
const iItem = orderItem?.variant?.inventory_items.find((ii) => ii.inventory.id === r.inventory_item_id);
return {
line_item_id: i.id,
inventory_item_id: r.inventory_item_id,
quantity: utils_1.MathBN.mult(iItem?.required_quantity ?? 1, i.quantity),
title: iItem?.inventory.title ||
orderItem.variant_title ||
orderItem.title,
sku: iItem?.inventory.sku || orderItem.variant_sku || "",
barcode: orderItem.variant_barcode || "",
};
});
})
.flat();
let locationId = input.location_id;
if (!locationId) {
locationId = shippingOption.service_zone.fulfillment_set.location?.id;
}
if (!locationId) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Cannot create fulfillment without stock location, either provide a location or you should link the shipping option ${shippingOption.id} to a stock location.`);
}
const shippingAddress = order.shipping_address ?? { id: undefined };
delete shippingAddress.id;
return {
input: {
location_id: locationId,
provider_id: shippingOption.provider_id,
shipping_option_id: shippingOption.id,
order: order,
data: shippingMethod.data,
items: fulfillmentItems,
requires_shipping: someItemsRequireShipping,
labels: input.labels ?? [],
delivery_address: shippingAddress,
packed_at: new Date(),
metadata: input.metadata,
},
};
}
function prepareInventoryUpdate({ reservations, order, input, inputItemsMap, itemsList, }) {
const toDelete = [];
const toUpdate = [];
const inventoryAdjustment = [];
const orderItemsMap = new Map((itemsList ?? order.items).map((i) => [i.id, i]));
const reservationMap = (0, build_reservations_map_1.buildReservationsMap)(reservations);
const allItems = itemsList ?? order.items;
const itemsToFulfill = allItems.filter((i) => i.id in inputItemsMap);
// iterate over items that are being fulfilled
for (const item of itemsToFulfill) {
const reservations = reservationMap.get(item.id);
const orderItem = orderItemsMap.get(item.id);
if (!reservations?.length) {
if (item.variant?.manage_inventory) {
throw new Error(`No stock reservation found for item ${item.id} - ${item.title} (${item.variant_title})`);
}
continue;
}
const inputQuantity = inputItemsMap[item.id]?.quantity ?? item.quantity;
reservations.forEach((reservation) => {
if (utils_1.MathBN.gt(inputQuantity, reservation.quantity)) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Quantity to fulfill exceeds the reserved quantity for the item: ${item.id}`);
}
const iItem = orderItem?.variant?.inventory_items.find((ii) => ii.inventory.id === reservation.inventory_item_id);
const adjustemntQuantity = utils_1.MathBN.mult(inputQuantity, iItem?.required_quantity ?? 1);
const remainingReservationQuantity = utils_1.MathBN.sub(reservation.quantity, adjustemntQuantity);
inventoryAdjustment.push({
inventory_item_id: reservation.inventory_item_id,
location_id: input.location_id ?? reservation.location_id,
adjustment: utils_1.MathBN.mult(adjustemntQuantity, -1),
});
if (utils_1.MathBN.eq(remainingReservationQuantity, 0)) {
toDelete.push(reservation.id);
}
else {
toUpdate.push({
id: reservation.id,
quantity: remainingReservationQuantity,
location_id: input.location_id ?? reservation.location_id,
});
}
});
}
return {
toDelete,
toUpdate,
inventoryAdjustment,
};
}
exports.createOrderFulfillmentWorkflowId = "create-order-fulfillment";
/**
* This workflow creates a fulfillment for an order. It's used by the [Create Order Fulfillment Admin API Route](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments).
*
* This workflow has a hook that allows you to perform custom actions on the created fulfillment. For example, you can pass under `additional_data` custom data that
* allows you to create custom data models linked to the fulfillment.
*
* You can also use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around creating a fulfillment.
*
* @example
* const { result } = await createOrderFulfillmentWorkflow(container)
* .run({
* input: {
* order_id: "order_123",
* items: [
* {
* id: "orli_123",
* quantity: 1,
* }
* ],
* additional_data: {
* send_oms: true
* }
* }
* })
*
* @summary
*
* Creates a fulfillment for an order.
*
* @property hooks.fulfillmentCreated - This hook is executed after the fulfillment is created. You can consume this hook to perform custom actions on the created fulfillment.
*/
exports.createOrderFulfillmentWorkflow = (0, workflows_sdk_1.createWorkflow)(exports.createOrderFulfillmentWorkflowId, (input) => {
const order = (0, common_1.useRemoteQueryStep)({
entry_point: "orders",
fields: [
"id",
"display_id",
"status",
"customer_id",
"customer.*",
"sales_channel_id",
"sales_channel.*",
"region_id",
"region.*",
"currency_code",
"items.*",
"items.variant.manage_inventory",
"items.variant.allow_backorder",
"items.variant.product.id",
"items.variant.product.shipping_profile.id",
"items.variant.weight",
"items.variant.length",
"items.variant.height",
"items.variant.width",
"items.variant.material",
"items.variant_title",
"items.variant.upc",
"items.variant.sku",
"items.variant.barcode",
"items.variant.hs_code",
"items.variant.origin_country",
"items.variant.product.origin_country",
"items.variant.product.hs_code",
"items.variant.product.mid_code",
"items.variant.product.material",
"items.tax_lines.rate",
"subtotal",
"discount_total",
"tax_total",
"item_total",
"shipping_total",
"total",
"created_at",
"items.variant.inventory_items.required_quantity",
"items.variant.inventory_items.inventory.id",
"items.variant.inventory_items.inventory.title",
"items.variant.inventory_items.inventory.sku",
"shipping_address.*",
"shipping_methods.id",
"shipping_methods.shipping_option_id",
"shipping_methods.data",
],
variables: { id: input.order_id },
list: false,
throw_if_key_not_found: true,
});
(0, exports.createFulfillmentValidateOrder)({ order, inputItems: input.items });
const inputItemsMap = (0, workflows_sdk_1.transform)(input, ({ items }) => {
return items.reduce((acc, item) => {
acc[item.id] = item;
return acc;
}, {});
});
const shippingOptionId = (0, workflows_sdk_1.transform)({ order, input }, (data) => {
return (data.input.shipping_option_id ??
data.order.shipping_methods?.[0]?.shipping_option_id);
});
const shippingMethod = (0, workflows_sdk_1.transform)({ order, shippingOptionId }, (data) => {
return {
data: data.order.shipping_methods?.find((sm) => sm.shipping_option_id === data.shippingOptionId)?.data,
};
});
const shippingOption = (0, common_1.useRemoteQueryStep)({
entry_point: "shipping_options",
fields: [
"id",
"provider_id",
"service_zone.fulfillment_set.location.id",
"shipping_profile_id",
],
variables: {
id: shippingOptionId,
},
list: false,
}).config({ name: "get-shipping-option" });
const lineItemIds = (0, workflows_sdk_1.transform)({ order, itemsList: input.items_list, inputItemsMap }, ({ order, itemsList, inputItemsMap }) => {
return (itemsList ?? order.items)
.map((i) => i.id)
.filter((i) => i in inputItemsMap);
});
const reservations = (0, common_1.useRemoteQueryStep)({
entry_point: "reservations",
fields: [
"id",
"line_item_id",
"quantity",
"inventory_item_id",
"location_id",
],
variables: {
filter: {
line_item_id: lineItemIds,
},
},
}).config({ name: "get-reservations" });
const fulfillmentData = (0, workflows_sdk_1.transform)({
order,
input,
shippingOption,
shippingMethod,
reservations,
itemsList: input.items_list,
}, prepareFulfillmentData);
const fulfillment = fulfillment_1.createFulfillmentWorkflow.runAsStep(fulfillmentData);
const registerOrderFulfillmentData = (0, workflows_sdk_1.transform)({
order,
fulfillment,
input,
inputItemsMap,
itemsList: input.items ?? input.items_list,
}, prepareRegisterOrderFulfillmentData);
const link = (0, workflows_sdk_1.transform)({ order_id: input.order_id, fulfillment }, (data) => {
return [
{
[utils_1.Modules.ORDER]: { order_id: data.order_id },
[utils_1.Modules.FULFILLMENT]: { fulfillment_id: data.fulfillment.id },
},
];
});
const { toDelete, toUpdate, inventoryAdjustment } = (0, workflows_sdk_1.transform)({
order,
reservations,
input,
inputItemsMap,
itemsList: input.items_list,
}, prepareInventoryUpdate);
(0, inventory_1.adjustInventoryLevelsStep)(inventoryAdjustment);
(0, workflows_sdk_1.parallelize)((0, steps_1.registerOrderFulfillmentStep)(registerOrderFulfillmentData), (0, common_1.createRemoteLinkStep)(link), (0, reservation_1.updateReservationsStep)(toUpdate), (0, reservation_1.deleteReservationsStep)(toDelete), (0, common_1.emitEventStep)({
eventName: utils_1.OrderWorkflowEvents.FULFILLMENT_CREATED,
data: {
order_id: input.order_id,
fulfillment_id: fulfillment.id,
no_notification: input.no_notification,
},
}));
const fulfillmentCreated = (0, workflows_sdk_1.createHook)("fulfillmentCreated", {
fulfillment,
additional_data: input.additional_data,
});
// trigger event OrderModuleService.Events.FULFILLMENT_CREATED
return new workflows_sdk_1.WorkflowResponse(fulfillment, {
hooks: [fulfillmentCreated],
});
});
//# sourceMappingURL=create-fulfillment.js.map
;