Browse Source

fix(core): Publish state transition events after persisting entities

Relates to #245. Previously, the events would be published *prior* to the associated Order etc. entities being persisted to the DB. This meant that any event subscribers which then attempted to update that Order entity would have their changes clobbered by the subsequent call to `.save()`, resulting in vexing bugs.
Michael Bromley 6 years ago
parent
commit
005a553575

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

@@ -6,8 +6,6 @@ import { IllegalOperationError } from '../../../common/error/errors';
 import { FSM, StateMachineConfig, Transitions } from '../../../common/finite-state-machine';
 import { FSM, StateMachineConfig, Transitions } from '../../../common/finite-state-machine';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
-import { EventBus } from '../../../event-bus/event-bus';
-import { OrderStateTransitionEvent } from '../../../event-bus/events/order-state-transition-event';
 import { HistoryService } from '../../services/history.service';
 import { HistoryService } from '../../services/history.service';
 import { PromotionService } from '../../services/promotion.service';
 import { PromotionService } from '../../services/promotion.service';
 import { StockMovementService } from '../../services/stock-movement.service';
 import { StockMovementService } from '../../services/stock-movement.service';
@@ -24,7 +22,6 @@ export class OrderStateMachine {
         private stockMovementService: StockMovementService,
         private stockMovementService: StockMovementService,
         private historyService: HistoryService,
         private historyService: HistoryService,
         private promotionService: PromotionService,
         private promotionService: PromotionService,
-        private eventBus: EventBus,
     ) {
     ) {
         this.config = this.initConfig();
         this.config = this.initConfig();
     }
     }
@@ -75,7 +72,6 @@ export class OrderStateMachine {
         if (toState === 'Cancelled') {
         if (toState === 'Cancelled') {
             data.order.active = false;
             data.order.active = false;
         }
         }
-        this.eventBus.publish(new OrderStateTransitionEvent(fromState, toState, data.ctx, data.order));
         await this.historyService.createHistoryEntryForOrder({
         await this.historyService.createHistoryEntryForOrder({
             orderId: data.order.id,
             orderId: data.order.id,
             type: HistoryEntryType.ORDER_STATE_TRANSITION,
             type: HistoryEntryType.ORDER_STATE_TRANSITION,

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

@@ -7,22 +7,18 @@ import { FSM, StateMachineConfig } from '../../../common/finite-state-machine';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
-import { EventBus } from '../../../event-bus/event-bus';
-import { PaymentStateTransitionEvent } from '../../../event-bus/events/payment-state-transition-event';
 import { HistoryService } from '../../services/history.service';
 import { HistoryService } from '../../services/history.service';
 
 
 import { PaymentState, paymentStateTransitions, PaymentTransitionData } from './payment-state';
 import { PaymentState, paymentStateTransitions, PaymentTransitionData } from './payment-state';
 
 
 @Injectable()
 @Injectable()
 export class PaymentStateMachine {
 export class PaymentStateMachine {
-
     private readonly config: StateMachineConfig<PaymentState, PaymentTransitionData> = {
     private readonly config: StateMachineConfig<PaymentState, PaymentTransitionData> = {
         transitions: paymentStateTransitions,
         transitions: paymentStateTransitions,
         onTransitionStart: async (fromState, toState, data) => {
         onTransitionStart: async (fromState, toState, data) => {
             return true;
             return true;
         },
         },
         onTransitionEnd: async (fromState, toState, data) => {
         onTransitionEnd: async (fromState, toState, data) => {
-            this.eventBus.publish(new PaymentStateTransitionEvent(fromState, toState, data.ctx, data.payment, data.order));
             await this.historyService.createHistoryEntryForOrder({
             await this.historyService.createHistoryEntryForOrder({
                 ctx: data.ctx,
                 ctx: data.ctx,
                 orderId: data.order.id,
                 orderId: data.order.id,
@@ -42,9 +38,7 @@ export class PaymentStateMachine {
         },
         },
     };
     };
 
 
-    constructor(private configService: ConfigService,
-                private historyService: HistoryService,
-                private eventBus: EventBus) {}
+    constructor(private configService: ConfigService, private historyService: HistoryService) {}
 
 
     getNextStates(payment: Payment): PaymentState[] {
     getNextStates(payment: Payment): PaymentState[] {
         const fsm = new FSM(this.config, payment.state);
         const fsm = new FSM(this.config, payment.state);

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

@@ -7,22 +7,18 @@ import { FSM, StateMachineConfig } from '../../../common/finite-state-machine';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
-import { EventBus } from '../../../event-bus/event-bus';
-import { RefundStateTransitionEvent } from '../../../event-bus/events/refund-state-transition-event';
 import { HistoryService } from '../../services/history.service';
 import { HistoryService } from '../../services/history.service';
 
 
 import { RefundState, refundStateTransitions, RefundTransitionData } from './refund-state';
 import { RefundState, refundStateTransitions, RefundTransitionData } from './refund-state';
 
 
 @Injectable()
 @Injectable()
 export class RefundStateMachine {
 export class RefundStateMachine {
-
     private readonly config: StateMachineConfig<RefundState, RefundTransitionData> = {
     private readonly config: StateMachineConfig<RefundState, RefundTransitionData> = {
         transitions: refundStateTransitions,
         transitions: refundStateTransitions,
         onTransitionStart: async (fromState, toState, data) => {
         onTransitionStart: async (fromState, toState, data) => {
             return true;
             return true;
         },
         },
         onTransitionEnd: async (fromState, toState, data) => {
         onTransitionEnd: async (fromState, toState, data) => {
-            this.eventBus.publish(new RefundStateTransitionEvent(fromState, toState, data.ctx, data.refund, data.order));
             await this.historyService.createHistoryEntryForOrder({
             await this.historyService.createHistoryEntryForOrder({
                 ctx: data.ctx,
                 ctx: data.ctx,
                 orderId: data.order.id,
                 orderId: data.order.id,
@@ -43,9 +39,7 @@ export class RefundStateMachine {
         },
         },
     };
     };
 
 
-    constructor(private configService: ConfigService,
-                private historyService: HistoryService,
-                private eventBus: EventBus) {}
+    constructor(private configService: ConfigService, private historyService: HistoryService) {}
 
 
     getNextStates(refund: Refund): RefundState[] {
     getNextStates(refund: Refund): RefundState[] {
         const fsm = new FSM(this.config, refund.state);
         const fsm = new FSM(this.config, refund.state);

+ 21 - 3
packages/core/src/service/services/order.service.ts

@@ -38,6 +38,10 @@ import { ProductVariant } from '../../entity/product-variant/product-variant.ent
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Refund } from '../../entity/refund/refund.entity';
 import { Refund } from '../../entity/refund/refund.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
+import { EventBus } from '../../event-bus/event-bus';
+import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
+import { PaymentStateTransitionEvent } from '../../event-bus/events/payment-state-transition-event';
+import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
@@ -76,6 +80,7 @@ export class OrderService {
         private refundStateMachine: RefundStateMachine,
         private refundStateMachine: RefundStateMachine,
         private historyService: HistoryService,
         private historyService: HistoryService,
         private promotionService: PromotionService,
         private promotionService: PromotionService,
+        private eventBus: EventBus,
     ) {}
     ) {}
 
 
     findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
     findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
@@ -378,8 +383,10 @@ export class OrderService {
 
 
     async transitionToState(ctx: RequestContext, orderId: ID, state: OrderState): Promise<Order> {
     async transitionToState(ctx: RequestContext, orderId: ID, state: OrderState): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
+        const fromState = order.state;
         await this.orderStateMachine.transition(ctx, order, state);
         await this.orderStateMachine.transition(ctx, order, state);
         await this.connection.getRepository(Order).save(order, { reload: false });
         await this.connection.getRepository(Order).save(order, { reload: false });
+        this.eventBus.publish(new OrderStateTransitionEvent(fromState, state, ctx, order));
         return order;
         return order;
     }
     }
 
 
@@ -423,9 +430,14 @@ export class OrderService {
         const payment = await getEntityOrThrow(this.connection, Payment, paymentId, { relations: ['order'] });
         const payment = await getEntityOrThrow(this.connection, Payment, paymentId, { relations: ['order'] });
         const settlePaymentResult = await this.paymentMethodService.settlePayment(payment, payment.order);
         const settlePaymentResult = await this.paymentMethodService.settlePayment(payment, payment.order);
         if (settlePaymentResult.success) {
         if (settlePaymentResult.success) {
-            await this.paymentStateMachine.transition(ctx, payment.order, payment, 'Settled');
+            const fromState = payment.state;
+            const toState = 'Settled';
+            await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
             payment.metadata = { ...payment.metadata, ...settlePaymentResult.metadata };
             payment.metadata = { ...payment.metadata, ...settlePaymentResult.metadata };
             await this.connection.getRepository(Payment).save(payment, { reload: false });
             await this.connection.getRepository(Payment).save(payment, { reload: false });
+            this.eventBus.publish(
+                new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
+            );
             if (payment.amount === payment.order.total) {
             if (payment.amount === payment.order.total) {
                 await this.transitionToState(ctx, payment.order.id, 'PaymentSettled');
                 await this.transitionToState(ctx, payment.order.id, 'PaymentSettled');
             }
             }
@@ -642,8 +654,14 @@ export class OrderService {
             relations: ['payment', 'payment.order'],
             relations: ['payment', 'payment.order'],
         });
         });
         refund.transactionId = input.transactionId;
         refund.transactionId = input.transactionId;
-        await this.refundStateMachine.transition(ctx, refund.payment.order, refund, 'Settled');
-        return this.connection.getRepository(Refund).save(refund);
+        const fromState = refund.state;
+        const toState = 'Settled';
+        await this.refundStateMachine.transition(ctx, refund.payment.order, refund, toState);
+        await this.connection.getRepository(Refund).save(refund);
+        this.eventBus.publish(
+            new RefundStateTransitionEvent(fromState, toState, ctx, refund, refund.payment.order),
+        );
+        return refund;
     }
     }
 
 
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {

+ 13 - 3
packages/core/src/service/services/payment-method.service.ts

@@ -9,7 +9,6 @@ import { Connection } from 'typeorm';
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
-import { getConfig } from '../../config/config-helpers';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import {
 import {
     PaymentMethodArgs,
     PaymentMethodArgs,
@@ -17,11 +16,13 @@ import {
     PaymentMethodHandler,
     PaymentMethodHandler,
 } from '../../config/payment-method/payment-method-handler';
 } from '../../config/payment-method/payment-method-handler';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 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 { Order } from '../../entity/order/order.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
 import { Refund } from '../../entity/refund/refund.entity';
 import { Refund } from '../../entity/refund/refund.entity';
+import { EventBus } from '../../event-bus/event-bus';
+import { PaymentStateTransitionEvent } from '../../event-bus/events/payment-state-transition-event';
+import { RefundStateTransitionEvent } from '../../event-bus/events/refund-state-transition-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
 import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
@@ -36,6 +37,7 @@ export class PaymentMethodService {
         private listQueryBuilder: ListQueryBuilder,
         private listQueryBuilder: ListQueryBuilder,
         private paymentStateMachine: PaymentStateMachine,
         private paymentStateMachine: PaymentStateMachine,
         private refundStateMachine: RefundStateMachine,
         private refundStateMachine: RefundStateMachine,
+        private eventBus: EventBus,
     ) {}
     ) {}
 
 
     async initPaymentMethods() {
     async initPaymentMethods() {
@@ -78,11 +80,15 @@ export class PaymentMethodService {
     ): Promise<Payment> {
     ): Promise<Payment> {
         const { paymentMethod, handler } = await this.getMethodAndHandler(method);
         const { paymentMethod, handler } = await this.getMethodAndHandler(method);
         const result = await handler.createPayment(order, paymentMethod.configArgs, metadata || {});
         const result = await handler.createPayment(order, paymentMethod.configArgs, metadata || {});
+        const initialState = 'Created';
         const payment = await this.connection
         const payment = await this.connection
             .getRepository(Payment)
             .getRepository(Payment)
-            .save(new Payment({ ...result, state: 'Created' }));
+            .save(new Payment({ ...result, state: initialState }));
         await this.paymentStateMachine.transition(ctx, order, payment, result.state);
         await this.paymentStateMachine.transition(ctx, order, payment, result.state);
         await this.connection.getRepository(Payment).save(payment, { reload: false });
         await this.connection.getRepository(Payment).save(payment, { reload: false });
+        this.eventBus.publish(
+            new PaymentStateTransitionEvent(initialState, result.state, ctx, payment, payment.order),
+        );
         return payment;
         return payment;
     }
     }
 
 
@@ -126,8 +132,12 @@ export class PaymentMethodService {
         }
         }
         refund = await this.connection.getRepository(Refund).save(refund);
         refund = await this.connection.getRepository(Refund).save(refund);
         if (createRefundResult) {
         if (createRefundResult) {
+            const fromState = refund.state;
             await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
             await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
             await this.connection.getRepository(Refund).save(refund, { reload: false });
             await this.connection.getRepository(Refund).save(refund, { reload: false });
+            this.eventBus.publish(
+                new RefundStateTransitionEvent(fromState, createRefundResult.state, ctx, refund, order),
+            );
         }
         }
         return refund;
         return refund;
     }
     }