From 6f48ee284d902f87bad40c312debbf96a333d0f1 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 7 Aug 2023 11:20:45 +0200 Subject: [PATCH] fix(core): Fix logic relating to partial fulfillments Fixes #2324 fixes #2191 --- packages/core/e2e/order.e2e-spec.ts | 47 +++++++++++++++++++ .../src/service/helpers/utils/order-utils.ts | 35 ++++++++++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/core/e2e/order.e2e-spec.ts b/packages/core/e2e/order.e2e-spec.ts index 5d3409ccde..1af3d9b0b2 100644 --- a/packages/core/e2e/order.e2e-spec.ts +++ b/packages/core/e2e/order.e2e-spec.ts @@ -35,6 +35,7 @@ import * as Codegen from './graphql/generated-e2e-admin-types'; import { AddManualPaymentDocument, CanceledOrderFragment, + CreateFulfillmentDocument, ErrorCode, FulfillmentFragment, GetOrderDocument, @@ -47,6 +48,7 @@ import { RefundFragment, SortOrder, StockMovementType, + TransitFulfillmentDocument, } from './graphql/generated-e2e-admin-types'; import * as CodegenShop from './graphql/generated-e2e-shop-types'; import { @@ -2609,6 +2611,51 @@ describe('Orders resolver', () => { expect(order!.state).toBe('PaymentSettled'); }); + + // https://github.com/vendure-ecommerce/vendure/issues/2191 + it('correctly transitions order & fulfillment on partial fulfillment being shipped', async () => { + await shopClient.asUserWithCredentials(customers[0].emailAddress, password); + const { addItemToOrder } = await shopClient.query< + CodegenShop.AddItemToOrderMutation, + CodegenShop.AddItemToOrderMutationVariables + >(ADD_ITEM_TO_ORDER, { + productVariantId: 'T_6', + quantity: 3, + }); + await proceedToArrangingPayment(shopClient); + orderGuard.assertSuccess(addItemToOrder); + + const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod); + orderGuard.assertSuccess(order); + + const { addFulfillmentToOrder } = await adminClient.query(CreateFulfillmentDocument, { + input: { + lines: [{ orderLineId: order.lines[0].id, quantity: 2 }], + handler: { + code: manualFulfillmentHandler.code, + arguments: [ + { name: 'method', value: 'Test2' }, + { name: 'trackingCode', value: '222' }, + ], + }, + }, + }); + fulfillmentGuard.assertSuccess(addFulfillmentToOrder); + + const { transitionFulfillmentToState } = await adminClient.query(TransitFulfillmentDocument, { + id: addFulfillmentToOrder.id, + state: 'Shipped', + }); + fulfillmentGuard.assertSuccess(transitionFulfillmentToState); + + expect(transitionFulfillmentToState.id).toBe(addFulfillmentToOrder.id); + expect(transitionFulfillmentToState.state).toBe('Shipped'); + + const { order: order2 } = await adminClient.query(GetOrderDocument, { + id: order.id, + }); + expect(order2?.state).toBe('PartiallyShipped'); + }); }); }); diff --git a/packages/core/src/service/helpers/utils/order-utils.ts b/packages/core/src/service/helpers/utils/order-utils.ts index 942d94df24..2e41680b8b 100644 --- a/packages/core/src/service/helpers/utils/order-utils.ts +++ b/packages/core/src/service/helpers/utils/order-utils.ts @@ -44,7 +44,10 @@ export function totalCoveredByPayments(order: Order, state?: PaymentState | Paym * Returns true if all (non-cancelled) OrderItems are delivered. */ export function orderItemsAreDelivered(order: Order) { - return getOrderLinesFulfillmentStates(order).every(state => state === 'Delivered'); + return ( + getOrderLinesFulfillmentStates(order).every(state => state === 'Delivered') && + !isOrderPartiallyFulfilled(order) + ); } /** @@ -52,7 +55,10 @@ export function orderItemsAreDelivered(order: Order) { */ export function orderItemsArePartiallyDelivered(order: Order) { const states = getOrderLinesFulfillmentStates(order); - return states.some(state => state === 'Delivered') && !states.every(state => state === 'Delivered'); + return ( + states.some(state => state === 'Delivered') && + (!states.every(state => state === 'Delivered') || isOrderPartiallyFulfilled(order)) + ); } function getOrderLinesFulfillmentStates(order: Order): Array { @@ -65,7 +71,7 @@ function getOrderLinesFulfillmentStates(order: Order): Array l.fulfillment.state); } else { return undefined; @@ -81,14 +87,20 @@ function getOrderLinesFulfillmentStates(order: Order): Array state === 'Shipped') && !states.every(state => state === 'Shipped'); + return ( + states.some(state => state === 'Shipped') && + (!states.every(state => state === 'Shipped') || isOrderPartiallyFulfilled(order)) + ); } /** * Returns true if all (non-cancelled) OrderItems are shipped. */ export function orderItemsAreShipped(order: Order) { - return getOrderLinesFulfillmentStates(order).every(state => state === 'Shipped'); + return ( + getOrderLinesFulfillmentStates(order).every(state => state === 'Shipped') && + !isOrderPartiallyFulfilled(order) + ); } /** @@ -107,6 +119,19 @@ function getOrderFulfillmentLines(order: Order): FulfillmentLine[] { ); } +/** + * Returns true if Fulfillments exist for only some but not all of the + * order items. + */ +function isOrderPartiallyFulfilled(order: Order) { + const fulfillmentLines = getOrderFulfillmentLines(order); + const lines = fulfillmentLines.reduce((acc, item) => { + acc[item.orderLineId] = (acc[item.orderLineId] || 0) + item.quantity; + return acc; + }, {} as { [orderLineId: string]: number }); + return order.lines.some(line => line.quantity > lines[line.id]); +} + export async function getOrdersFromLines( ctx: RequestContext, connection: TransactionalConnection,