From 39e5eadefcf123c364240c665e8b455d1a8c38c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 12 May 2025 19:09:23 +0200 Subject: [PATCH] fix(core-flows, dashboard): reservation recreation on fulfilment cancel + allocation button display (#12447) **What** - fix recreation of reservations on fulfilment cancel - fix allocate items button display --- CLOSES CMRC-1018 --- .changeset/orange-boats-rescue.md | 5 + .../http/__tests__/order/admin/order.spec.ts | 249 +++++++++++++++++- .../admin/dashboard/src/hooks/api/orders.tsx | 8 + .../order-summary-section.tsx | 2 +- .../workflows/cancel-order-fulfillment.ts | 4 +- 5 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 .changeset/orange-boats-rescue.md diff --git a/.changeset/orange-boats-rescue.md b/.changeset/orange-boats-rescue.md new file mode 100644 index 0000000000..12ae6bdab1 --- /dev/null +++ b/.changeset/orange-boats-rescue.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +fix(dashboard, core-flows): allocation button condition and reservation recreation on fulfilment cancel diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index 339262920c..5ef87cf83e 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -1014,16 +1014,22 @@ medusaIntegrationTestRunner({ let inventoryItemDesk let inventoryItemLeg + let region + let salesChannel + let stockLocation + let shippingOption + let storeHeaders + beforeEach(async () => { const container = getContainer() const publishableKey = await generatePublishableKey(container) - const storeHeaders = generateStoreHeaders({ + storeHeaders = generateStoreHeaders({ publishableKey, }) - const region = ( + region = ( await api.post( "/admin/regions", { name: "Test region", currency_code: "usd" }, @@ -1031,7 +1037,7 @@ medusaIntegrationTestRunner({ ) ).data.region - const salesChannel = ( + salesChannel = ( await api.post( "/admin/sales-channels", { name: "first channel", description: "channel" }, @@ -1039,7 +1045,7 @@ medusaIntegrationTestRunner({ ) ).data.sales_channel - const stockLocation = ( + stockLocation = ( await api.post( `/admin/stock-locations`, { name: "test location" }, @@ -1087,7 +1093,7 @@ medusaIntegrationTestRunner({ adminHeaders ) - const shippingProfile = ( + shippingProfile = ( await api.post( `/admin/shipping-profiles`, { name: `test-${stockLocation.id}`, type: "default" }, @@ -1160,7 +1166,7 @@ medusaIntegrationTestRunner({ adminHeaders ) - const shippingOption = ( + shippingOption = ( await api.post( `/admin/shipping-options`, { @@ -1241,6 +1247,227 @@ medusaIntegrationTestRunner({ ).data.order }) + it("should create and cancel a fulfillment with reservations recreation multiple times", async () => { + const inventoryItemTablet = ( + await api.post( + `/admin/inventory-items`, + { sku: "tablet" }, + adminHeaders + ) + ).data.inventory_item + + await api.post( + `/admin/inventory-items/${inventoryItemTablet.id}/location-levels`, + { + location_id: stockLocation.id, + stocked_quantity: 10, + }, + adminHeaders + ) + + const productTablet = ( + await api.post( + "/admin/products", + { + title: `Tablet`, + shipping_profile_id: shippingProfile.id, + options: [{ title: "color", values: ["green"] }], + variants: [ + { + title: "Green tablet", + sku: "green-tablet", + inventory_items: [ + { + inventory_item_id: inventoryItemTablet.id, + required_quantity: 1, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 1000, + }, + ], + options: { + color: "green", + }, + }, + ], + }, + adminHeaders + ) + ).data.product + + const cartTablet = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + email: "tony@stark-industries.com", + region_id: region.id, + shipping_address: { + address_1: "test address 1", + address_2: "test address 2", + city: "ny", + country_code: "us", + province: "ny", + postal_code: "94016", + }, + billing_address: { + address_1: "test billing address 1", + address_2: "test billing address 2", + city: "ny", + country_code: "us", + province: "ny", + postal_code: "94016", + }, + sales_channel_id: salesChannel.id, + items: [ + { quantity: 1, variant_id: productTablet.variants[0].id }, + ], + }, + storeHeaders + ) + ).data.cart + + await api.post( + `/store/carts/${cartTablet.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + const paymentCollectionTablet = ( + await api.post( + `/store/payment-collections`, + { + cart_id: cartTablet.id, + }, + storeHeaders + ) + ).data.payment_collection + + await api.post( + `/store/payment-collections/${paymentCollectionTablet.id}/payment-sessions`, + { provider_id: "pp_system_default" }, + storeHeaders + ) + + const tabletOrder = ( + await api.post( + `/store/carts/${cartTablet.id}/complete`, + {}, + storeHeaders + ) + ).data.order + + const lineItemId = tabletOrder.items[0].id + + let reservations = (await api.get(`/admin/reservations`, adminHeaders)) + .data.reservations + + expect(reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + line_item_id: lineItemId, + inventory_item_id: inventoryItemTablet.id, + quantity: 1, + }), + ]) + ) + + // create a fulfillment for the entiretablet order + const fulOrder = ( + await api.post( + `/admin/orders/${tabletOrder.id}/fulfillments?fields=*fulfillments,*fulfillments.items`, + { + items: [{ id: tabletOrder.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + ).data.order + + reservations = ( + await api.get( + `/admin/reservations?line_item_id[]=${lineItemId}`, + adminHeaders + ) + ).data.reservations + + // no more reservations since everything is fuliflled + expect(reservations).toEqual(expect.arrayContaining([])) + + // cancel the fulfillment + await api.post( + `/admin/orders/${tabletOrder.id}/fulfillments/${fulOrder.fulfillments[0].id}/cancel`, + {}, + adminHeaders + ) + + reservations = ( + await api.get( + `/admin/reservations?line_item_id=${lineItemId}`, + adminHeaders + ) + ).data.reservations + + // reservations are recreated after the fulfillment is canceled + expect(reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + line_item_id: lineItemId, + inventory_item_id: inventoryItemTablet.id, + quantity: 1, + }), + ]) + ) + + // create a fulfillment for the entiretablet order again + const fulOrder2 = ( + await api.post( + `/admin/orders/${tabletOrder.id}/fulfillments?fields=*fulfillments,*fulfillments.items`, + { + items: [{ id: tabletOrder.items[0].id, quantity: 1 }], + }, + adminHeaders + ) + ).data.order + + reservations = ( + await api.get( + `/admin/reservations?line_item_id[]=${lineItemId}`, + adminHeaders + ) + ).data.reservations + + // no more reservations since everything is fuliflled again + expect(reservations).toEqual(expect.arrayContaining([])) + + // cancel the fulfillment + await api.post( + `/admin/orders/${tabletOrder.id}/fulfillments/${fulOrder2.fulfillments[0].id}/cancel`, + {}, + adminHeaders + ) + + reservations = ( + await api.get( + `/admin/reservations?line_item_id=${lineItemId}`, + adminHeaders + ) + ).data.reservations + + // reservations are recreated again + expect(reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + line_item_id: lineItemId, + inventory_item_id: inventoryItemTablet.id, + quantity: 1, + }), + ]) + ) + }) + it("should correctly manage reservations when canceling a fulfillment (with inventory kit)", async () => { let reservations = (await api.get(`/admin/reservations`, adminHeaders)) .data.reservations @@ -1258,6 +1485,8 @@ medusaIntegrationTestRunner({ ]) ) + const lineItemId = order.items[0].id + // 1. create a partial fulfillment const fulOrder = ( await api.post( @@ -1339,8 +1568,12 @@ medusaIntegrationTestRunner({ ) ).data.order - reservations = (await api.get(`/admin/reservations`, adminHeaders)).data - .reservations + reservations = ( + await api.get( + `/admin/reservations?line_item_id[]=${lineItemId}`, + adminHeaders + ) + ).data.reservations // 6. no more reservations since the entier quantity is fulfilled expect(reservations).toEqual([]) diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx index 3559f96687..923a50e5f6 100644 --- a/packages/admin/dashboard/src/hooks/api/orders.tsx +++ b/packages/admin/dashboard/src/hooks/api/orders.tsx @@ -220,6 +220,14 @@ export const useCancelOrderFulfillment = ( queryKey: ordersQueryKeys.preview(orderId), }) + queryClient.invalidateQueries({ + queryKey: reservationItemsQueryKeys.lists(), + }) + + queryClient.invalidateQueries({ + queryKey: inventoryItemsQueryKeys.details(), + }) + options?.onSuccess?.(data, variables, context) }, ...options, diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 8676815436..8f7412bd11 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -114,7 +114,7 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { } return false - }, [reservations]) + }, [order.items, reservations]) const unpaidPaymentCollection = order.payment_collections.find( (pc) => pc.status === "not_paid" diff --git a/packages/core/core-flows/src/order/workflows/cancel-order-fulfillment.ts b/packages/core/core-flows/src/order/workflows/cancel-order-fulfillment.ts index 5698307633..6546e0eba9 100644 --- a/packages/core/core-flows/src/order/workflows/cancel-order-fulfillment.ts +++ b/packages/core/core-flows/src/order/workflows/cancel-order-fulfillment.ts @@ -203,6 +203,7 @@ function prepareInventoryUpdate({ inventory_item_id: string location_id: string quantity: BigNumberInput + line_item_id: string }[] = [] const toUpdate: { id: string @@ -236,6 +237,7 @@ function prepareInventoryUpdate({ inventory_item_id: iitem.inventory.id, location_id: fulfillment.location_id, quantity: fulfillmentItem.quantity, // <- this is the inventory quantity that is being fulfilled so it menas it does include the required quantity + line_item_id: fulfillmentItem.line_item_id as string, }) } else { toUpdate.push({ @@ -338,7 +340,7 @@ export const cancelOrderFulfillmentWorkflow = createWorkflow( "location_id", ], variables: { - filter: { + filters: { line_item_id: lineItemIds, }, },