Browse Source

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

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

File diff suppressed because it is too large
+ 528 - 510
packages/core/e2e/graphql/generated-e2e-admin-types.ts


File diff suppressed because it is too large
+ 616 - 600
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 66 - 63
packages/core/e2e/order-process.e2e-spec.ts

@@ -1,5 +1,11 @@
 /* tslint:disable:no-non-null-assertion */
-import { CustomOrderProcess, mergeConfig, OrderState, TransactionalConnection } from '@vendure/core';
+import {
+    CustomOrderProcess,
+    defaultOrderProcess,
+    mergeConfig,
+    OrderState,
+    TransactionalConnection,
+} from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import path from 'path';
 
@@ -7,20 +13,17 @@ import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
-import { AdminTransition, GetOrder, OrderFragment } from './graphql/generated-e2e-admin-types';
+import * as Codegen from './graphql/generated-e2e-admin-types';
+import { OrderFragment } from './graphql/generated-e2e-admin-types';
 import {
-    AddItemToOrder,
-    AddPaymentToOrder,
+    AddPaymentToOrderMutation,
+    AddPaymentToOrderMutationVariables,
     ErrorCode,
-    GetNextOrderStates,
-    SetCustomerForOrder,
-    SetShippingAddress,
-    SetShippingMethod,
     TestOrderFragmentFragment,
-    TransitionToState,
     TransitionToStateMutation,
     TransitionToStateMutationVariables,
 } from './graphql/generated-e2e-shop-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
 import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
@@ -96,7 +99,7 @@ describe('Order process', () => {
 
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
-            orderOptions: { process: [customOrderProcess as any, customOrderProcess2 as any] },
+            orderOptions: { process: [defaultOrderProcess, customOrderProcess, customOrderProcess2] as any },
             paymentOptions: {
                 paymentMethodHandlers: [testSuccessfulPaymentMethod],
             },
@@ -130,13 +133,13 @@ describe('Order process', () => {
             transitionEndSpy.mockClear();
             await shopClient.asAnonymousUser();
 
-            await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
-                ADD_ITEM_TO_ORDER,
-                {
-                    productVariantId: 'T_1',
-                    quantity: 1,
-                },
-            );
+            await shopClient.query<
+                CodegenShop.AddItemToOrderMutation,
+                CodegenShop.AddItemToOrderMutationVariables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
 
             expect(transitionStartSpy).toHaveBeenCalledTimes(1);
             expect(transitionEndSpy).toHaveBeenCalledTimes(1);
@@ -152,15 +155,15 @@ describe('Order process', () => {
         });
 
         it('replaced transition target', async () => {
-            await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
-                ADD_ITEM_TO_ORDER,
-                {
-                    productVariantId: 'T_1',
-                    quantity: 1,
-                },
-            );
+            await shopClient.query<
+                CodegenShop.AddItemToOrderMutation,
+                CodegenShop.AddItemToOrderMutationVariables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
 
-            const { nextOrderStates } = await shopClient.query<Codegen.GetNextOrderStatesQuery>(
+            const { nextOrderStates } = await shopClient.query<CodegenShop.GetNextOrderStatesQuery>(
                 GET_NEXT_STATES,
             );
 
@@ -172,8 +175,8 @@ describe('Order process', () => {
             transitionEndSpy.mockClear();
 
             const { transitionOrderToState } = await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
@@ -193,8 +196,8 @@ describe('Order process', () => {
             transitionErrorSpy.mockClear();
 
             await shopClient.query<
-                Codegen.SetCustomerForOrderMutation,
-                Codegen.SetCustomerForOrderMutationVariables
+                CodegenShop.SetCustomerForOrderMutation,
+                CodegenShop.SetCustomerForOrderMutationVariables
             >(SET_CUSTOMER, {
                 input: {
                     firstName: 'Joe',
@@ -204,8 +207,8 @@ describe('Order process', () => {
             });
 
             const { transitionOrderToState } = await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
@@ -233,8 +236,8 @@ describe('Order process', () => {
             transitionEndSpy.mockClear();
 
             await shopClient.query<
-                Codegen.SetCustomerForOrderMutation,
-                Codegen.SetCustomerForOrderMutationVariables
+                CodegenShop.SetCustomerForOrderMutation,
+                CodegenShop.SetCustomerForOrderMutationVariables
             >(SET_CUSTOMER, {
                 input: {
                     firstName: 'Joe',
@@ -244,8 +247,8 @@ describe('Order process', () => {
             });
 
             const { transitionOrderToState } = await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
@@ -260,15 +263,15 @@ describe('Order process', () => {
             transitionEndSpy.mockClear();
             transitionEndSpy2.mockClear();
 
-            const { nextOrderStates } = await shopClient.query<Codegen.GetNextOrderStatesQuery>(
+            const { nextOrderStates } = await shopClient.query<CodegenShop.GetNextOrderStatesQuery>(
                 GET_NEXT_STATES,
             );
 
             expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
 
             await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'AddingItems',
             });
@@ -282,10 +285,10 @@ describe('Order process', () => {
 
         // https://github.com/vendure-ecommerce/vendure/issues/963
         it('allows addPaymentToOrder from a custom state', async () => {
-            await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
-                SET_SHIPPING_METHOD,
-                { id: 'T_1' },
-            );
+            await shopClient.query<
+                CodegenShop.SetShippingMethodMutation,
+                CodegenShop.SetShippingMethodMutationVariables
+            >(SET_SHIPPING_METHOD, { id: 'T_1' });
             const result0 = await shopClient.query<
                 TransitionToStateMutation,
                 TransitionToStateMutationVariables
@@ -309,8 +312,8 @@ describe('Order process', () => {
             orderErrorGuard.assertSuccess(result2.transitionOrderToState);
             expect(result2.transitionOrderToState.state).toBe('PaymentProcessing');
             const { addPaymentToOrder } = await shopClient.query<
-                AddPaymentToOrder.Mutation,
-                AddPaymentToOrder.Variables
+                AddPaymentToOrderMutation,
+                AddPaymentToOrderMutationVariables
             >(ADD_PAYMENT, {
                 input: {
                     method: testSuccessfulPaymentMethod.code,
@@ -327,16 +330,16 @@ describe('Order process', () => {
 
         beforeAll(async () => {
             await shopClient.asAnonymousUser();
-            await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
-                ADD_ITEM_TO_ORDER,
-                {
-                    productVariantId: 'T_1',
-                    quantity: 1,
-                },
-            );
             await shopClient.query<
-                Codegen.SetCustomerForOrderMutation,
-                Codegen.SetCustomerForOrderMutationVariables
+                CodegenShop.AddItemToOrderMutation,
+                CodegenShop.AddItemToOrderMutationVariables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 1,
+            });
+            await shopClient.query<
+                CodegenShop.SetCustomerForOrderMutation,
+                CodegenShop.SetCustomerForOrderMutationVariables
             >(SET_CUSTOMER, {
                 input: {
                     firstName: 'Su',
@@ -345,8 +348,8 @@ describe('Order process', () => {
                 },
             });
             await shopClient.query<
-                Codegen.SetShippingAddressMutation,
-                Codegen.SetShippingAddressMutationVariables
+                CodegenShop.SetShippingAddressMutation,
+                CodegenShop.SetShippingAddressMutationVariables
             >(SET_SHIPPING_ADDRESS, {
                 input: {
                     fullName: 'name',
@@ -358,18 +361,18 @@ describe('Order process', () => {
                 },
             });
             await shopClient.query<
-                Codegen.SetShippingMethodMutation,
-                Codegen.SetShippingMethodMutationVariables
+                CodegenShop.SetShippingMethodMutation,
+                CodegenShop.SetShippingMethodMutationVariables
             >(SET_SHIPPING_METHOD, { id: 'T_1' });
             await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
             const { transitionOrderToState } = await shopClient.query<
-                Codegen.TransitionToStateMutation,
-                Codegen.TransitionToStateMutationVariables
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
             >(TRANSITION_TO_STATE, {
                 state: 'ArrangingPayment',
             });
@@ -434,8 +437,8 @@ describe('Order process', () => {
 
         it('cannot manually transition to Cancelled', async () => {
             const { addPaymentToOrder } = await shopClient.query<
-                Codegen.AddPaymentToOrderMutation,
-                Codegen.AddPaymentToOrderMutationVariables
+                CodegenShop.AddPaymentToOrderMutation,
+                CodegenShop.AddPaymentToOrderMutationVariables
             >(ADD_PAYMENT, {
                 input: {
                     method: testSuccessfulPaymentMethod.code,

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

@@ -3,6 +3,7 @@ import {
     CustomOrderProcess,
     CustomPaymentProcess,
     DefaultLogger,
+    defaultOrderProcess,
     LanguageCode,
     mergeConfig,
     Order,
@@ -124,7 +125,7 @@ describe('Payment process', () => {
         mergeConfig(testConfig(), {
             // logger: new DefaultLogger(),
             orderOptions: {
-                process: [customOrderProcess as any],
+                process: [defaultOrderProcess, customOrderProcess] as any,
                 orderPlacedStrategy: new TestOrderPlacedStrategy(),
             },
             paymentOptions: {

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

@@ -26,6 +26,7 @@ import { DefaultActiveOrderStrategy } from './order/default-active-order-strateg
 import { DefaultChangedPriceHandlingStrategy } from './order/default-changed-price-handling-strategy';
 import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-item-price-calculation-strategy';
 import { DefaultOrderPlacedStrategy } from './order/default-order-placed-strategy';
+import { defaultOrderProcess } from './order/default-order-process';
 import { DefaultOrderSellerStrategy } from './order/default-order-seller-strategy';
 import { DefaultStockAllocationStrategy } from './order/default-stock-allocation-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
@@ -136,7 +137,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         orderItemPriceCalculationStrategy: new DefaultOrderItemPriceCalculationStrategy(),
         mergeStrategy: new MergeOrdersStrategy(),
         checkoutMergeStrategy: new UseGuestStrategy(),
-        process: [],
+        process: [defaultOrderProcess],
         stockAllocationStrategy: new DefaultStockAllocationStrategy(),
         orderCodeStrategy: new DefaultOrderCodeStrategy(),
         orderByCodeAccessStrategy: new DefaultOrderByCodeAccessStrategy('2h'),

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

@@ -33,9 +33,10 @@ export * from './merge-config';
 export * from './order/active-order-strategy';
 export * from './order/default-active-order-strategy';
 export * from './order/changed-price-handling-strategy';
-export * from './order/custom-order-process';
+export * from './order/order-process';
 export * from './order/default-changed-price-handling-strategy';
 export * from './order/default-order-placed-strategy';
+export * from './order/default-order-process';
 export * from './order/default-order-seller-strategy';
 export * from './order/default-stock-allocation-strategy';
 export * from './order/merge-orders-strategy';

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

@@ -0,0 +1,446 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+import { unique } from '@vendure/common/lib/unique';
+
+import { RequestContext } from '../../api/index';
+import { TransactionalConnection } from '../../connection/transactional-connection';
+import { Order, Payment, ProductVariant } from '../../entity/index';
+import { OrderModification } from '../../entity/order-modification/order-modification.entity';
+import { OrderPlacedEvent } from '../../event-bus/events/order-placed-event';
+import { OrderState } from '../../service/helpers/order-state-machine/order-state';
+import {
+    orderItemsAreAllCancelled,
+    orderItemsAreDelivered,
+    orderItemsArePartiallyDelivered,
+    orderItemsArePartiallyShipped,
+    orderItemsAreShipped,
+    orderTotalIsCovered,
+    totalCoveredByPayments,
+} from '../../service/helpers/utils/order-utils';
+
+import { OrderProcess } from './order-process';
+
+declare module '../../service/helpers/order-state-machine/order-state' {
+    interface OrderStates {
+        ArrangingPayment: never;
+        PaymentAuthorized: never;
+        PaymentSettled: never;
+        PartiallyShipped: never;
+        Shipped: never;
+        PartiallyDelivered: never;
+        Delivered: never;
+        Modifying: never;
+        ArrangingAdditionalPayment: never;
+    }
+}
+
+/**
+ * @description
+ * Options which can be passed to the {@link configureDefaultOrderProcess} function
+ * to configure an instance of the default {@link OrderProcess}. By default, all
+ * options are set to `true`.
+ *
+ * @docsCategory Orders
+ * @docsPage OrderProcess
+ * @since 2.0.0
+ */
+export interface DefaultOrderProcessOptions {
+    /**
+     * @description
+     * Prevents an Order from transitioning out of the `Modifying` state if
+     * the Order price has changed and there is no Payment or Refund associated
+     * with the Modification.
+     *
+     * @default true
+     */
+    checkModificationPayments?: boolean;
+    /**
+     * @description
+     * Prevents an Order from transitioning out of the `ArrangingAdditionalPayment` state if
+     * the Order's Payments do not cover the full amount of `totalWithTax`.
+     *
+     * @default true
+     */
+    checkAdditionalPaymentsAmount?: boolean;
+    /**
+     * @description
+     * Prevents the transition from `AddingItems` to any other state (apart from `Cancelled`) if
+     * and of the ProductVariants no longer exists due to deletion.
+     *
+     * @default true
+     */
+    checkAllVariantsExist?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `ArrangingPayment` state if the active Order has no lines.
+     *
+     * @default true
+     */
+    arrangingPaymentRequiresContents?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `ArrangingPayment` state if the active Order has no customer
+     * associated with it.
+     *
+     * @default true
+     */
+    arrangingPaymentRequiresCustomer?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `ArrangingPayment` state if the active Order has no shipping
+     * method set.
+     *
+     * @default true
+     */
+    arrangingPaymentRequiresShipping?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `ArrangingPayment` state if there is insufficient saleable
+     * stock to cover the contents of the Order.
+     *
+     * @default true
+     */
+    arrangingPaymentRequiresStock?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `PaymentAuthorized` or `PaymentSettled` states if the order
+     * `totalWithTax` amount is not covered by Payment(s) in the corresponding states.
+     *
+     * @default true
+     */
+    checkPaymentsCoverTotal?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `Cancelled` state unless all OrderItems are already
+     * cancelled.
+     *
+     * @default true
+     */
+    checkAllItemsBeforeCancel?: boolean;
+    /**
+     * @description
+     * Prevents transition to the `Shipped`, `PartiallyShipped`, `Delivered` & `PartiallyDelivered` states unless
+     * there are corresponding Fulfillments in the correct states to allow this. E.g. `Shipped` only if all items in
+     * the Order are part of a Fulfillment which itself is in the `Shipped` state.
+     *
+     * @default true
+     */
+    checkFulfillmentStates?: boolean;
+}
+
+/**
+ * @description
+ * Used to configure a customized instance of the default {@link OrderProcess} that ships with Vendure.
+ * Using this function allows you to turn off certain checks and constraints that are enabled by default.
+ *
+ * ```TypeScript
+ * import { configureDefaultOrderProcess, VendureConfig } from '\@vendure/core';
+ *
+ * const myCustomOrderProcess = configureDefaultOrderProcess({
+ *   // Disable the constraint that requires
+ *   // Orders to have a shipping method assigned
+ *   // before payment.
+ *   arrangingPaymentRequiresShipping: false,
+ * });
+ *
+ * export const config: VendureConfig = {
+ *   orderOptions: {
+ *     process: [myCustomOrderProcess],
+ *   },
+ * };
+ * ```
+ * @docsCategory Orders
+ * @docsPage OrderProcess
+ * @since 2.0.0
+ */
+export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions) {
+    let connection: TransactionalConnection;
+    let productVariantService: import('../../service/index').ProductVariantService;
+    let configService: import('../config.service').ConfigService;
+    let eventBus: import('../../event-bus/index').EventBus;
+    let stockMovementService: import('../../service/index').StockMovementService;
+    let historyService: import('../../service/index').HistoryService;
+
+    const orderProcess: OrderProcess<OrderState> = {
+        transitions: {
+            Created: {
+                to: ['AddingItems', 'Draft'],
+            },
+            Draft: {
+                to: ['Cancelled', 'ArrangingPayment'],
+            },
+            AddingItems: {
+                to: ['ArrangingPayment', 'Cancelled'],
+            },
+            ArrangingPayment: {
+                to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems', 'Cancelled'],
+            },
+            PaymentAuthorized: {
+                to: ['PaymentSettled', 'Cancelled', 'Modifying', 'ArrangingAdditionalPayment'],
+            },
+            PaymentSettled: {
+                to: [
+                    'PartiallyDelivered',
+                    'Delivered',
+                    'PartiallyShipped',
+                    'Shipped',
+                    'Cancelled',
+                    'Modifying',
+                    'ArrangingAdditionalPayment',
+                ],
+            },
+            PartiallyShipped: {
+                to: ['Shipped', 'PartiallyDelivered', 'Cancelled', 'Modifying'],
+            },
+            Shipped: {
+                to: ['PartiallyDelivered', 'Delivered', 'Cancelled', 'Modifying'],
+            },
+            PartiallyDelivered: {
+                to: ['Delivered', 'Cancelled', 'Modifying'],
+            },
+            Delivered: {
+                to: ['Cancelled'],
+            },
+            Modifying: {
+                to: [
+                    'PaymentAuthorized',
+                    'PaymentSettled',
+                    'PartiallyShipped',
+                    'Shipped',
+                    'PartiallyDelivered',
+                    'ArrangingAdditionalPayment',
+                ],
+            },
+            ArrangingAdditionalPayment: {
+                to: [
+                    'PaymentAuthorized',
+                    'PaymentSettled',
+                    'PartiallyShipped',
+                    'Shipped',
+                    'PartiallyDelivered',
+                    '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 EventBus = await import('../../event-bus/index').then(m => m.EventBus);
+            const StockMovementService = await import('../../service/index').then(
+                m => m.StockMovementService,
+            );
+            const HistoryService = await import('../../service/index').then(m => m.HistoryService);
+            const ProductVariantService = await import('../../service/index').then(
+                m => m.ProductVariantService,
+            );
+            connection = injector.get(TransactionalConnection);
+            productVariantService = injector.get(ProductVariantService);
+            configService = injector.get(ConfigService);
+            eventBus = injector.get(EventBus);
+            stockMovementService = injector.get(StockMovementService);
+            historyService = injector.get(HistoryService);
+        },
+
+        async onTransitionStart(fromState, toState, { ctx, order }) {
+            if (options.checkModificationPayments !== false && fromState === 'Modifying') {
+                const modifications = await connection
+                    .getRepository(ctx, OrderModification)
+                    .find({ where: { order }, relations: ['refund', 'payment'] });
+                if (toState === 'ArrangingAdditionalPayment') {
+                    if (
+                        0 < modifications.length &&
+                        modifications.every(modification => modification.isSettled)
+                    ) {
+                        return `message.cannot-transition-no-additional-payments-needed`;
+                    }
+                } else {
+                    if (modifications.some(modification => !modification.isSettled)) {
+                        return `message.cannot-transition-without-modification-payment`;
+                    }
+                }
+            }
+            if (
+                options.checkAdditionalPaymentsAmount !== false &&
+                fromState === 'ArrangingAdditionalPayment'
+            ) {
+                if (toState === 'Cancelled') {
+                    return;
+                }
+                const existingPayments = await connection.getRepository(ctx, Payment).find({
+                    relations: ['refunds'],
+                    where: {
+                        order: { id: order.id },
+                    },
+                });
+                order.payments = existingPayments;
+                const deficit = order.totalWithTax - totalCoveredByPayments(order);
+                if (0 < deficit) {
+                    return `message.cannot-transition-from-arranging-additional-payment`;
+                }
+            }
+            if (
+                options.checkAllVariantsExist !== false &&
+                fromState === 'AddingItems' &&
+                toState !== 'Cancelled' &&
+                order.lines.length > 0
+            ) {
+                const variantIds = unique(order.lines.map(l => l.productVariant.id));
+                const qb = connection
+                    .getRepository(ctx, ProductVariant)
+                    .createQueryBuilder('variant')
+                    .leftJoin('variant.product', 'product')
+                    .where('variant.deletedAt IS NULL')
+                    .andWhere('product.deletedAt IS NULL')
+                    .andWhere('variant.id IN (:...variantIds)', { variantIds });
+                const availableVariants = await qb.getMany();
+                if (availableVariants.length !== variantIds.length) {
+                    return `message.cannot-transition-order-contains-products-which-are-unavailable`;
+                }
+            }
+            if (toState === 'ArrangingPayment') {
+                if (options.arrangingPaymentRequiresContents !== false && order.lines.length === 0) {
+                    return `message.cannot-transition-to-payment-when-order-is-empty`;
+                }
+                if (options.arrangingPaymentRequiresCustomer !== false && !order.customer) {
+                    return `message.cannot-transition-to-payment-without-customer`;
+                }
+                if (
+                    options.arrangingPaymentRequiresShipping !== false &&
+                    (!order.shippingLines || order.shippingLines.length === 0)
+                ) {
+                    return `message.cannot-transition-to-payment-without-shipping-method`;
+                }
+                if (options.arrangingPaymentRequiresStock !== false) {
+                    const variantsWithInsufficientSaleableStock: ProductVariant[] = [];
+                    for (const line of order.lines) {
+                        const availableStock = await productVariantService.getSaleableStockLevel(
+                            ctx,
+                            line.productVariant,
+                        );
+                        if (line.quantity > availableStock) {
+                            variantsWithInsufficientSaleableStock.push(line.productVariant);
+                        }
+                    }
+                    if (variantsWithInsufficientSaleableStock.length) {
+                        return ctx.translate(
+                            'message.cannot-transition-to-payment-due-to-insufficient-stock',
+                            {
+                                productVariantNames: variantsWithInsufficientSaleableStock
+                                    .map(v => v.name)
+                                    .join(', '),
+                            },
+                        );
+                    }
+                }
+            }
+            if (options.checkPaymentsCoverTotal !== false) {
+                if (toState === 'PaymentAuthorized') {
+                    const hasAnAuthorizedPayment = !!order.payments.find(p => p.state === 'Authorized');
+                    if (!orderTotalIsCovered(order, ['Authorized', 'Settled']) || !hasAnAuthorizedPayment) {
+                        return `message.cannot-transition-without-authorized-payments`;
+                    }
+                }
+                if (toState === 'PaymentSettled' && !orderTotalIsCovered(order, 'Settled')) {
+                    return `message.cannot-transition-without-settled-payments`;
+                }
+            }
+            if (options.checkAllItemsBeforeCancel !== false) {
+                if (
+                    toState === 'Cancelled' &&
+                    fromState !== 'AddingItems' &&
+                    fromState !== 'ArrangingPayment'
+                ) {
+                    if (!orderItemsAreAllCancelled(order)) {
+                        return `message.cannot-transition-unless-all-cancelled`;
+                    }
+                }
+            }
+            if (options.checkFulfillmentStates !== false) {
+                if (toState === 'PartiallyShipped') {
+                    const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
+                    if (!orderItemsArePartiallyShipped(orderWithFulfillments)) {
+                        return `message.cannot-transition-unless-some-order-items-shipped`;
+                    }
+                }
+                if (toState === 'Shipped') {
+                    const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
+                    if (!orderItemsAreShipped(orderWithFulfillments)) {
+                        return `message.cannot-transition-unless-all-order-items-shipped`;
+                    }
+                }
+                if (toState === 'PartiallyDelivered') {
+                    const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
+                    if (!orderItemsArePartiallyDelivered(orderWithFulfillments)) {
+                        return `message.cannot-transition-unless-some-order-items-delivered`;
+                    }
+                }
+                if (toState === 'Delivered') {
+                    const orderWithFulfillments = await findOrderWithFulfillments(ctx, order.id);
+                    if (!orderItemsAreDelivered(orderWithFulfillments)) {
+                        return `message.cannot-transition-unless-all-order-items-delivered`;
+                    }
+                }
+            }
+        },
+        async onTransitionEnd(fromState, toState, data) {
+            const { ctx, order } = data;
+            const { stockAllocationStrategy, orderPlacedStrategy } = configService.orderOptions;
+            if (order.active) {
+                const shouldSetAsPlaced = orderPlacedStrategy.shouldSetAsPlaced(
+                    ctx,
+                    fromState,
+                    toState,
+                    order,
+                );
+                if (shouldSetAsPlaced) {
+                    order.active = false;
+                    order.orderPlacedAt = new Date();
+                    eventBus.publish(new OrderPlacedEvent(fromState, toState, ctx, order));
+                }
+            }
+            const shouldAllocateStock = await stockAllocationStrategy.shouldAllocateStock(
+                ctx,
+                fromState,
+                toState,
+                order,
+            );
+            if (shouldAllocateStock) {
+                await stockMovementService.createAllocationsForOrder(ctx, order);
+            }
+            if (toState === 'Cancelled') {
+                order.active = false;
+            }
+            await historyService.createHistoryEntryForOrder({
+                orderId: order.id,
+                type: HistoryEntryType.ORDER_STATE_TRANSITION,
+                ctx,
+                data: {
+                    from: fromState,
+                    to: toState,
+                },
+            });
+        },
+    };
+
+    async function findOrderWithFulfillments(ctx: RequestContext, id: ID): Promise<Order> {
+        return await connection.getEntityOrThrow(ctx, Order, id, {
+            relations: ['lines', 'lines.items', 'lines.items.fulfillments'],
+        });
+    }
+
+    return orderProcess;
+}
+
+/**
+ * @description
+ * This is the built-in {@link OrderProcess} that ships with Vendure.
+ *
+ * @docsCategory Orders
+ * @docsPage OrderProcess
+ * @since 2.0.0
+ */
+export const defaultOrderProcess = configureDefaultOrderProcess({});

+ 16 - 3
packages/core/src/config/order/custom-order-process.ts → packages/core/src/config/order/order-process.ts

@@ -14,16 +14,29 @@ import {
 
 /**
  * @description
- * Used to define extensions to or modifications of the default order process.
+ * An OrderProcess is used to define the way the order process works as in: what states an Order can be
+ * in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, an
+ * OrderProcess 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 orders
+ * @docsPage OrderProcess
+ * @docsWeight 0
  */
-export interface CustomOrderProcess<State extends keyof CustomOrderStates | string>
-    extends InjectableStrategy {
+export interface OrderProcess<State extends keyof CustomOrderStates | string> extends InjectableStrategy {
     transitions?: Transitions<State, State | OrderState> & Partial<Transitions<OrderState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | OrderState, OrderTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | OrderState, OrderTransitionData>;
     onTransitionError?: OnTransitionErrorFn<State | OrderState>;
 }
+
+/**
+ * @description
+ * Used to define extensions to or modifications of the default order process.
+ *
+ * @deprecated Use OrderProcess
+ */
+export interface CustomOrderProcess<State extends keyof CustomOrderStates | string>
+    extends OrderProcess<State> {}

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

@@ -28,12 +28,12 @@ import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { ActiveOrderStrategy } from './order/active-order-strategy';
 import { ChangedPriceHandlingStrategy } from './order/changed-price-handling-strategy';
-import { CustomOrderProcess } from './order/custom-order-process';
 import { OrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { OrderCodeStrategy } from './order/order-code-strategy';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
 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';
@@ -490,11 +490,11 @@ export interface OrderOptions {
     /**
      * @description
      * Allows the definition of custom states and transition logic for the order process state machine.
-     * Takes an array of objects implementing the {@link CustomOrderProcess} interface.
+     * Takes an array of objects implementing the {@link OrderProcess} interface.
      *
      * @default []
      */
-    process?: Array<CustomOrderProcess<any>>;
+    process?: Array<OrderProcess<any>>;
     /**
      * @description
      * Determines the point of the order process at which stock gets allocated.

+ 0 - 15
packages/core/src/entity/order/aggregate-order.entity.ts

@@ -1,15 +0,0 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity, OneToMany } from 'typeorm';
-
-import { Order } from './order.entity';
-import { SellerOrder } from './seller-order.entity';
-
-@ChildEntity()
-export class AggregateOrder extends Order {
-    constructor(input?: DeepPartial<AggregateOrder>) {
-        super(input);
-    }
-
-    @OneToMany(type => SellerOrder, sellerOrder => sellerOrder.aggregateOrder)
-    sellerOrders: SellerOrder[];
-}

+ 0 - 14
packages/core/src/entity/order/seller-order.entity.ts

@@ -1,14 +0,0 @@
-import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity, ManyToOne } from 'typeorm';
-
-import { AggregateOrder } from './aggregate-order.entity';
-import { Order } from './order.entity';
-
-@ChildEntity()
-export class SellerOrder extends Order {
-    constructor(input?: DeepPartial<SellerOrder>) {
-        super(input);
-    }
-    @ManyToOne(type => AggregateOrder, aggregateOrder => aggregateOrder.sellerOrders)
-    aggregateOrder: AggregateOrder;
-}

+ 9 - 182
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -1,7 +1,4 @@
 import { Injectable } from '@nestjs/common';
-import { HistoryEntryType } from '@vendure/common/lib/generated-types';
-import { ID } from '@vendure/common/lib/shared-types';
-import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
@@ -11,28 +8,16 @@ 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 { OrderProcess } from '../../../config/index';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
-import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
 import { Order } from '../../../entity/order/order.entity';
-import { Payment } from '../../../entity/payment/payment.entity';
-import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
-import { OrderPlacedEvent } from '../../../event-bus/events/order-placed-event';
 import { EventBus } from '../../../event-bus/index';
 import { HistoryService } from '../../services/history.service';
 import { ProductVariantService } from '../../services/product-variant.service';
 import { PromotionService } from '../../services/promotion.service';
 import { StockMovementService } from '../../services/stock-movement.service';
-import {
-    orderItemsAreAllCancelled,
-    orderItemsAreDelivered,
-    orderItemsArePartiallyDelivered,
-    orderItemsArePartiallyShipped,
-    orderItemsAreShipped,
-    orderTotalIsCovered,
-    totalCoveredByPayments,
-} from '../utils/order-utils';
 
-import { OrderState, orderStateTransitions, OrderTransitionData } from './order-state';
+import { OrderState, OrderTransitionData } from './order-state';
 
 @Injectable()
 export class OrderStateMachine {
@@ -70,170 +55,14 @@ export class OrderStateMachine {
         order.state = fsm.currentState;
     }
 
-    private async findOrderWithFulfillments(ctx: RequestContext, id: ID): Promise<Order> {
-        return await this.connection.getEntityOrThrow(ctx, Order, id, {
-            relations: ['lines', 'lines.items', 'lines.items.fulfillments'],
-        });
-    }
-
-    /**
-     * Specific business logic to be executed on Order state transitions.
-     */
-    private async onTransitionStart(fromState: OrderState, toState: OrderState, data: OrderTransitionData) {
-        if (fromState === 'Modifying') {
-            const modifications = await this.connection
-                .getRepository(data.ctx, OrderModification)
-                .find({ where: { order: data.order }, relations: ['refund', 'payment'] });
-            if (toState === 'ArrangingAdditionalPayment') {
-                if (0 < modifications.length && modifications.every(modification => modification.isSettled)) {
-                    return `message.cannot-transition-no-additional-payments-needed`;
-                }
-            } else {
-                if (modifications.some(modification => !modification.isSettled)) {
-                    return `message.cannot-transition-without-modification-payment`;
-                }
-            }
-        }
-        if (fromState === 'ArrangingAdditionalPayment') {
-            if (toState === 'Cancelled') {
-                return;
-            }
-            const existingPayments = await this.connection.getRepository(data.ctx, Payment).find({
-                relations: ['refunds'],
-                where: {
-                    order: { id: data.order.id },
-                },
-            });
-            data.order.payments = existingPayments;
-            const deficit = data.order.totalWithTax - totalCoveredByPayments(data.order);
-            if (0 < deficit) {
-                return `message.cannot-transition-from-arranging-additional-payment`;
-            }
-        }
-        if (fromState === 'AddingItems' && toState !== 'Cancelled' && data.order.lines.length > 0) {
-            const variantIds = unique(data.order.lines.map(l => l.productVariant.id));
-            const qb = this.connection
-                .getRepository(data.ctx, ProductVariant)
-                .createQueryBuilder('variant')
-                .leftJoin('variant.product', 'product')
-                .where('variant.deletedAt IS NULL')
-                .andWhere('product.deletedAt IS NULL')
-                .andWhere('variant.id IN (:...variantIds)', { variantIds });
-            const availableVariants = await qb.getMany();
-            if (availableVariants.length !== variantIds.length) {
-                return `message.cannot-transition-order-contains-products-which-are-unavailable`;
-            }
-        }
-        if (toState === 'ArrangingPayment') {
-            if (data.order.lines.length === 0) {
-                return `message.cannot-transition-to-payment-when-order-is-empty`;
-            }
-            if (!data.order.customer) {
-                return `message.cannot-transition-to-payment-without-customer`;
-            }
-            if (!data.order.shippingLines || data.order.shippingLines.length === 0) {
-                return `message.cannot-transition-to-payment-without-shipping-method`;
-            }
-            const variantsWithInsufficientSaleableStock: ProductVariant[] = [];
-            for (const line of data.order.lines) {
-                const availableStock = await this.productVariantService.getSaleableStockLevel(
-                    data.ctx,
-                    line.productVariant,
-                );
-                if (line.quantity > availableStock) {
-                    variantsWithInsufficientSaleableStock.push(line.productVariant);
-                }
-            }
-            if (variantsWithInsufficientSaleableStock.length) {
-                return data.ctx.translate('message.cannot-transition-to-payment-due-to-insufficient-stock', {
-                    productVariantNames: variantsWithInsufficientSaleableStock.map(v => v.name).join(', '),
-                });
-            }
-        }
-        if (toState === 'PaymentAuthorized') {
-            const hasAnAuthorizedPayment = !!data.order.payments.find(p => p.state === 'Authorized');
-            if (!orderTotalIsCovered(data.order, ['Authorized', 'Settled']) || !hasAnAuthorizedPayment) {
-                return `message.cannot-transition-without-authorized-payments`;
-            }
-        }
-        if (toState === 'PaymentSettled' && !orderTotalIsCovered(data.order, 'Settled')) {
-            return `message.cannot-transition-without-settled-payments`;
-        }
-        if (toState === 'Cancelled' && fromState !== 'AddingItems' && fromState !== 'ArrangingPayment') {
-            if (!orderItemsAreAllCancelled(data.order)) {
-                return `message.cannot-transition-unless-all-cancelled`;
-            }
-        }
-        if (toState === 'PartiallyShipped') {
-            const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
-            if (!orderItemsArePartiallyShipped(orderWithFulfillments)) {
-                return `message.cannot-transition-unless-some-order-items-shipped`;
-            }
-        }
-        if (toState === 'Shipped') {
-            const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
-            if (!orderItemsAreShipped(orderWithFulfillments)) {
-                return `message.cannot-transition-unless-all-order-items-shipped`;
-            }
-        }
-        if (toState === 'PartiallyDelivered') {
-            const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
-            if (!orderItemsArePartiallyDelivered(orderWithFulfillments)) {
-                return `message.cannot-transition-unless-some-order-items-delivered`;
-            }
-        }
-        if (toState === 'Delivered') {
-            const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
-            if (!orderItemsAreDelivered(orderWithFulfillments)) {
-                return `message.cannot-transition-unless-all-order-items-delivered`;
-            }
-        }
-    }
-
-    /**
-     * Specific business logic to be executed after Order state transition completes.
-     */
-    private async onTransitionEnd(fromState: OrderState, toState: OrderState, data: OrderTransitionData) {
-        const { ctx, order } = data;
-        const { stockAllocationStrategy, orderPlacedStrategy } = this.configService.orderOptions;
-        if (order.active) {
-            const shouldSetAsPlaced = orderPlacedStrategy.shouldSetAsPlaced(ctx, fromState, toState, order);
-            if (shouldSetAsPlaced) {
-                order.active = false;
-                order.orderPlacedAt = new Date();
-                this.eventBus.publish(new OrderPlacedEvent(fromState, toState, ctx, order));
-            }
-        }
-        const shouldAllocateStock = await stockAllocationStrategy.shouldAllocateStock(
-            ctx,
-            fromState,
-            toState,
-            order,
-        );
-        if (shouldAllocateStock) {
-            await this.stockMovementService.createAllocationsForOrder(ctx, order);
-        }
-        if (toState === 'Cancelled') {
-            order.active = false;
-        }
-        await this.historyService.createHistoryEntryForOrder({
-            orderId: order.id,
-            type: HistoryEntryType.ORDER_STATE_TRANSITION,
-            ctx,
-            data: {
-                from: fromState,
-                to: toState,
-            },
-        });
-    }
-
     private initConfig(): StateMachineConfig<OrderState, OrderTransitionData> {
-        const customProcesses = this.configService.orderOptions.process ?? [];
+        const orderProcesses = this.configService.orderOptions.process ?? [];
 
-        const allTransitions = customProcesses.reduce(
+        const emptyProcess: OrderProcess<any> = { transitions: {} };
+        const allTransitions = orderProcesses.reduce(
             (transitions, process) =>
                 mergeTransitionDefinitions(transitions, process.transitions as Transitions<any>),
-            orderStateTransitions,
+            {} as Transitions<OrderState>,
         );
 
         const validationResult = validateTransitionDefinition(allTransitions, 'AddingItems');
@@ -241,7 +70,7 @@ export class OrderStateMachine {
         return {
             transitions: allTransitions,
             onTransitionStart: async (fromState, toState, data) => {
-                for (const process of customProcesses) {
+                for (const process of orderProcesses) {
                     if (typeof process.onTransitionStart === 'function') {
                         const result = await awaitPromiseOrObservable(
                             process.onTransitionStart(fromState, toState, data),
@@ -251,18 +80,16 @@ export class OrderStateMachine {
                         }
                     }
                 }
-                return this.onTransitionStart(fromState, toState, data);
             },
             onTransitionEnd: async (fromState, toState, data) => {
-                for (const process of customProcesses) {
+                for (const process of orderProcesses) {
                     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 orderProcesses) {
                     if (typeof process.onTransitionError === 'function') {
                         await awaitPromiseOrObservable(
                             process.onTransitionError(fromState, toState, message),

+ 21 - 74
packages/core/src/service/helpers/order-state-machine/order-state.ts

@@ -7,96 +7,43 @@ import { Order } from '../../../entity/order/order.entity';
  * An interface to extend standard {@link OrderState}.
  *
  * @docsCategory orders
+ * @deprecated use OrderStates
  */
 export interface CustomOrderStates {}
 
+/**
+ * @description
+ * An interface to extend the {@link OrderState} type.
+ *
+ * @docsCategory orders
+ * @docsPage OrderProcess
+ * @since 2.0.0
+ */
+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.
  *
  * @docsCategory orders
+ * @docsPage OrderProcess
  */
 export type OrderState =
     | 'Created'
     | 'Draft'
     | 'AddingItems'
-    | 'ArrangingPayment'
-    | 'PaymentAuthorized'
-    | 'PaymentSettled'
-    | 'PartiallyShipped'
-    | 'Shipped'
-    | 'PartiallyDelivered'
-    | 'Delivered'
-    | 'Modifying'
-    | 'ArrangingAdditionalPayment'
     | 'Cancelled'
-    | keyof CustomOrderStates;
-
-export const orderStateTransitions: Transitions<OrderState> = {
-    Created: {
-        to: ['AddingItems', 'Draft'],
-    },
-    Draft: {
-        to: ['Cancelled', 'ArrangingPayment'],
-    },
-    AddingItems: {
-        to: ['ArrangingPayment', 'Cancelled'],
-    },
-    ArrangingPayment: {
-        to: ['PaymentAuthorized', 'PaymentSettled', 'AddingItems', 'Cancelled'],
-    },
-    PaymentAuthorized: {
-        to: ['PaymentSettled', 'Cancelled', 'Modifying', 'ArrangingAdditionalPayment'],
-    },
-    PaymentSettled: {
-        to: [
-            'PartiallyDelivered',
-            'Delivered',
-            'PartiallyShipped',
-            'Shipped',
-            'Cancelled',
-            'Modifying',
-            'ArrangingAdditionalPayment',
-        ],
-    },
-    PartiallyShipped: {
-        to: ['Shipped', 'PartiallyDelivered', 'Cancelled', 'Modifying'],
-    },
-    Shipped: {
-        to: ['PartiallyDelivered', 'Delivered', 'Cancelled', 'Modifying'],
-    },
-    PartiallyDelivered: {
-        to: ['Delivered', 'Cancelled', 'Modifying'],
-    },
-    Delivered: {
-        to: ['Cancelled'],
-    },
-    Modifying: {
-        to: [
-            'PaymentAuthorized',
-            'PaymentSettled',
-            'PartiallyShipped',
-            'Shipped',
-            'PartiallyDelivered',
-            'ArrangingAdditionalPayment',
-        ],
-    },
-    ArrangingAdditionalPayment: {
-        to: [
-            'PaymentAuthorized',
-            'PaymentSettled',
-            'PartiallyShipped',
-            'Shipped',
-            'PartiallyDelivered',
-            'Cancelled',
-        ],
-    },
-    Cancelled: {
-        to: [],
-    },
-};
+    | keyof CustomOrderStates
+    | keyof OrderStates;
 
+/**
+ * @description
+ * This is the object passed to the {@link OrderProcess} state transition hooks.
+ *
+ * @docsCategory orders
+ * @docsPage OrderProcess
+ */
 export interface OrderTransitionData {
     ctx: RequestContext;
     order: Order;

+ 1 - 0
packages/core/src/service/index.ts

@@ -21,6 +21,7 @@ export * from './helpers/translatable-saver/translatable-saver';
 export * from './helpers/translator/translator.service';
 export * from './helpers/utils/patch-entity';
 export * from './helpers/utils/translate-entity';
+export * from './helpers/utils/order-utils';
 export * from './helpers/verification-token-generator/verification-token-generator';
 export * from './services/administrator.service';
 export * from './services/asset.service';

Some files were not shown because too many files changed in this diff