fix(core-flows): fulfillment reservation check (#11735)

This commit is contained in:
Frane Polić
2025-03-09 13:47:10 +01:00
committed by GitHub
parent b7678983a9
commit 70eaaa9196
9 changed files with 84 additions and 22 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
fix(core-flows): fulfillment reservation check

View File

@@ -331,6 +331,17 @@ medusaIntegrationTestRunner({
}, },
]) ])
// create reservation for inventory item that is initially on the order
const inventoryModule = container.resolve(Modules.INVENTORY)
await inventoryModule.createReservationItems([
{
inventory_item_id: inventoryItem.id,
location_id: location.id,
quantity: 2,
line_item_id: order.items[0].id,
},
])
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Return shipping", name: "Return shipping",
service_zone_id: fulfillmentSet.service_zones[0].id, service_zone_id: fulfillmentSet.service_zones[0].id,

View File

@@ -325,6 +325,17 @@ medusaIntegrationTestRunner({
}, },
]) ])
// create reservation for inventory item that is initially on the order
const inventoryModule = container.resolve(Modules.INVENTORY)
await inventoryModule.createReservationItems([
{
inventory_item_id: inventoryItem.id,
location_id: location.id,
quantity: 2,
line_item_id: order.items[0].id,
},
])
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Return shipping", name: "Return shipping",
service_zone_id: fulfillmentSet.service_zones[0].id, service_zone_id: fulfillmentSet.service_zones[0].id,

View File

@@ -17,6 +17,7 @@ medusaIntegrationTestRunner({
let location let location
let item let item
let returnShippingOption let returnShippingOption
let outboundShippingOption
const shippingProviderId = "manual_test-provider" const shippingProviderId = "manual_test-provider"
beforeEach(async () => { beforeEach(async () => {
@@ -135,7 +136,7 @@ medusaIntegrationTestRunner({
prices: [ prices: [
{ {
currency_code: "usd", currency_code: "usd",
amount: 20, amount: 0, // Free shipping
}, },
], ],
rules: [ rules: [
@@ -160,7 +161,7 @@ medusaIntegrationTestRunner({
) )
).data.shipping_option ).data.shipping_option
const outboundShippingOption = ( outboundShippingOption = (
await api.post( await api.post(
"/admin/shipping-options", "/admin/shipping-options",
outboundShippingOptionPayload, outboundShippingOptionPayload,
@@ -289,6 +290,15 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
// If claim has outbound items, we need to set the outbound shipping method for reservation to be created
// TODO: change condition in confirm workfow to crate reservation depending on shipping since we can have
// reservations for inventory items that don't require shipping
await api.post(
`/admin/claims/${singleOutboundClaim.id}/outbound/shipping-method`,
{ shipping_option_id: outboundShippingOption.id },
adminHeaders
)
await api.post( await api.post(
`/admin/claims/${singleOutboundClaim.id}/request`, `/admin/claims/${singleOutboundClaim.id}/request`,
{}, {},

View File

@@ -239,6 +239,17 @@ medusaIntegrationTestRunner({
}, },
]) ])
// create reservation for inventory item that is initially on the order
const inventoryModule = container.resolve(Modules.INVENTORY)
await inventoryModule.createReservationItems([
{
inventory_item_id: inventoryItem.id,
location_id: location.id,
quantity: 2,
line_item_id: order.items[0].id,
},
])
const shippingOptionPayload = { const shippingOptionPayload = {
name: "Return shipping", name: "Return shipping",
service_zone_id: fulfillmentSet.service_zones[0].id, service_zone_id: fulfillmentSet.service_zones[0].id,
@@ -866,7 +877,9 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.inventory_levels ).data.inventory_levels
expect(inventoryLevel[0].stocked_quantity).toEqual(2)
// we had 2 items in stock that were reserved for the order and then fulfilled
expect(inventoryLevel[0].stocked_quantity).toEqual(0)
let result = await api.post( let result = await api.post(
`/admin/returns/${returnId}/receive`, `/admin/returns/${returnId}/receive`,
@@ -987,7 +1000,7 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
).data.inventory_levels ).data.inventory_levels
expect(inventoryLevel[0].stocked_quantity).toEqual(3) expect(inventoryLevel[0].stocked_quantity).toEqual(1)
}) })
}) })

View File

@@ -486,6 +486,9 @@ medusaIntegrationTestRunner({
it("should revert an order fulfillment when it fails and recreate it when tried again", async () => { it("should revert an order fulfillment when it fails and recreate it when tried again", async () => {
const order = await createOrderFixture({ container, product, location }) const order = await createOrderFixture({ container, product, location })
const itemId = order.items?.find((i) => i.title === "Custom Item 2")!
.id as string
// Create a fulfillment // Create a fulfillment
const createOrderFulfillmentData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput = const createOrderFulfillmentData: OrderWorkflow.CreateOrderFulfillmentWorkflowInput =
{ {
@@ -493,7 +496,7 @@ medusaIntegrationTestRunner({
created_by: "user_1", created_by: "user_1",
items: [ items: [
{ {
id: order.items![0].id, id: itemId,
quantity: 1, quantity: 1,
}, },
], ],
@@ -540,12 +543,14 @@ medusaIntegrationTestRunner({
const [orderFulfill] = await remoteQuery(remoteQueryObject) const [orderFulfill] = await remoteQuery(remoteQueryObject)
const fulfilledItem = orderFulfill.items?.find((i) => i.id === itemId)
expect(orderFulfill.fulfillments).toHaveLength(1) expect(orderFulfill.fulfillments).toHaveLength(1)
expect(orderFulfill.items[0].detail.fulfilled_quantity).toEqual(1) expect(fulfilledItem?.detail.fulfilled_quantity).toEqual(1)
const inventoryModule = container.resolve(Modules.INVENTORY) const inventoryModule = container.resolve(Modules.INVENTORY)
const reservation = await inventoryModule.listReservationItems({ const reservation = await inventoryModule.listReservationItems({
line_item_id: order.items![0].id, line_item_id: itemId,
}) })
expect(reservation).toHaveLength(0) expect(reservation).toHaveLength(0)

View File

@@ -42,16 +42,16 @@ export type OrderClaimAddNewItemValidationStepInput = {
} }
/** /**
* This step validates that new items can be added to the claim. If the * This step validates that new items can be added to the claim. If the
* order or claim is canceled, or the order change is not active, the step will throw an error. * order or claim is canceled, or the order change is not active, the step will throw an error.
* *
* :::note * :::note
* *
* You can retrieve an order, order claim, and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * You can retrieve an order, order claim, and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
* or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
* *
* ::: * :::
* *
* @example * @example
* const data = orderClaimAddNewItemValidationStep({ * const data = orderClaimAddNewItemValidationStep({
* order: { * order: {
@@ -85,10 +85,10 @@ export const orderClaimAddNewItemWorkflowId = "claim-add-new-item"
/** /**
* This workflow adds outbound (or new) items to a claim. It's used by the * This workflow adds outbound (or new) items to a claim. It's used by the
* [Add Outbound Items Admin API Route](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditems). * [Add Outbound Items Admin API Route](https://docs.medusajs.com/api/admin#claims_postclaimsidoutbounditems).
* *
* You can use this workflow within your customizations or your own custom workflows, allowing you to add outbound items to a claim * You can use this workflow within your customizations or your own custom workflows, allowing you to add outbound items to a claim
* in your custom flows. * in your custom flows.
* *
* @example * @example
* const { result } = await orderClaimAddNewItemWorkflow(container) * const { result } = await orderClaimAddNewItemWorkflow(container)
* .run({ * .run({
@@ -102,9 +102,9 @@ export const orderClaimAddNewItemWorkflowId = "claim-add-new-item"
* ] * ]
* } * }
* }) * })
* *
* @summary * @summary
* *
* Add outbound or new items to a claim. * Add outbound or new items to a claim.
*/ */
export const orderClaimAddNewItemWorkflow = createWorkflow( export const orderClaimAddNewItemWorkflow = createWorkflow(

View File

@@ -239,7 +239,7 @@ function extractShippingOption({ orderPreview, orderClaim, returnId }) {
for (const action of modifiedShippingMethod_.actions) { for (const action of modifiedShippingMethod_.actions) {
if (action.action === ChangeActionType.SHIPPING_ADD) { if (action.action === ChangeActionType.SHIPPING_ADD) {
if (action.return?.id === returnId) { if (!!action.return_id && action.return_id === returnId) {
returnShippingMethod = shippingMethod returnShippingMethod = shippingMethod
} else if (action.claim_id === orderClaim.id) { } else if (action.claim_id === orderClaim.id) {
claimShippingMethod = shippingMethod claimShippingMethod = shippingMethod

View File

@@ -241,10 +241,14 @@ function prepareInventoryUpdate({
}[] = [] }[] = []
const allItems = itemsList ?? order.items const allItems = itemsList ?? order.items
for (const item of allItems) { const itemsToFulfill = allItems.filter((i) => i.id in inputItemsMap)
// iterate over items that are being fulfilled
for (const item of itemsToFulfill) {
const reservation = reservationMap[item.id] const reservation = reservationMap[item.id]
if (!reservation) { if (!reservation) {
if (item.manage_inventory) { if (item.variant?.manage_inventory) {
throw new Error( throw new Error(
`No stock reservation found for item ${item.id} - ${item.title} (${item.variant_title})` `No stock reservation found for item ${item.id} - ${item.title} (${item.variant_title})`
) )
@@ -345,6 +349,7 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
"items.variant.height", "items.variant.height",
"items.variant.width", "items.variant.width",
"items.variant.material", "items.variant.material",
"items.variant_title",
"shipping_address.*", "shipping_address.*",
"shipping_methods.id", "shipping_methods.id",
"shipping_methods.shipping_option_id", "shipping_methods.shipping_option_id",
@@ -394,9 +399,11 @@ export const createOrderFulfillmentWorkflow = createWorkflow(
}).config({ name: "get-shipping-option" }) }).config({ name: "get-shipping-option" })
const lineItemIds = transform( const lineItemIds = transform(
{ order, itemsList: input.items_list }, { order, itemsList: input.items_list, inputItemsMap },
({ order, itemsList }) => { ({ order, itemsList, inputItemsMap }) => {
return (itemsList ?? order.items)!.map((i) => i.id) return (itemsList ?? order.items)!
.map((i) => i.id)
.filter((i) => i in inputItemsMap)
} }
) )
const reservations = useRemoteQueryStep({ const reservations = useRemoteQueryStep({