ソースを参照

feat(core): New "Created" initial state for Orders

Relates to #510.

BREAKING CHANGE: Orders now start in the new "Created" state, and then _immediately_ transition
to the "AddingItems" state. This allows e.g. event listeners to pick up newly-created Orders.
Michael Bromley 5 年 前
コミット
7a774e3bfd

+ 2 - 2
packages/core/e2e/global-settings.e2e-spec.ts

@@ -52,8 +52,8 @@ describe('GlobalSettings resolver', () => {
 
         it('includes orderProcess', () => {
             expect(globalSettings.serverConfig.orderProcess[0]).toEqual({
-                name: 'AddingItems',
-                to: ['ArrangingPayment', 'Cancelled'],
+                name: 'Created',
+                to: ['AddingItems'],
             });
         });
 

+ 19 - 1
packages/core/e2e/order-process.e2e-spec.ts

@@ -4,7 +4,7 @@ import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { AdminTransition, GetOrder, OrderFragment } from './graphql/generated-e2e-admin-types';
@@ -109,6 +109,24 @@ describe('Order process', () => {
         await server.destroy();
     });
 
+    describe('Initial transition', () => {
+        it('transitions from Created to AddingItems on creation', async () => {
+            transitionStartSpy.mockClear();
+            transitionEndSpy.mockClear();
+            await shopClient.asAnonymousUser();
+
+            await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
+
+            expect(transitionStartSpy).toHaveBeenCalledTimes(1);
+            expect(transitionEndSpy).toHaveBeenCalledTimes(1);
+            expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
+            expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
+        });
+    });
+
     describe('CustomOrderProcess', () => {
         it('CustomOrderProcess is injectable', () => {
             expect(initSpy).toHaveBeenCalled();

+ 22 - 1
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -15,7 +15,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import {
@@ -182,6 +182,13 @@ describe('Promotions applied to Orders', () => {
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
 
             expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_COUPON_APPLIED,
                     data: {
@@ -219,6 +226,13 @@ describe('Promotions applied to Orders', () => {
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
 
             expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_COUPON_APPLIED,
                     data: {
@@ -244,6 +258,13 @@ describe('Promotions applied to Orders', () => {
             });
 
             expect(removeCouponCode!.history.items.map(i => omit(i, ['id']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_COUPON_APPLIED,
                     data: {

+ 47 - 9
packages/core/e2e/order.e2e-spec.ts

@@ -157,13 +157,21 @@ describe('Orders resolver', () => {
         expect(result.order!.id).toBe('T_2');
     });
 
-    it('order history initially empty', async () => {
+    it('order history initially contains Created -> AddingItems transition', async () => {
         const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(
             GET_ORDER_HISTORY,
             { id: 'T_1' },
         );
-        expect(order!.history.totalItems).toBe(0);
-        expect(order!.history.items).toEqual([]);
+        expect(order!.history.totalItems).toBe(1);
+        expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+            {
+                type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                data: {
+                    from: 'Created',
+                    to: 'AddingItems',
+                },
+            },
+        ]);
     });
 
     describe('payments', () => {
@@ -278,6 +286,13 @@ describe('Orders resolver', () => {
                 { id: 'T_2', options: { sort: { id: SortOrder.ASC } } },
             );
             expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_STATE_TRANSITION,
                     data: {
@@ -538,7 +553,7 @@ describe('Orders resolver', () => {
                 {
                     id: 'T_2',
                     options: {
-                        skip: 5,
+                        skip: 6,
                     },
                 },
             );
@@ -1035,6 +1050,13 @@ describe('Orders resolver', () => {
                 },
             );
             expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_STATE_TRANSITION,
                     data: {
@@ -1272,6 +1294,13 @@ describe('Orders resolver', () => {
                 },
             );
             expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_STATE_TRANSITION,
                     data: {
@@ -1356,7 +1385,7 @@ describe('Orders resolver', () => {
                 {
                     id: orderId,
                     options: {
-                        skip: 0,
+                        skip: 1,
                     },
                 },
             );
@@ -1374,7 +1403,9 @@ describe('Orders resolver', () => {
 
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
 
-            expect(activeOrder!.history.items.map(pick(['type', 'data']))).toEqual([]);
+            expect(activeOrder!.history.items.map(pick(['type']))).toEqual([
+                { type: HistoryEntryType.ORDER_STATE_TRANSITION },
+            ]);
         });
 
         it('public note', async () => {
@@ -1396,7 +1427,7 @@ describe('Orders resolver', () => {
                 {
                     id: orderId,
                     options: {
-                        skip: 1,
+                        skip: 2,
                     },
                 },
             );
@@ -1413,6 +1444,13 @@ describe('Orders resolver', () => {
             const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
 
             expect(activeOrder!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                    data: {
+                        from: 'Created',
+                        to: 'AddingItems',
+                    },
+                },
                 {
                     type: HistoryEntryType.ORDER_NOTE,
                     data: {
@@ -1443,7 +1481,7 @@ describe('Orders resolver', () => {
                 GetOrderHistory.Query,
                 GetOrderHistory.Variables
             >(GET_ORDER_HISTORY, { id: orderId });
-            expect(before?.history.totalItems).toBe(2);
+            expect(before?.history.totalItems).toBe(3);
 
             const { deleteOrderNote } = await adminClient.query<
                 DeleteOrderNote.Mutation,
@@ -1458,7 +1496,7 @@ describe('Orders resolver', () => {
                 GetOrderHistory.Query,
                 GetOrderHistory.Variables
             >(GET_ORDER_HISTORY, { id: orderId });
-            expect(after?.history.totalItems).toBe(1);
+            expect(after?.history.totalItems).toBe(2);
         });
     });
 });

+ 0 - 3
packages/core/e2e/shop-order.e2e-spec.ts

@@ -22,7 +22,6 @@ import {
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 import {
-    ActiveOrderCustomer,
     ActiveOrderCustomerFragment,
     AddItemToOrder,
     AddPaymentToOrder,
@@ -37,7 +36,6 @@ import {
     GetNextOrderStates,
     GetOrderByCode,
     GetShippingMethods,
-    PaymentDeclinedError,
     RemoveAllOrderLines,
     RemoveItemFromOrder,
     SetBillingAddress,
@@ -75,7 +73,6 @@ import {
     SET_CUSTOMER,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
-    TEST_ORDER_FRAGMENT,
     TRANSITION_TO_STATE,
     UPDATED_ORDER_FRAGMENT,
 } from './graphql/shop-definitions';

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

@@ -29,7 +29,7 @@ import { OrderState, orderStateTransitions, OrderTransitionData } from './order-
 @Injectable()
 export class OrderStateMachine {
     readonly config: StateMachineConfig<OrderState, OrderTransitionData>;
-    private readonly initialState: OrderState = 'AddingItems';
+    private readonly initialState: OrderState = 'Created';
 
     constructor(
         private connection: TransactionalConnection,

+ 4 - 0
packages/core/src/service/helpers/order-state-machine/order-state.ts

@@ -10,6 +10,7 @@ import { Order } from '../../../entity/order/order.entity';
  * @docsCategory orders
  */
 export type OrderState =
+    | 'Created'
     | 'AddingItems'
     | 'ArrangingPayment'
     | 'PaymentAuthorized'
@@ -21,6 +22,9 @@ export type OrderState =
     | 'Cancelled';
 
 export const orderStateTransitions: Transitions<OrderState> = {
+    Created: {
+        to: ['AddingItems'],
+    },
     AddingItems: {
         to: ['ArrangingPayment', 'Cancelled'],
     },

+ 7 - 1
packages/core/src/service/services/order.service.ts

@@ -292,7 +292,13 @@ export class OrderService {
             }
         }
         this.channelService.assignToCurrentChannel(newOrder, ctx);
-        return this.connection.getRepository(ctx, Order).save(newOrder);
+        const order = await this.connection.getRepository(ctx, Order).save(newOrder);
+        const transitionResult = await this.transitionToState(ctx, order.id, 'AddingItems');
+        if (isGraphQlErrorResult(transitionResult)) {
+            // this should never occur, so we will throw rather than return
+            throw transitionResult;
+        }
+        return transitionResult;
     }
 
     async updateCustomFields(ctx: RequestContext, orderId: ID, customFields: any) {