Sfoglia il codice sorgente

feat(core): Extract hard-coded payment state & process

This commit makes it possible to completely configure the payment process by extracting all
transition validation and allowing the developer to replace with custom logic.
Michael Bromley 3 anni fa
parent
commit
4c5c9460aa

+ 2 - 1
packages/core/src/config/config.module.ts

@@ -95,7 +95,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             process: fulfillmentProcess,
             shippingLineAssignmentStrategy,
         } = this.configService.shippingOptions;
-        const { customPaymentProcess } = this.configService.paymentOptions;
+        const { customPaymentProcess, process: paymentProcess } = this.configService.paymentOptions;
         const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
         const { entityIdStrategy } = this.configService.entityOptions;
         const { healthChecks } = this.configService.systemOptions;
@@ -125,6 +125,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             ...customFulfillmentProcess,
             ...fulfillmentProcess,
             ...customPaymentProcess,
+            ...paymentProcess,
             stockAllocationStrategy,
             stockDisplayStrategy,
             ...healthChecks,

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -34,6 +34,7 @@ import { MergeOrdersStrategy } from './order/merge-orders-strategy';
 import { DefaultOrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { DefaultOrderCodeStrategy } from './order/order-code-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
+import { defaultPaymentProcess } from './payment/default-payment-process';
 import { defaultPromotionActions, defaultPromotionConditions } from './promotion';
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
@@ -152,6 +153,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         paymentMethodEligibilityCheckers: [],
         paymentMethodHandlers: [],
         customPaymentProcess: [],
+        process: [defaultPaymentProcess],
     },
     taxOptions: {
         taxZoneStrategy: new DefaultTaxZoneStrategy(),

+ 2 - 1
packages/core/src/config/index.ts

@@ -50,7 +50,8 @@ export * from './order/stock-allocation-strategy';
 export * from './order/use-existing-strategy';
 export * from './order/use-guest-if-existing-empty-strategy';
 export * from './order/use-guest-strategy';
-export * from './payment/custom-payment-process';
+export * from './payment/payment-process';
+export * from './payment/default-payment-process';
 export * from './payment/dummy-payment-method-handler';
 export * from './payment/example-payment-method-handler';
 export * from './payment/payment-method-eligibility-checker';

+ 69 - 0
packages/core/src/config/payment/default-payment-process.ts

@@ -0,0 +1,69 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+
+import { awaitPromiseOrObservable, Transitions } from '../../common/index';
+import { FulfillmentState, PaymentState } from '../../service/index';
+
+import { PaymentProcess } from './payment-process';
+
+declare module '../../service/helpers/payment-state-machine/payment-state' {
+    interface PaymentStates {
+        Authorized: never;
+        Settled: never;
+        Declined: never;
+    }
+}
+
+let configService: import('../config.service').ConfigService;
+let historyService: import('../../service/index').HistoryService;
+
+/**
+ * @description
+ * The default {@link PaymentProcess}
+ *
+ * @docsCategory payment
+ */
+export const defaultPaymentProcess: PaymentProcess<PaymentState> = {
+    transitions: {
+        Created: {
+            to: ['Authorized', 'Settled', 'Declined', 'Error', 'Cancelled'],
+        },
+        Authorized: {
+            to: ['Settled', 'Error', 'Cancelled'],
+        },
+        Settled: {
+            to: ['Cancelled'],
+        },
+        Declined: {
+            to: ['Cancelled'],
+        },
+        Error: {
+            to: ['Cancelled'],
+        },
+        Cancelled: {
+            to: [],
+        },
+    },
+    async init(injector) {
+        // Lazily import these services to avoid a circular dependency error
+        // 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);
+        configService = injector.get(ConfigService);
+        historyService = injector.get(HistoryService);
+    },
+    async onTransitionStart(fromState, toState, data) {
+        // nothing here by default
+    },
+    async onTransitionEnd(fromState, toState, data) {
+        await historyService.createHistoryEntryForOrder({
+            ctx: data.ctx,
+            orderId: data.order.id,
+            type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
+            data: {
+                paymentId: data.payment.id,
+                from: fromState,
+                to: toState,
+            },
+        });
+    },
+};

+ 18 - 4
packages/core/src/config/payment/custom-payment-process.ts → packages/core/src/config/payment/payment-process.ts

@@ -13,16 +13,30 @@ import {
 
 /**
  * @description
- * Used to define extensions to or modifications of the default payment process.
+ * A PaymentProcess is used to define the way the payment process works as in: what states a Payment can be
+ * in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, a
+ * PaymentProcess can perform checks before allowing a state transition to occur, and the `onTransitionEnd()`
+ * hook allows logic to be executed after a state change.
  *
  * For detailed description of the interface members, see the {@link StateMachineConfig} docs.
  *
- * @docsCategory fulfillment
+ * @docsCategory payment
+ * @since 2.0.0
  */
-export interface CustomPaymentProcess<State extends keyof CustomPaymentStates | string>
-    extends InjectableStrategy {
+export interface PaymentProcess<State extends keyof CustomPaymentStates | string> extends InjectableStrategy {
     transitions?: Transitions<State, State | PaymentState> & Partial<Transitions<PaymentState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | PaymentState, PaymentTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | PaymentState, PaymentTransitionData>;
     onTransitionError?: OnTransitionErrorFn<State | PaymentState>;
 }
+
+/**
+ * @description
+ * Used to define extensions to or modifications of the default payment process.
+ *
+ * For detailed description of the interface members, see the {@link StateMachineConfig} docs.
+ *
+ * @deprecated use PaymentProcess
+ */
+export interface CustomPaymentProcess<State extends keyof CustomPaymentStates | string>
+    extends PaymentProcess<State> {}

+ 11 - 3
packages/core/src/config/vendure-config.ts

@@ -36,9 +36,9 @@ import { OrderPlacedStrategy } from './order/order-placed-strategy';
 import { OrderProcess } from './order/order-process';
 import { OrderSellerStrategy } from './order/order-seller-strategy';
 import { StockAllocationStrategy } from './order/stock-allocation-strategy';
-import { CustomPaymentProcess } from './payment/custom-payment-process';
 import { PaymentMethodEligibilityChecker } from './payment/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from './payment/payment-method-handler';
+import { PaymentProcess } from './payment/payment-process';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionCondition } from './promotion/promotion-condition';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
@@ -705,6 +705,7 @@ export interface ShippingOptions {
      * Takes an array of objects implementing the {@link FulfillmentProcess} interface.
      *
      * @since 2.0.0
+     * @default defaultFulfillmentProcess
      */
     process?: Array<FulfillmentProcess<any>>;
     /**
@@ -756,12 +757,19 @@ export interface PaymentOptions {
      * {@link PaymentMethod}s
      */
     paymentMethodEligibilityCheckers?: PaymentMethodEligibilityChecker[];
+    /**
+     * @deprecated use `process`
+     */
+    customPaymentProcess?: Array<PaymentProcess<any>>;
     /**
      * @description
      * Allows the definition of custom states and transition logic for the payment process state machine.
-     * Takes an array of objects implementing the {@link CustomPaymentProcess} interface.
+     * Takes an array of objects implementing the {@link PaymentProcess} interface.
+     *
+     * @default defaultPaymentProcess
+     * @since 2.0.0
      */
-    customPaymentProcess?: Array<CustomPaymentProcess<any>>;
+    process?: Array<PaymentProcess<any>>;
 }
 
 /**

+ 8 - 40
packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts

@@ -1,5 +1,4 @@
 import { Injectable } from '@nestjs/common';
-import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
@@ -11,16 +10,15 @@ import { awaitPromiseOrObservable } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
-import { HistoryService } from '../../services/history.service';
 
-import { PaymentState, paymentStateTransitions, PaymentTransitionData } from './payment-state';
+import { PaymentState, PaymentTransitionData } from './payment-state';
 
 @Injectable()
 export class PaymentStateMachine {
     private readonly config: StateMachineConfig<PaymentState, PaymentTransitionData>;
     private readonly initialState: PaymentState = 'Created';
 
-    constructor(private configService: ConfigService, private historyService: HistoryService) {
+    constructor(private configService: ConfigService) {
         this.config = this.initConfig();
     }
 
@@ -43,42 +41,14 @@ export class PaymentStateMachine {
         payment.state = state;
     }
 
-    /**
-     * Specific business logic to be executed on Payment state transitions.
-     */
-    private async onTransitionStart(
-        fromState: PaymentState,
-        toState: PaymentState,
-        data: PaymentTransitionData,
-    ) {
-        /**/
-    }
-
-    private async onTransitionEnd(
-        fromState: PaymentState,
-        toState: PaymentState,
-        data: PaymentTransitionData,
-    ) {
-        await this.historyService.createHistoryEntryForOrder({
-            ctx: data.ctx,
-            orderId: data.order.id,
-            type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
-            data: {
-                paymentId: data.payment.id,
-                from: fromState,
-                to: toState,
-            },
-        });
-    }
-
     private initConfig(): StateMachineConfig<PaymentState, PaymentTransitionData> {
         const { paymentMethodHandlers } = this.configService.paymentOptions;
         const customProcesses = this.configService.paymentOptions.customPaymentProcess ?? [];
-
-        const allTransitions = customProcesses.reduce(
+        const processes = [...customProcesses, ...(this.configService.paymentOptions.process ?? [])];
+        const allTransitions = processes.reduce(
             (transitions, process) =>
                 mergeTransitionDefinitions(transitions, process.transitions as Transitions<any>),
-            paymentStateTransitions,
+            {} as Transitions<PaymentState>,
         );
 
         validateTransitionDefinition(allTransitions, this.initialState);
@@ -86,7 +56,7 @@ export class PaymentStateMachine {
         return {
             transitions: allTransitions,
             onTransitionStart: async (fromState, toState, data) => {
-                for (const process of customProcesses) {
+                for (const process of processes) {
                     if (typeof process.onTransitionStart === 'function') {
                         const result = await awaitPromiseOrObservable(
                             process.onTransitionStart(fromState, toState, data),
@@ -106,18 +76,16 @@ export class PaymentStateMachine {
                         }
                     }
                 }
-                return this.onTransitionStart(fromState, toState, data);
             },
             onTransitionEnd: async (fromState, toState, data) => {
-                for (const process of customProcesses) {
+                for (const process of processes) {
                     if (typeof process.onTransitionEnd === 'function') {
                         await awaitPromiseOrObservable(process.onTransitionEnd(fromState, toState, data));
                     }
                 }
-                await this.onTransitionEnd(fromState, toState, data);
             },
             onError: async (fromState, toState, message) => {
-                for (const process of customProcesses) {
+                for (const process of processes) {
                     if (typeof process.onTransitionError === 'function') {
                         await awaitPromiseOrObservable(
                             process.onTransitionError(fromState, toState, message),

+ 11 - 27
packages/core/src/service/helpers/payment-state-machine/payment-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';
 import { Payment } from '../../../entity/payment/payment.entity';
 
@@ -7,10 +6,18 @@ import { Payment } from '../../../entity/payment/payment.entity';
  * @description
  * An interface to extend standard {@link PaymentState}.
  *
- * @docsCategory payment
+ * @deprecated use PaymentStates
  */
 export interface CustomPaymentStates {}
 
+/**
+ * @description
+ * An interface to extend standard {@link PaymentState}.
+ *
+ * @docsCategory payment
+ */
+export interface PaymentStates {}
+
 /**
  * @description
  * These are the default states of the payment process.
@@ -19,33 +26,10 @@ export interface CustomPaymentStates {}
  */
 export type PaymentState =
     | 'Created'
-    | 'Authorized'
-    | 'Settled'
-    | 'Declined'
     | 'Error'
     | 'Cancelled'
-    | keyof CustomPaymentStates;
-
-export const paymentStateTransitions: Transitions<PaymentState> = {
-    Created: {
-        to: ['Authorized', 'Settled', 'Declined', 'Error', 'Cancelled'],
-    },
-    Authorized: {
-        to: ['Settled', 'Error', 'Cancelled'],
-    },
-    Settled: {
-        to: ['Cancelled'],
-    },
-    Declined: {
-        to: ['Cancelled'],
-    },
-    Error: {
-        to: ['Cancelled'],
-    },
-    Cancelled: {
-        to: [],
-    },
-};
+    | keyof CustomPaymentStates
+    | keyof PaymentStates;
 
 /**
  * @description