Просмотр исходного кода

refactor(core): Extract all hard-coded transition logic

Michael Bromley 3 лет назад
Родитель
Сommit
7b2daa6217

+ 36 - 33
packages/core/e2e/order.e2e-spec.ts

@@ -1940,8 +1940,8 @@ describe('Orders resolver', () => {
             expect(onCancelPaymentSpy).not.toHaveBeenCalled();
 
             const { cancelPayment } = await adminClient.query<
-                CancelPaymentMutation,
-                CancelPaymentMutationVariables
+                Codegen.CancelPaymentMutation,
+                Codegen.CancelPaymentMutationVariables
             >(CANCEL_PAYMENT, {
                 paymentId,
             });
@@ -1962,20 +1962,20 @@ describe('Orders resolver', () => {
             const paymentId = order.payments![0].id;
 
             const { cancelPayment } = await adminClient.query<
-                CancelPaymentMutation,
-                CancelPaymentMutationVariables
+                Codegen.CancelPaymentMutation,
+                Codegen.CancelPaymentMutationVariables
             >(CANCEL_PAYMENT, {
                 paymentId,
             });
 
             paymentGuard.assertErrorResult(cancelPayment);
             expect(cancelPayment.message).toBe('Cancelling the payment failed');
-            const { order: checkorder } = await adminClient.query<GetOrderQuery, GetOrderQueryVariables>(
-                GET_ORDER,
-                {
-                    id: order.id,
-                },
-            );
+            const { order: checkorder } = await adminClient.query<
+                Codegen.GetOrderQuery,
+                Codegen.GetOrderQueryVariables
+            >(GET_ORDER, {
+                id: order.id,
+            });
             expect(checkorder!.payments![0].state).toBe('Authorized');
             expect(checkorder!.payments![0].metadata).toEqual({ cancellationData: 'foo' });
         });
@@ -2577,36 +2577,39 @@ describe('Orders resolver', () => {
             const order = await addPaymentToOrder(shopClient, singleStageRefundablePaymentMethod);
             orderGuard.assertSuccess(order);
 
-            await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
-                CREATE_FULFILLMENT,
-                {
-                    input: {
-                        lines: [
-                            {
-                                orderLineId: order.lines[0].id,
-                                quantity: 1,
-                            },
-                        ],
-                        handler: {
-                            code: manualFulfillmentHandler.code,
-                            arguments: [{ name: 'method', value: 'Test' }],
+            await adminClient.query<
+                Codegen.CreateFulfillmentMutation,
+                Codegen.CreateFulfillmentMutationVariables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    lines: [
+                        {
+                            orderLineId: order.lines[0].id,
+                            quantity: 1,
                         },
+                    ],
+                    handler: {
+                        code: manualFulfillmentHandler.code,
+                        arguments: [{ name: 'method', value: 'Test' }],
                     },
                 },
-            );
+            });
 
-            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
-                CANCEL_ORDER,
-                {
-                    input: {
-                        orderId: order.id,
-                        lines: [{ orderLineId: order.lines[0].id, quantity: 1 }],
-                    },
+            const { cancelOrder } = await adminClient.query<
+                Codegen.CancelOrderMutation,
+                Codegen.CancelOrderMutationVariables
+            >(CANCEL_ORDER, {
+                input: {
+                    orderId: order.id,
+                    lines: [{ orderLineId: order.lines[0].id, quantity: 1 }],
                 },
-            );
+            });
             orderGuard.assertSuccess(cancelOrder);
 
-            const { order: order2 } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+            const { order: order2 } = await adminClient.query<
+                Codegen.GetOrderQuery,
+                Codegen.GetOrderQueryVariables
+            >(GET_ORDER, {
                 id: order.id,
             });
 

+ 19 - 19
packages/core/src/common/finite-state-machine/finite-state-machine.spec.ts

@@ -36,13 +36,13 @@ describe('Finite State Machine', () => {
         const initialState = 'DoorsClosed';
         const fsm = new FSM<TestState>({ transitions }, initialState);
 
-        fsm.transitionTo('Moving');
+        fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe('Moving');
-        fsm.transitionTo('DoorsClosed');
+        fsm.transitionTo('DoorsClosed', {});
         expect(fsm.currentState).toBe('DoorsClosed');
-        fsm.transitionTo('DoorsOpen');
+        fsm.transitionTo('DoorsOpen', {});
         expect(fsm.currentState).toBe('DoorsOpen');
-        fsm.transitionTo('DoorsClosed');
+        fsm.transitionTo('DoorsClosed', {});
         expect(fsm.currentState).toBe('DoorsClosed');
     });
 
@@ -50,13 +50,13 @@ describe('Finite State Machine', () => {
         const initialState = 'DoorsOpen';
         const fsm = new FSM<TestState>({ transitions }, initialState);
 
-        fsm.transitionTo('Moving');
+        fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe('DoorsOpen');
-        fsm.transitionTo('DoorsClosed');
+        fsm.transitionTo('DoorsClosed', {});
         expect(fsm.currentState).toBe('DoorsClosed');
-        fsm.transitionTo('Moving');
+        fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe('Moving');
-        fsm.transitionTo('DoorsOpen');
+        fsm.transitionTo('DoorsOpen', {});
         expect(fsm.currentState).toBe('Moving');
     });
 
@@ -81,7 +81,7 @@ describe('Finite State Machine', () => {
         expect(currentStateDuringCallback).toBe(initialState);
     });
 
-    it('onTransitionEnd() is invoked after a transition takes place', () => {
+    it('onTransitionEnd() is invoked after a transition takes place', async () => {
         const initialState = 'DoorsClosed';
         const spy = jest.fn();
         const data = 123;
@@ -96,8 +96,8 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        fsm.transitionTo('Moving', data);
-
+        const { finalize } = await fsm.transitionTo('Moving', data);
+        await finalize();
         expect(spy).toHaveBeenCalledWith(initialState, 'Moving', data);
         expect(currentStateDuringCallback).toBe('Moving');
     });
@@ -112,7 +112,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe(initialState);
     });
 
@@ -126,7 +126,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe(initialState);
     });
 
@@ -140,7 +140,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe(initialState);
     });
 
@@ -154,7 +154,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe(initialState);
     });
 
@@ -168,7 +168,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe('Moving');
     });
 
@@ -184,7 +184,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(fsm.currentState).toBe('Moving');
     });
 
@@ -199,7 +199,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(spy).toHaveBeenCalledWith(initialState, 'Moving', undefined);
     });
 
@@ -215,7 +215,7 @@ describe('Finite State Machine', () => {
             initialState,
         );
 
-        await fsm.transitionTo('Moving');
+        await fsm.transitionTo('Moving', {});
         expect(spy).toHaveBeenCalledWith(initialState, 'Moving', 'error');
     });
 });

+ 15 - 8
packages/core/src/common/finite-state-machine/finite-state-machine.ts

@@ -36,8 +36,10 @@ export class FSM<T extends string, Data = any> {
      * Attempts to transition from the current state to the given state. If this transition is not allowed
      * per the config, then an error will be logged.
      */
-    transitionTo(state: T, data?: Data): void;
-    async transitionTo(state: T, data: Data) {
+    async transitionTo(state: T, data: Data): Promise<{ finalize: () => Promise<any> }> {
+        const finalizeNoop: () => Promise<any> = async () => {
+            /**/
+        };
         if (this.canTransitionTo(state)) {
             // If the onTransitionStart callback is defined, invoke it. If it returns false,
             // then the transition will be cancelled.
@@ -46,21 +48,26 @@ export class FSM<T extends string, Data = any> {
                     this.config.onTransitionStart(this._currentState, state, data),
                 );
                 if (canTransition === false) {
-                    return;
+                    return { finalize: finalizeNoop };
                 } else if (typeof canTransition === 'string') {
                     await this.onError(this._currentState, state, canTransition);
-                    return;
+                    return { finalize: finalizeNoop };
                 }
             }
             const fromState = this._currentState;
             // All is well, so transition to the new state.
             this._currentState = state;
             // If the onTransitionEnd callback is defined, invoke it.
-            if (typeof this.config.onTransitionEnd === 'function') {
-                await awaitPromiseOrObservable(this.config.onTransitionEnd(fromState, state, data));
-            }
+            return {
+                finalize: async () => {
+                    if (typeof this.config.onTransitionEnd === 'function') {
+                        await awaitPromiseOrObservable(this.config.onTransitionEnd(fromState, state, data));
+                    }
+                },
+            };
         } else {
-            return this.onError(this._currentState, state);
+            await this.onError(this._currentState, state);
+            return { finalize: finalizeNoop };
         }
     }
 

+ 97 - 6
packages/core/src/config/fulfillment/default-fulfillment-process.ts

@@ -1,7 +1,14 @@
 import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
 
-import { awaitPromiseOrObservable, Transitions } from '../../common/index';
-import { FulfillmentState } from '../../service/index';
+import { RequestContext } from '../../api/common/request-context';
+import { awaitPromiseOrObservable } from '../../common/index';
+import { Fulfillment } from '../../entity/index';
+import { OrderItem } from '../../entity/order-item/order-item.entity';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { Order } from '../../entity/order/order.entity';
+import { orderItemsAreDelivered, orderItemsAreShipped } from '../../service/helpers/utils/order-utils';
+import { FulfillmentState, OrderState } from '../../service/index';
 
 import { FulfillmentProcess } from './fulfillment-process';
 
@@ -12,8 +19,11 @@ declare module '../../service/helpers/fulfillment-state-machine/fulfillment-stat
     }
 }
 
+let connection: import('../../connection/transactional-connection').TransactionalConnection;
 let configService: import('../config.service').ConfigService;
+let orderService: import('../../service/index').OrderService;
 let historyService: import('../../service/index').HistoryService;
+let stockMovementService: import('../../service/index').StockMovementService;
 
 /**
  * @description
@@ -42,10 +52,18 @@ export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
     async init(injector) {
         // Lazily import these services to avoid a circular dependency error
         // due to this being used as part of the DefaultConfig
+        const TransactionalConnection = await import('../../connection/transactional-connection').then(
+            m => m.TransactionalConnection,
+        );
         const ConfigService = await import('../config.service').then(m => m.ConfigService);
         const HistoryService = await import('../../service/index').then(m => m.HistoryService);
+        const OrderService = await import('../../service/index').then(m => m.OrderService);
+        const StockMovementService = await import('../../service/index').then(m => m.StockMovementService);
+        connection = injector.get(TransactionalConnection);
         configService = injector.get(ConfigService);
+        orderService = injector.get(OrderService);
         historyService = injector.get(HistoryService);
+        stockMovementService = injector.get(StockMovementService);
     },
     async onTransitionStart(fromState, toState, data) {
         const { fulfillmentHandlers } = configService.shippingOptions;
@@ -59,19 +77,92 @@ export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
             }
         }
     },
-    async onTransitionEnd(fromState, toState, data) {
-        const historyEntryPromises = data.orders.map(order =>
+    async onTransitionEnd(fromState, toState, { ctx, fulfillment, orders }) {
+        if (toState === 'Cancelled') {
+            await stockMovementService.createCancellationsForOrderItems(ctx, fulfillment.orderItems);
+            const lines = await groupOrderItemsIntoLines(ctx, fulfillment.orderItems);
+            await stockMovementService.createAllocationsForOrderLines(ctx, lines);
+        }
+        const historyEntryPromises = orders.map(order =>
             historyService.createHistoryEntryForOrder({
                 orderId: order.id,
                 type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
-                ctx: data.ctx,
+                ctx,
                 data: {
-                    fulfillmentId: data.fulfillment.id,
+                    fulfillmentId: fulfillment.id,
                     from: fromState,
                     to: toState,
                 },
             }),
         );
+
         await Promise.all(historyEntryPromises);
+
+        await Promise.all(
+            orders.map(order =>
+                handleFulfillmentStateTransitByOrder(ctx, order, fulfillment, fromState, toState),
+            ),
+        );
     },
 };
+
+async function groupOrderItemsIntoLines(
+    ctx: RequestContext,
+    orderItems: OrderItem[],
+): Promise<Array<{ orderLine: OrderLine; quantity: number }>> {
+    const orderLineIdQuantityMap = new Map<ID, number>();
+    for (const item of orderItems) {
+        const quantity = orderLineIdQuantityMap.get(item.lineId);
+        if (quantity == null) {
+            orderLineIdQuantityMap.set(item.lineId, 1);
+        } else {
+            orderLineIdQuantityMap.set(item.lineId, quantity + 1);
+        }
+    }
+    const orderLines = await connection
+        .getRepository(ctx, OrderLine)
+        .findByIds([...orderLineIdQuantityMap.keys()], {
+            relations: ['productVariant'],
+        });
+    return orderLines.map(orderLine => ({
+        orderLine,
+        // tslint:disable-next-line:no-non-null-assertion
+        quantity: orderLineIdQuantityMap.get(orderLine.id)!,
+    }));
+}
+
+async function handleFulfillmentStateTransitByOrder(
+    ctx: RequestContext,
+    order: Order,
+    fulfillment: Fulfillment,
+    fromState: FulfillmentState,
+    toState: FulfillmentState,
+): Promise<void> {
+    const nextOrderStates = orderService.getNextOrderStates(order);
+
+    const transitionOrderIfStateAvailable = (state: OrderState) =>
+        nextOrderStates.includes(state) && orderService.transitionToState(ctx, order.id, state);
+
+    if (toState === 'Shipped') {
+        const orderWithFulfillment = await getOrderWithFulfillments(ctx, order.id);
+        if (orderItemsAreShipped(orderWithFulfillment)) {
+            await transitionOrderIfStateAvailable('Shipped');
+        } else {
+            await transitionOrderIfStateAvailable('PartiallyShipped');
+        }
+    }
+    if (toState === 'Delivered') {
+        const orderWithFulfillment = await getOrderWithFulfillments(ctx, order.id);
+        if (orderItemsAreDelivered(orderWithFulfillment)) {
+            await transitionOrderIfStateAvailable('Delivered');
+        } else {
+            await transitionOrderIfStateAvailable('PartiallyDelivered');
+        }
+    }
+}
+
+async function getOrderWithFulfillments(ctx: RequestContext, orderId: ID) {
+    return await connection.getEntityOrThrow(ctx, Order, orderId, {
+        relations: ['lines', 'lines.items', 'lines.items.fulfillments'],
+    });
+}

+ 4 - 0
packages/core/src/config/order/default-order-process.ts

@@ -160,6 +160,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
     let eventBus: import('../../event-bus/index').EventBus;
     let stockMovementService: import('../../service/index').StockMovementService;
     let historyService: import('../../service/index').HistoryService;
+    let orderSplitter: import('../../service/index').OrderSplitter;
 
     const orderProcess: OrderProcess<OrderState> = {
         transitions: {
@@ -234,6 +235,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
                 m => m.StockMovementService,
             );
             const HistoryService = await import('../../service/index').then(m => m.HistoryService);
+            const OrderSplitter = await import('../../service/index').then(m => m.OrderSplitter);
             const ProductVariantService = await import('../../service/index').then(
                 m => m.ProductVariantService,
             );
@@ -243,6 +245,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
             eventBus = injector.get(EventBus);
             stockMovementService = injector.get(StockMovementService);
             historyService = injector.get(HistoryService);
+            orderSplitter = injector.get(OrderSplitter);
         },
 
         async onTransitionStart(fromState, toState, { ctx, order }) {
@@ -400,6 +403,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
                     order.active = false;
                     order.orderPlacedAt = new Date();
                     eventBus.publish(new OrderPlacedEvent(fromState, toState, ctx, order));
+                    await orderSplitter.createSellerOrders(ctx, order);
                 }
             }
             const shouldAllocateStock = await stockAllocationStrategy.shouldAllocateStock(

+ 15 - 2
packages/core/src/config/payment/default-payment-process.ts

@@ -1,7 +1,7 @@
 import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
-import { awaitPromiseOrObservable, Transitions } from '../../common/index';
-import { FulfillmentState, PaymentState } from '../../service/index';
+import { orderTotalIsCovered } from '../../service/helpers/utils/order-utils';
+import { PaymentState } from '../../service/index';
 
 import { PaymentProcess } from './payment-process';
 
@@ -14,6 +14,7 @@ declare module '../../service/helpers/payment-state-machine/payment-state' {
 }
 
 let configService: import('../config.service').ConfigService;
+let orderService: import('../../service/services/order.service').OrderService;
 let historyService: import('../../service/index').HistoryService;
 
 /**
@@ -48,13 +49,18 @@ export const defaultPaymentProcess: PaymentProcess<PaymentState> = {
         // due to this being used as part of the DefaultConfig
         const ConfigService = await import('../config.service').then(m => m.ConfigService);
         const HistoryService = await import('../../service/index').then(m => m.HistoryService);
+        const OrderService = await import('../../service/services/order.service').then(m => m.OrderService);
         configService = injector.get(ConfigService);
         historyService = injector.get(HistoryService);
+        orderService = injector.get(OrderService);
     },
     async onTransitionStart(fromState, toState, data) {
         // nothing here by default
     },
     async onTransitionEnd(fromState, toState, data) {
+        const { ctx, payment, order } = data;
+        order.payments = await orderService.getOrderPayments(ctx, order.id);
+
         await historyService.createHistoryEntryForOrder({
             ctx: data.ctx,
             orderId: data.order.id,
@@ -65,5 +71,12 @@ export const defaultPaymentProcess: PaymentProcess<PaymentState> = {
                 to: toState,
             },
         });
+
+        if (orderTotalIsCovered(order, 'Settled') && order.state !== 'PaymentSettled') {
+            await orderService.transitionToState(ctx, order.id, 'PaymentSettled');
+        }
+        if (orderTotalIsCovered(order, ['Authorized', 'Settled']) && order.state !== 'PaymentAuthorized') {
+            await orderService.transitionToState(ctx, order.id, 'PaymentAuthorized');
+        }
     },
 };

+ 4 - 2
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts

@@ -8,6 +8,7 @@ import { StateMachineConfig, Transitions } from '../../../common/finite-state-ma
 import { validateTransitionDefinition } from '../../../common/finite-state-machine/validate-transition-definition';
 import { awaitPromiseOrObservable } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
+import { TransactionalConnection } from '../../../connection/index';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 
@@ -18,7 +19,7 @@ export class FulfillmentStateMachine {
     readonly config: StateMachineConfig<FulfillmentState, FulfillmentTransitionData>;
     private readonly initialState: FulfillmentState = 'Created';
 
-    constructor(private configService: ConfigService) {
+    constructor(private configService: ConfigService, private connection: TransactionalConnection) {
         this.config = this.initConfig();
     }
 
@@ -42,8 +43,9 @@ export class FulfillmentStateMachine {
         state: FulfillmentState,
     ) {
         const fsm = new FSM(this.config, fulfillment.state);
-        await fsm.transitionTo(state, { ctx, orders, fulfillment });
+        const result = await fsm.transitionTo(state, { ctx, orders, fulfillment });
         fulfillment.state = fsm.currentState;
+        return result;
     }
 
     private initConfig(): StateMachineConfig<FulfillmentState, FulfillmentTransitionData> {

+ 4 - 6
packages/core/src/service/helpers/order-splitter/order-splitter.ts

@@ -7,6 +7,7 @@ import { ConfigService } from '../../../config/index';
 import { TransactionalConnection } from '../../../connection/index';
 import { Channel, Order, OrderItem, OrderLine, ShippingLine, Surcharge } from '../../../entity/index';
 import { ChannelService } from '../../services/channel.service';
+import { OrderService } from '../../services/order.service';
 
 @Injectable()
 export class OrderSplitter {
@@ -14,13 +15,10 @@ export class OrderSplitter {
         private connection: TransactionalConnection,
         private configService: ConfigService,
         private channelService: ChannelService,
+        private orderService: OrderService,
     ) {}
 
-    async createSellerOrders(
-        ctx: RequestContext,
-        order: Order,
-        afterSellerOrderCreated: (sellerOrder: Order) => Promise<any>,
-    ): Promise<Order[]> {
+    async createSellerOrders(ctx: RequestContext, order: Order): Promise<Order[]> {
         const { orderSellerStrategy } = this.configService.orderOptions;
         const partialOrders = await orderSellerStrategy.splitOrder?.(ctx, order);
         if (!partialOrders || partialOrders.length === 0) {
@@ -68,7 +66,7 @@ export class OrderSplitter {
             );
 
             order.sellerOrders.push(sellerOrder);
-            await afterSellerOrderCreated(sellerOrder);
+            await this.orderService.applyPriceAdjustments(ctx, sellerOrder);
         }
         await orderSellerStrategy.afterSellerOrdersCreated?.(ctx, order, order.sellerOrders);
         return order.sellerOrders;

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

@@ -9,6 +9,7 @@ import { validateTransitionDefinition } from '../../../common/finite-state-machi
 import { awaitPromiseOrObservable } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { OrderProcess } from '../../../config/index';
+import { TransactionalConnection } from '../../../connection/index';
 import { Order } from '../../../entity/order/order.entity';
 
 import { OrderState, OrderTransitionData } from './order-state';
@@ -18,7 +19,7 @@ export class OrderStateMachine {
     readonly config: StateMachineConfig<OrderState, OrderTransitionData>;
     private readonly initialState: OrderState = 'Created';
 
-    constructor(private configService: ConfigService) {
+    constructor(private configService: ConfigService, private connection: TransactionalConnection) {
         this.config = this.initConfig();
     }
 
@@ -37,8 +38,9 @@ export class OrderStateMachine {
 
     async transition(ctx: RequestContext, order: Order, state: OrderState) {
         const fsm = new FSM(this.config, order.state);
-        await fsm.transitionTo(state, { ctx, order });
+        const result = await fsm.transitionTo(state, { ctx, order });
         order.state = fsm.currentState;
+        return result;
     }
 
     private initConfig(): StateMachineConfig<OrderState, OrderTransitionData> {

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

@@ -1,5 +1,4 @@
 import { RequestContext } from '../../../api/common/request-context';
-import { Transitions } from '../../../common/finite-state-machine/types';
 import { Order } from '../../../entity/order/order.entity';
 
 /**

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

@@ -37,8 +37,9 @@ export class PaymentStateMachine {
 
     async transition(ctx: RequestContext, order: Order, payment: Payment, state: PaymentState) {
         const fsm = new FSM(this.config, payment.state);
-        await fsm.transitionTo(state, { ctx, order, payment });
+        const result = await fsm.transitionTo(state, { ctx, order, payment });
         payment.state = state;
+        return result;
     }
 
     private initConfig(): StateMachineConfig<PaymentState, PaymentTransitionData> {

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

@@ -49,7 +49,8 @@ export class RefundStateMachine {
 
     async transition(ctx: RequestContext, order: Order, refund: Refund, state: RefundState) {
         const fsm = new FSM(this.config, refund.state);
-        await fsm.transitionTo(state, { ctx, order, refund });
+        const result = await fsm.transitionTo(state, { ctx, order, refund });
         refund.state = state;
+        return result;
     }
 }

+ 4 - 2
packages/core/src/service/services/fulfillment.service.ts

@@ -209,15 +209,17 @@ export class FulfillmentService {
             .where('line.id IN (:...lineIds)', { lineIds })
             .getMany();
         const fromState = fulfillment.state;
+        let finalize: () => Promise<any>;
         try {
-            await this.fulfillmentStateMachine.transition(ctx, fulfillment, orders, state);
+            const result = await this.fulfillmentStateMachine.transition(ctx, fulfillment, orders, state);
+            finalize = result.finalize;
         } catch (e: any) {
             const transitionError = ctx.translate(e.message, { fromState, toState: state });
             return new FulfillmentStateTransitionError({ transitionError, fromState, toState: state });
         }
         await this.connection.getRepository(ctx, Fulfillment).save(fulfillment, { reload: false });
         this.eventBus.publish(new FulfillmentStateTransitionEvent(fromState, state, ctx, fulfillment));
-
+        await finalize();
         return { fulfillment, orders, fromState, toState: state };
     }
 

+ 22 - 130
packages/core/src/service/services/order.service.ts

@@ -108,7 +108,6 @@ import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-build
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
 import { OrderModifier } from '../helpers/order-modifier/order-modifier';
-import { OrderSplitter } from '../helpers/order-splitter/order-splitter';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderStateMachine } from '../helpers/order-state-machine/order-state-machine';
 import { PaymentState } from '../helpers/payment-state-machine/payment-state';
@@ -116,13 +115,7 @@ import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-st
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { TranslatorService } from '../helpers/translator/translator.service';
-import {
-    orderItemsAreAllCancelled,
-    orderItemsAreDelivered,
-    orderItemsAreShipped,
-    orderTotalIsCovered,
-    totalCoveredByPayments,
-} from '../helpers/utils/order-utils';
+import { orderItemsAreAllCancelled, totalCoveredByPayments } from '../helpers/utils/order-utils';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { ChannelService } from './channel.service';
@@ -154,7 +147,6 @@ export class OrderService {
         private shippingCalculator: ShippingCalculator,
         private orderStateMachine: OrderStateMachine,
         private orderMerger: OrderMerger,
-        private orderSplitter: OrderSplitter,
         private paymentService: PaymentService,
         private paymentStateMachine: PaymentStateMachine,
         private paymentMethodService: PaymentMethodService,
@@ -962,20 +954,18 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         order.payments = await this.getOrderPayments(ctx, orderId);
         const fromState = order.state;
-        const fromActiveStatus = order.active;
+        let finalize: () => Promise<any>;
         try {
-            await this.orderStateMachine.transition(ctx, order, state);
+            const result = await this.orderStateMachine.transition(ctx, order, state);
+            finalize = result.finalize;
         } catch (e: any) {
             const transitionError = ctx.translate(e.message, { fromState, toState: state });
             return new OrderStateTransitionError({ transitionError, fromState, toState: state });
         }
-        if (fromActiveStatus === true && order.active === false) {
-            await this.orderSplitter.createSellerOrders(ctx, order, sellerOrder =>
-                this.applyPriceAdjustments(ctx, sellerOrder),
-            );
-        }
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, state, ctx, order));
+        await finalize();
+        await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         return order;
     }
 
@@ -989,23 +979,11 @@ export class OrderService {
         fulfillmentId: ID,
         state: FulfillmentState,
     ): Promise<Fulfillment | FulfillmentStateTransitionError> {
-        // TODO: v2: Extract this into a user-configurable area, i.e. CustomFulfillmentProcess,
-        // so that users are able to opt-out of such hard-coded transformations that rely on
-        // the default order states
         const result = await this.fulfillmentService.transitionToState(ctx, fulfillmentId, state);
         if (isGraphQlErrorResult(result)) {
             return result;
         }
-        const { fulfillment, fromState, toState, orders } = result;
-        if (toState === 'Cancelled') {
-            await this.stockMovementService.createCancellationsForOrderItems(ctx, fulfillment.orderItems);
-            const lines = await this.groupOrderItemsIntoLines(ctx, fulfillment.orderItems);
-            await this.stockMovementService.createAllocationsForOrderLines(ctx, lines);
-        }
-        await Promise.all(
-            orders.map(order => this.handleFulfillmentStateTransitByOrder(ctx, order, fromState, toState)),
-        );
-        return fulfillment;
+        return result.fulfillment;
     }
 
     /**
@@ -1049,35 +1027,6 @@ export class OrderService {
         return this.getOrderOrThrow(ctx, input.orderId);
     }
 
-    private async handleFulfillmentStateTransitByOrder(
-        ctx: RequestContext,
-        order: Order,
-        fromState: FulfillmentState,
-        toState: FulfillmentState,
-    ): Promise<void> {
-        const nextOrderStates = this.getNextOrderStates(order);
-
-        const transitionOrderIfStateAvailable = (state: OrderState) =>
-            nextOrderStates.includes(state) && this.transitionToState(ctx, order.id, state);
-
-        if (toState === 'Shipped') {
-            const orderWithFulfillment = await this.getOrderWithFulfillments(ctx, order.id);
-            if (orderItemsAreShipped(orderWithFulfillment)) {
-                await transitionOrderIfStateAvailable('Shipped');
-            } else {
-                await transitionOrderIfStateAvailable('PartiallyShipped');
-            }
-        }
-        if (toState === 'Delivered') {
-            const orderWithFulfillment = await this.getOrderWithFulfillments(ctx, order.id);
-            if (orderItemsAreDelivered(orderWithFulfillment)) {
-                await transitionOrderIfStateAvailable('Delivered');
-            } else {
-                await transitionOrderIfStateAvailable('PartiallyDelivered');
-            }
-        }
-    }
-
     /**
      * @description
      * Transitions the given {@link Payment} to a new state. If the order totalWithTax price is then
@@ -1093,11 +1042,6 @@ export class OrderService {
         if (isGraphQlErrorResult(result)) {
             return result;
         }
-        const order = await this.findOne(ctx, result.order.id);
-        if (order) {
-            order.payments = await this.getOrderPayments(ctx, order.id);
-            await this.transitionOrderIfTotalIsCovered(ctx, order);
-        }
         return result;
     }
 
@@ -1129,9 +1073,12 @@ export class OrderService {
             return payment;
         }
 
-        const existingPayments = await this.getOrderPayments(ctx, orderId);
-        order.payments = [...existingPayments, payment];
-        await this.connection.getRepository(ctx, Order).save(order, { reload: false });
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder()
+            .relation('payments')
+            .of(order)
+            .add(payment);
 
         if (payment.state === 'Error') {
             return new PaymentFailedError({ paymentErrorMessage: payment.errorMessage || '' });
@@ -1140,7 +1087,7 @@ export class OrderService {
             return new PaymentDeclinedError({ paymentErrorMessage: payment.errorMessage || '' });
         }
 
-        return this.transitionOrderIfTotalIsCovered(ctx, order);
+        return assertFound(this.findOne(ctx, order.id));
     }
 
     /**
@@ -1164,28 +1111,6 @@ export class OrderService {
         return canTransitionToPaymentAuthorized && canTransitionToPaymentSettled;
     }
 
-    /**
-     * TODO: v2: Extract this into a user-configurable area, i.e. CustomPaymentProcess,
-     * so that users are able to opt-out of such hard-coded transformations that rely on
-     * the default order states
-     * @param ctx
-     * @param order
-     * @private
-     */
-    private async transitionOrderIfTotalIsCovered(
-        ctx: RequestContext,
-        order: Order,
-    ): Promise<Order | OrderStateTransitionError> {
-        const orderId = order.id;
-        if (orderTotalIsCovered(order, 'Settled') && order.state !== 'PaymentSettled') {
-            return this.transitionToState(ctx, orderId, 'PaymentSettled');
-        }
-        if (orderTotalIsCovered(order, ['Authorized', 'Settled']) && order.state !== 'PaymentAuthorized') {
-            return this.transitionToState(ctx, orderId, 'PaymentAuthorized');
-        }
-        return order;
-    }
-
     /**
      * @description
      * This method is used after modifying an existing completed order using the `modifyOrder()` method. If the modifications
@@ -1224,7 +1149,7 @@ export class OrderService {
             modification.payment = payment;
             await this.connection.getRepository(ctx, OrderModification).save(modification);
         }
-        return order;
+        return assertFound(this.findOne(ctx, order.id));
     }
 
     /**
@@ -1241,14 +1166,6 @@ export class OrderService {
             if (payment.state !== 'Settled') {
                 return new SettlePaymentError({ paymentErrorMessage: payment.errorMessage || '' });
             }
-            const order = await this.findOne(ctx, payment.order.id);
-            if (order) {
-                order.payments = await this.getOrderPayments(ctx, order.id);
-                const orderTransitionResult = await this.transitionOrderIfTotalIsCovered(ctx, order);
-                if (isGraphQlErrorResult(orderTransitionResult)) {
-                    return orderTransitionResult;
-                }
-            }
         }
         return payment;
     }
@@ -1576,8 +1493,14 @@ export class OrderService {
         refund.transactionId = input.transactionId;
         const fromState = refund.state;
         const toState = 'Settled';
-        await this.refundStateMachine.transition(ctx, refund.payment.order, refund, toState);
+        const { finalize } = await this.refundStateMachine.transition(
+            ctx,
+            refund.payment.order,
+            refund,
+            toState,
+        );
         await this.connection.getRepository(ctx, Refund).save(refund);
+        await finalize();
         this.eventBus.publish(
             new RefundStateTransitionEvent(fromState, toState, ctx, refund, refund.payment.order),
         );
@@ -1899,12 +1822,6 @@ export class OrderService {
         return assertFound(this.findOne(ctx, order.id));
     }
 
-    private async getOrderWithFulfillments(ctx: RequestContext, orderId: ID): Promise<Order> {
-        return await this.connection.getEntityOrThrow(ctx, Order, orderId, {
-            relations: ['lines', 'lines.items', 'lines.items.fulfillments'],
-        });
-    }
-
     private async getOrdersAndItemsFromLines(
         ctx: RequestContext,
         orderLinesInput: OrderLineInput[],
@@ -1964,29 +1881,4 @@ export class OrderService {
         }
         return merged;
     }
-
-    private async groupOrderItemsIntoLines(
-        ctx: RequestContext,
-        orderItems: OrderItem[],
-    ): Promise<Array<{ orderLine: OrderLine; quantity: number }>> {
-        const orderLineIdQuantityMap = new Map<ID, number>();
-        for (const item of orderItems) {
-            const quantity = orderLineIdQuantityMap.get(item.lineId);
-            if (quantity == null) {
-                orderLineIdQuantityMap.set(item.lineId, 1);
-            } else {
-                orderLineIdQuantityMap.set(item.lineId, quantity + 1);
-            }
-        }
-        const orderLines = await this.connection
-            .getRepository(ctx, OrderLine)
-            .findByIds([...orderLineIdQuantityMap.keys()], {
-                relations: ['productVariant'],
-            });
-        return orderLines.map(orderLine => ({
-            orderLine,
-            // tslint:disable-next-line:no-non-null-assertion
-            quantity: orderLineIdQuantityMap.get(orderLine.id)!,
-        }));
-    }
 }

+ 29 - 4
packages/core/src/service/services/payment.service.ts

@@ -125,11 +125,18 @@ export class PaymentService {
         const payment = await this.connection
             .getRepository(ctx, Payment)
             .save(new Payment({ ...result, method, state: initialState }));
-        await this.paymentStateMachine.transition(ctx, order, payment, result.state);
+        const { finalize } = await this.paymentStateMachine.transition(ctx, order, payment, result.state);
         await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder()
+            .relation('payments')
+            .of(order)
+            .add(payment);
         this.eventBus.publish(
             new PaymentStateTransitionEvent(initialState, result.state, ctx, payment, order),
         );
+        await finalize();
         return payment;
     }
 
@@ -206,8 +213,10 @@ export class PaymentService {
             await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
             return payment;
         }
+        let finalize: () => Promise<any>;
         try {
-            await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
+            const result = await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
+            finalize = result.finalize;
         } catch (e: any) {
             const transitionError = ctx.translate(e.message, { fromState, toState });
             return new PaymentStateTransitionError({ transitionError, fromState, toState });
@@ -216,6 +225,7 @@ export class PaymentService {
         this.eventBus.publish(
             new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
         );
+        await finalize();
         return payment;
     }
 
@@ -240,9 +250,16 @@ export class PaymentService {
                 state: initialState,
             }),
         );
-        await this.paymentStateMachine.transition(ctx, order, payment, endState);
+        const { finalize } = await this.paymentStateMachine.transition(ctx, order, payment, endState);
         await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
+        await this.connection
+            .getRepository(ctx, Order)
+            .createQueryBuilder()
+            .relation('payments')
+            .of(order)
+            .add(payment);
         this.eventBus.publish(new PaymentStateTransitionEvent(initialState, endState, ctx, payment, order));
+        await finalize();
         return payment;
     }
 
@@ -340,9 +357,16 @@ export class PaymentService {
             }
             refund = await this.connection.getRepository(ctx, Refund).save(refund);
             if (createRefundResult) {
+                let finalize: () => Promise<any>;
                 const fromState = refund.state;
                 try {
-                    await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
+                    const result = await this.refundStateMachine.transition(
+                        ctx,
+                        order,
+                        refund,
+                        createRefundResult.state,
+                    );
+                    finalize = result.finalize;
                 } catch (e: any) {
                     return new RefundStateTransitionError({
                         transitionError: e.message,
@@ -351,6 +375,7 @@ export class PaymentService {
                     });
                 }
                 await this.connection.getRepository(ctx, Refund).save(refund, { reload: false });
+                await finalize();
                 this.eventBus.publish(
                     new RefundStateTransitionEvent(fromState, createRefundResult.state, ctx, refund, order),
                 );