Browse Source

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

This commit makes it possible to completely configure the fulfillment process by extracting all
transition validation and allowing the developer to replace with custom logic.
Michael Bromley 3 years ago
parent
commit
cdb2b75b3c

+ 2 - 1
packages/core/e2e/fulfillment-process.e2e-spec.ts

@@ -1,6 +1,7 @@
 /* tslint:disable:no-non-null-assertion */
 import {
     CustomFulfillmentProcess,
+    defaultFulfillmentProcess,
     manualFulfillmentHandler,
     mergeConfig,
     TransactionalConnection,
@@ -79,7 +80,7 @@ describe('Fulfillment process', () => {
         mergeConfig(testConfig(), {
             shippingOptions: {
                 ...testConfig().shippingOptions,
-                customFulfillmentProcess: [customOrderProcess as any, customOrderProcess2 as any],
+                process: [defaultFulfillmentProcess, customOrderProcess as any, customOrderProcess2 as any],
             },
             paymentOptions: {
                 paymentMethodHandlers: [testSuccessfulPaymentMethod],

+ 8 - 4
packages/core/src/config/config.module.ts

@@ -82,7 +82,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             mergeStrategy,
             checkoutMergeStrategy,
             orderItemPriceCalculationStrategy,
-            process,
+            process: orderProcess,
             orderCodeStrategy,
             orderByCodeAccessStrategy,
             stockAllocationStrategy,
@@ -90,8 +90,11 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             changedPriceHandlingStrategy,
             orderSellerStrategy,
         } = this.configService.orderOptions;
-        const { customFulfillmentProcess, shippingLineAssignmentStrategy } =
-            this.configService.shippingOptions;
+        const {
+            customFulfillmentProcess,
+            process: fulfillmentProcess,
+            shippingLineAssignmentStrategy,
+        } = this.configService.shippingOptions;
         const { customPaymentProcess } = this.configService.paymentOptions;
         const { entityIdStrategy: entityIdStrategyDeprecated } = this.configService;
         const { entityIdStrategy } = this.configService.entityOptions;
@@ -118,8 +121,9 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             ...[entityIdStrategy].filter(notNullOrUndefined),
             productVariantPriceCalculationStrategy,
             orderItemPriceCalculationStrategy,
-            ...process,
+            ...orderProcess,
             ...customFulfillmentProcess,
+            ...fulfillmentProcess,
             ...customPaymentProcess,
             stockAllocationStrategy,
             stockDisplayStrategy,

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

@@ -20,6 +20,7 @@ import { defaultCollectionFilters } from './catalog/default-collection-filters';
 import { DefaultProductVariantPriceCalculationStrategy } from './catalog/default-product-variant-price-calculation-strategy';
 import { DefaultStockDisplayStrategy } from './catalog/default-stock-display-strategy';
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
+import { defaultFulfillmentProcess } from './fulfillment/default-fulfillment-process';
 import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handler';
 import { DefaultLogger } from './logger/default-logger';
 import { DefaultActiveOrderStrategy } from './order/default-active-order-strategy';
@@ -129,6 +130,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         shippingCalculators: [defaultShippingCalculator],
         shippingLineAssignmentStrategy: new DefaultShippingLineAssignmentStrategy(),
         customFulfillmentProcess: [],
+        process: [defaultFulfillmentProcess],
         fulfillmentHandlers: [manualFulfillmentHandler],
     },
     orderOptions: {

+ 77 - 0
packages/core/src/config/fulfillment/default-fulfillment-process.ts

@@ -0,0 +1,77 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+
+import { awaitPromiseOrObservable, Transitions } from '../../common/index';
+import { FulfillmentState } from '../../service/index';
+
+import { FulfillmentProcess } from './fulfillment-process';
+
+declare module '../../service/helpers/fulfillment-state-machine/fulfillment-state' {
+    interface FulfillmentStates {
+        Shipped: never;
+        Delivered: never;
+    }
+}
+
+let configService: import('../config.service').ConfigService;
+let historyService: import('../../service/index').HistoryService;
+
+/**
+ * @description
+ * The default {@link FulfillmentProcess}
+ *
+ * @docsCategory fulfillment
+ */
+export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
+    transitions: {
+        Created: {
+            to: ['Pending'],
+        },
+        Pending: {
+            to: ['Shipped', 'Delivered', 'Cancelled'],
+        },
+        Shipped: {
+            to: ['Delivered', 'Cancelled'],
+        },
+        Delivered: {
+            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) {
+        const { fulfillmentHandlers } = configService.shippingOptions;
+        const fulfillmentHandler = fulfillmentHandlers.find(h => h.code === data.fulfillment.handlerCode);
+        if (fulfillmentHandler) {
+            const result = await awaitPromiseOrObservable(
+                fulfillmentHandler.onFulfillmentTransition(fromState, toState, data),
+            );
+            if (result === false || typeof result === 'string') {
+                return result;
+            }
+        }
+    },
+    async onTransitionEnd(fromState, toState, data) {
+        const historyEntryPromises = data.orders.map(order =>
+            historyService.createHistoryEntryForOrder({
+                orderId: order.id,
+                type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
+                ctx: data.ctx,
+                data: {
+                    fulfillmentId: data.fulfillment.id,
+                    from: fromState,
+                    to: toState,
+                },
+            }),
+        );
+        await Promise.all(historyEntryPromises);
+    },
+};

+ 16 - 2
packages/core/src/config/fulfillment/custom-fulfillment-process.ts → packages/core/src/config/fulfillment/fulfillment-process.ts

@@ -13,13 +13,16 @@ import {
 
 /**
  * @description
- * Used to define extensions to or modifications of the default fulfillment process.
+ * A FulfillmentProcess is used to define the way the fulfillment process works as in: what states a Fulfillment can be
+ * in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, a
+ * FulfillmentProcess 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
  */
-export interface CustomFulfillmentProcess<State extends keyof CustomFulfillmentStates | string>
+export interface FulfillmentProcess<State extends keyof CustomFulfillmentStates | string>
     extends InjectableStrategy {
     transitions?: Transitions<State, State | FulfillmentState> &
         Partial<Transitions<FulfillmentState | State>>;
@@ -27,3 +30,14 @@ export interface CustomFulfillmentProcess<State extends keyof CustomFulfillmentS
     onTransitionEnd?: OnTransitionEndFn<State | FulfillmentState, FulfillmentTransitionData>;
     onTransitionError?: OnTransitionErrorFn<State | FulfillmentState>;
 }
+
+/**
+ * @description
+ * Used to define extensions to or modifications of the default fulfillment process.
+ *
+ * For detailed description of the interface members, see the {@link StateMachineConfig} docs.
+ *
+ * @deprecated Use FulfillmentProcess
+ */
+export interface CustomFulfillmentProcess<State extends keyof CustomFulfillmentStates | string>
+    extends FulfillmentProcess<State> {}

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

@@ -21,7 +21,8 @@ export * from './entity-id-strategy/entity-id-strategy';
 export * from './entity-id-strategy/uuid-id-strategy';
 export * from './entity-metadata/entity-metadata-modifier';
 export * from './entity-metadata/add-foreign-key-indices';
-export * from './fulfillment/custom-fulfillment-process';
+export * from './fulfillment/default-fulfillment-process';
+export * from './fulfillment/fulfillment-process';
 export * from './fulfillment/fulfillment-handler';
 export * from './fulfillment/manual-fulfillment-handler';
 export * from './job-queue/inspectable-job-queue-strategy';

+ 13 - 4
packages/core/src/config/vendure-config.ts

@@ -22,8 +22,8 @@ import { StockDisplayStrategy } from './catalog/stock-display-strategy';
 import { CustomFields } from './custom-field/custom-field-types';
 import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
 import { EntityMetadataModifier } from './entity-metadata/entity-metadata-modifier';
-import { CustomFulfillmentProcess } from './fulfillment/custom-fulfillment-process';
 import { FulfillmentHandler } from './fulfillment/fulfillment-handler';
+import { FulfillmentProcess } from './fulfillment/fulfillment-process';
 import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { ActiveOrderStrategy } from './order/active-order-strategy';
@@ -694,10 +694,19 @@ export interface ShippingOptions {
     /**
      * @description
      * Allows the definition of custom states and transition logic for the fulfillment process state machine.
-     * Takes an array of objects implementing the {@link CustomFulfillmentProcess} interface.
+     * Takes an array of objects implementing the {@link FulfillmentProcess} interface.
+     *
+     * @deprecated use `process`
      */
-    customFulfillmentProcess?: Array<CustomFulfillmentProcess<any>>;
-
+    customFulfillmentProcess?: Array<FulfillmentProcess<any>>;
+    /**
+     * @description
+     * Allows the definition of custom states and transition logic for the fulfillment process state machine.
+     * Takes an array of objects implementing the {@link FulfillmentProcess} interface.
+     *
+     * @since 2.0.0
+     */
+    process?: Array<FulfillmentProcess<any>>;
     /**
      * @description
      * An array of available FulfillmentHandlers.

+ 8 - 57
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-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';
@@ -13,11 +12,7 @@ import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { HistoryService } from '../../services/history.service';
 
-import {
-    FulfillmentState,
-    fulfillmentStateTransitions,
-    FulfillmentTransitionData,
-} from './fulfillment-state';
+import { FulfillmentState, FulfillmentTransitionData } from './fulfillment-state';
 
 @Injectable()
 export class FulfillmentStateMachine {
@@ -52,56 +47,14 @@ export class FulfillmentStateMachine {
         fulfillment.state = fsm.currentState;
     }
 
-    /**
-     * Specific business logic to be executed on Fulfillment state transitions.
-     */
-    private async onTransitionStart(
-        fromState: FulfillmentState,
-        toState: FulfillmentState,
-        data: FulfillmentTransitionData,
-    ) {
-        const { fulfillmentHandlers } = this.configService.shippingOptions;
-        const fulfillmentHandler = fulfillmentHandlers.find(h => h.code === data.fulfillment.handlerCode);
-        if (fulfillmentHandler) {
-            const result = await awaitPromiseOrObservable(
-                fulfillmentHandler.onFulfillmentTransition(fromState, toState, data),
-            );
-            if (result === false || typeof result === 'string') {
-                return result;
-            }
-        }
-    }
-
-    /**
-     * Specific business logic to be executed after Fulfillment state transition completes.
-     */
-    private async onTransitionEnd(
-        fromState: FulfillmentState,
-        toState: FulfillmentState,
-        data: FulfillmentTransitionData,
-    ) {
-        const historyEntryPromises = data.orders.map(order =>
-            this.historyService.createHistoryEntryForOrder({
-                orderId: order.id,
-                type: HistoryEntryType.ORDER_FULFILLMENT_TRANSITION,
-                ctx: data.ctx,
-                data: {
-                    fulfillmentId: data.fulfillment.id,
-                    from: fromState,
-                    to: toState,
-                },
-            }),
-        );
-        await Promise.all(historyEntryPromises);
-    }
-
     private initConfig(): StateMachineConfig<FulfillmentState, FulfillmentTransitionData> {
+        // TODO: remove once the customFulfillmentProcess option is removed
         const customProcesses = this.configService.shippingOptions.customFulfillmentProcess ?? [];
-
-        const allTransitions = customProcesses.reduce(
+        const processes = [...customProcesses, ...(this.configService.shippingOptions.process ?? [])];
+        const allTransitions = processes.reduce(
             (transitions, process) =>
                 mergeTransitionDefinitions(transitions, process.transitions as Transitions<any>),
-            fulfillmentStateTransitions,
+            {} as Transitions<FulfillmentState>,
         );
 
         const validationResult = validateTransitionDefinition(allTransitions, 'Pending');
@@ -109,7 +62,7 @@ export class FulfillmentStateMachine {
         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),
@@ -119,18 +72,16 @@ export class FulfillmentStateMachine {
                         }
                     }
                 }
-                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),

+ 14 - 24
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state.ts

@@ -1,5 +1,4 @@
 import { RequestContext } from '../../../api/common/request-context';
-import { Transitions } from '../../../common/finite-state-machine/types';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 
@@ -7,41 +6,32 @@ import { Order } from '../../../entity/order/order.entity';
  * @description
  * An interface to extend standard {@link FulfillmentState}.
  *
- * @docsCategory fulfillment
+ * @deprecated use FulfillmentStates
  */
 export interface CustomFulfillmentStates {}
 
 /**
  * @description
- * These are the default states of the fulfillment process.
+ * An interface to extend standard {@link FulfillmentState}.
+ *
+ * @docsCategory fulfillment
+ */
+export interface FulfillmentStates {}
+
+/**
+ * @description
+ * These are the default states of the fulfillment process. By default, they will be extended
+ * by the {@link defaultFulfillmentProcess} to also include `Shipped` and `Delivered`.
+ *
  *
  * @docsCategory fulfillment
  */
 export type FulfillmentState =
     | 'Created'
     | 'Pending'
-    | 'Shipped'
-    | 'Delivered'
     | 'Cancelled'
-    | keyof CustomFulfillmentStates;
-
-export const fulfillmentStateTransitions: Transitions<FulfillmentState> = {
-    Created: {
-        to: ['Pending'],
-    },
-    Pending: {
-        to: ['Shipped', 'Delivered', 'Cancelled'],
-    },
-    Shipped: {
-        to: ['Delivered', 'Cancelled'],
-    },
-    Delivered: {
-        to: ['Cancelled'],
-    },
-    Cancelled: {
-        to: [],
-    },
-};
+    | keyof CustomFulfillmentStates
+    | keyof FulfillmentStates;
 
 /**
  * @description

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

@@ -24,7 +24,18 @@ export interface OrderStates {}
 /**
  * @description
  * These are the default states of the Order process. They can be augmented and
- * modified by using the {@link OrderOptions} `process` property.
+ * modified by using the {@link OrderOptions} `process` property, and by default
+ * the {@link defaultOrderProcess} will add the states
+ *
+ * - `ArrangingPayment`
+ * - `PaymentAuthorized`
+ * - `PaymentSettled`
+ * - `PartiallyShipped`
+ * - `Shipped`
+ * - `PartiallyDelivered`
+ * - `Delivered`
+ * - `Modifying`
+ * - `ArrangingAdditionalPayment`
  *
  * @docsCategory orders
  * @docsPage OrderProcess