diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 7960ff75097d2..3771881e9a8fa 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -877,7 +877,7 @@ medusaIntegrationTestRunner({ items: [ { variant_id: productExtra.variants[0].id, - quantity: 2, + quantity: 3, }, ], }, @@ -917,7 +917,7 @@ medusaIntegrationTestRunner({ await api.get(`/admin/orders/${order.id}`, adminHeaders) ).data.order - const fulfillableItem = fulfillOrder.items.find( + const fulfillableItem = fulfillOrder.items.filter( (item) => item.detail.fulfilled_quantity === 0 ) @@ -925,10 +925,85 @@ medusaIntegrationTestRunner({ `/admin/orders/${order.id}/fulfillments`, { location_id: location.id, - items: [{ id: fulfillableItem.id, quantity: 1 }], + items: [{ id: fulfillableItem[0].id, quantity: 1 }], + }, + adminHeaders + ) + + let orderResult = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + expect(orderResult.fulfillment_status).toEqual("partially_fulfilled") + + await api.post( + `/admin/orders/${order.id}/fulfillments`, + { + location_id: location.id, + items: [{ id: fulfillableItem[0].id, quantity: 2 }], }, adminHeaders ) + + orderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=*fulfillments,*fulfillments.items`, + adminHeaders + ) + ).data.order + + expect(orderResult.fulfillment_status).toEqual("fulfilled") + + await api.post( + `admin/orders/${order.id}/fulfillments/${orderResult.fulfillments[0].id}/shipments`, + { + items: orderResult.fulfillments[0]?.items?.map((i) => ({ + id: i.line_item_id, + quantity: i.quantity, + })), + }, + adminHeaders + ) + + orderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=*fulfillments,*fulfillments.items`, + adminHeaders + ) + ).data.order + + expect(orderResult.fulfillment_status).toEqual("partially_shipped") + + await api.post( + `admin/orders/${order.id}/fulfillments/${orderResult.fulfillments[1].id}/shipments`, + { + items: orderResult.fulfillments[1]?.items?.map((i) => ({ + id: i.line_item_id, + quantity: i.quantity, + })), + }, + adminHeaders + ) + + await api.post( + `admin/orders/${order.id}/fulfillments/${orderResult.fulfillments[2].id}/shipments`, + { + items: orderResult.fulfillments[2]?.items?.map((i) => ({ + id: i.line_item_id, + quantity: i.quantity, + })), + }, + adminHeaders + ) + + orderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=*fulfillments`, + adminHeaders + ) + ).data.order + + expect(orderResult.fulfillment_status).toEqual("shipped") }) }) }) diff --git a/packages/core/core-flows/src/order/utils/aggregate-status.ts b/packages/core/core-flows/src/order/utils/aggregate-status.ts index 9d0733e0f3477..d59d26949ac44 100644 --- a/packages/core/core-flows/src/order/utils/aggregate-status.ts +++ b/packages/core/core-flows/src/order/utils/aggregate-status.ts @@ -1,5 +1,5 @@ import { OrderDetailDTO } from "@medusajs/types" -import { MathBN } from "@medusajs/utils" +import { isDefined, MathBN } from "@medusajs/utils" export const getLastPaymentStatus = (order: OrderDetailDTO) => { const PaymentStatus = { @@ -103,17 +103,22 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { } let fulfillmentStatus = {} + for (const status in FulfillmentStatus) { fulfillmentStatus[FulfillmentStatus[status]] = 0 } const statusMap = { - packed_at: FulfillmentStatus.FULFILLED, - shipped_at: FulfillmentStatus.SHIPPED, - delivered_at: FulfillmentStatus.DELIVERED, canceled_at: FulfillmentStatus.CANCELED, + delivered_at: FulfillmentStatus.DELIVERED, + shipped_at: FulfillmentStatus.SHIPPED, + packed_at: FulfillmentStatus.FULFILLED, } + for (const fulfillmentCollection of order.fulfillments) { + // Note: The order of the statusMap keys is important as we break + // the loop when we have found a match. The match should be prioritized + // based on order of precedence of statuses for (const key in statusMap) { if (fulfillmentCollection[key]) { fulfillmentStatus[statusMap[key]] += 1 @@ -126,10 +131,20 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { const totalFulfillmentsExceptCanceled = totalFulfillments - fulfillmentStatus[FulfillmentStatus.CANCELED] + // Whenever there are any unfulfilled items in the order, it should be + // considered partially_[STATUS] where status is picked up from the hierarchy + // of statuses + const hasUnfulfilledItems = (order.items || [])?.filter( + (i) => + isDefined(i?.detail?.raw_fulfilled_quantity) && + MathBN.lt(i.detail.raw_fulfilled_quantity, i.raw_quantity) + ).length > 0 + if (fulfillmentStatus[FulfillmentStatus.DELIVERED] > 0) { if ( fulfillmentStatus[FulfillmentStatus.DELIVERED] === - totalFulfillmentsExceptCanceled + totalFulfillmentsExceptCanceled && + !hasUnfulfilledItems ) { return FulfillmentStatus.DELIVERED } @@ -140,7 +155,8 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { if (fulfillmentStatus[FulfillmentStatus.SHIPPED] > 0) { if ( fulfillmentStatus[FulfillmentStatus.SHIPPED] === - totalFulfillmentsExceptCanceled + totalFulfillmentsExceptCanceled && + !hasUnfulfilledItems ) { return FulfillmentStatus.SHIPPED } @@ -151,7 +167,8 @@ export const getLastFulfillmentStatus = (order: OrderDetailDTO) => { if (fulfillmentStatus[FulfillmentStatus.FULFILLED] > 0) { if ( fulfillmentStatus[FulfillmentStatus.FULFILLED] === - totalFulfillmentsExceptCanceled + totalFulfillmentsExceptCanceled && + !hasUnfulfilledItems ) { return FulfillmentStatus.FULFILLED } diff --git a/packages/core/core-flows/src/order/workflows/get-orders-list.ts b/packages/core/core-flows/src/order/workflows/get-orders-list.ts index 9d91aa3f50a91..936c8e92cc932 100644 --- a/packages/core/core-flows/src/order/workflows/get-orders-list.ts +++ b/packages/core/core-flows/src/order/workflows/get-orders-list.ts @@ -37,6 +37,7 @@ export const getOrdersListWorkflow = createWorkflow( "id", "status", "version", + "items.*", "payment_collections.status", "payment_collections.amount", "payment_collections.captured_amount",