Browse Source

fix(core): Loosen restriction on transitioning to PaymentAuthorized

It is valid to transition an Order to PaymentAuthorized if at least one payment is Authorized, and
the rest are either Authorized or Settled. Otherwise an edge-case can arise where it becomes
impossible to transition to any valid state.
Michael Bromley 5 years ago
parent
commit
59d39d6a9d

+ 5 - 2
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -108,8 +108,11 @@ export class OrderStateMachine {
                 return `message.cannot-transition-to-payment-without-customer`;
             }
         }
-        if (toState === 'PaymentAuthorized' && !orderTotalIsCovered(data.order, 'Authorized')) {
-            return `message.cannot-transition-without-authorized-payments`;
+        if (toState === 'PaymentAuthorized') {
+            const hasAnAuthorizedPayment = !!data.order.payments.find(p => p.state === 'Authorized');
+            if (!orderTotalIsCovered(data.order, ['Authorized', 'Settled']) || !hasAnAuthorizedPayment) {
+                return `message.cannot-transition-without-authorized-payments`;
+            }
         }
         if (toState === 'PaymentSettled' && !orderTotalIsCovered(data.order, 'Settled')) {
             return `message.cannot-transition-without-settled-payments`;

+ 87 - 0
packages/core/src/service/helpers/utils/order-utils.spec.ts

@@ -0,0 +1,87 @@
+import { Order } from '../../../entity/order/order.entity';
+import { Payment } from '../../../entity/payment/payment.entity';
+
+import { totalCoveredByPayments } from './order-utils';
+
+describe('totalCoveredByPayments()', () => {
+    it('single payment, any state, no refunds', () => {
+        const order = new Order({
+            payments: [
+                new Payment({
+                    state: 'Settled',
+                    amount: 500,
+                }),
+            ],
+        });
+
+        expect(totalCoveredByPayments(order)).toBe(500);
+    });
+
+    it('multiple payments, any state, no refunds', () => {
+        const order = new Order({
+            payments: [
+                new Payment({
+                    state: 'Settled',
+                    amount: 500,
+                }),
+                new Payment({
+                    state: 'Settled',
+                    amount: 300,
+                }),
+            ],
+        });
+
+        expect(totalCoveredByPayments(order)).toBe(800);
+    });
+
+    it('multiple payments, any state, error and declined', () => {
+        const order = new Order({
+            payments: [
+                new Payment({
+                    state: 'Error',
+                    amount: 500,
+                }),
+                new Payment({
+                    state: 'Declined',
+                    amount: 300,
+                }),
+            ],
+        });
+
+        expect(totalCoveredByPayments(order)).toBe(0);
+    });
+
+    it('multiple payments, single state', () => {
+        const order = new Order({
+            payments: [
+                new Payment({
+                    state: 'Settled',
+                    amount: 500,
+                }),
+                new Payment({
+                    state: 'Authorized',
+                    amount: 300,
+                }),
+            ],
+        });
+
+        expect(totalCoveredByPayments(order, 'Settled')).toBe(500);
+    });
+
+    it('multiple payments, multiple states', () => {
+        const order = new Order({
+            payments: [
+                new Payment({
+                    state: 'Settled',
+                    amount: 500,
+                }),
+                new Payment({
+                    state: 'Authorized',
+                    amount: 300,
+                }),
+            ],
+        });
+
+        expect(totalCoveredByPayments(order, ['Settled', 'Authorized'])).toBe(800);
+    });
+});

+ 5 - 3
packages/core/src/service/helpers/utils/order-utils.ts

@@ -7,7 +7,7 @@ import { PaymentState } from '../payment-state-machine/payment-state';
 /**
  * Returns true if the Order total is covered by Payments in the specified state.
  */
-export function orderTotalIsCovered(order: Order, state: PaymentState): boolean {
+export function orderTotalIsCovered(order: Order, state: PaymentState | PaymentState[]): boolean {
     const paymentsTotal = totalCoveredByPayments(order, state);
     return paymentsTotal === order.totalWithTax;
 }
@@ -15,9 +15,11 @@ export function orderTotalIsCovered(order: Order, state: PaymentState): boolean
 /**
  * Returns the total amount covered by all Payments (minus any refunds)
  */
-export function totalCoveredByPayments(order: Order, state?: PaymentState): number {
+export function totalCoveredByPayments(order: Order, state?: PaymentState | PaymentState[]): number {
     const payments = state
-        ? order.payments.filter(p => p.state === state)
+        ? Array.isArray(state)
+            ? order.payments.filter(p => state.includes(p.state))
+            : order.payments.filter(p => p.state === state)
         : order.payments.filter(p => p.state !== 'Error' && p.state !== 'Declined');
     let total = 0;
     for (const payment of payments) {