UNPKG

@medusajs/core-flows

Version:

Set of workflow definitions for Medusa

371 lines • 15.7 kB
"use strict"; 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