Skip to content

Commit

Permalink
feat(core): Extract hard-coded payment state & process
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelbromley committed Jan 17, 2023
1 parent cdb2b75 commit 4c5c946
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 76 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,6 +125,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
...customFulfillmentProcess,
...fulfillmentProcess,
...customPaymentProcess,
...paymentProcess,
stockAllocationStrategy,
stockDisplayStrategy,
...healthChecks,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -152,6 +153,7 @@ export const defaultConfig: RuntimeVendureConfig = {
paymentMethodEligibilityCheckers: [],
paymentMethodHandlers: [],
customPaymentProcess: [],
process: [defaultPaymentProcess],
},
taxOptions: {
taxZoneStrategy: new DefaultTaxZoneStrategy(),
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/config/payment/default-payment-process.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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> {}
14 changes: 11 additions & 3 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>>;
/**
Expand Down Expand Up @@ -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>>;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
}

Expand All @@ -43,50 +41,22 @@ 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);

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),
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
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';

/**
* @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.
Expand All @@ -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
Expand Down

0 comments on commit 4c5c946

Please sign in to comment.