Quellcode durchsuchen

feat(core): Implement configurable PriceCalculationStrategy

Relates to  #237
Michael Bromley vor 5 Jahren
Ursprung
Commit
3e2cc2bbdc

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

@@ -131,7 +131,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
         } = this.configService.assetOptions;
         } = this.configService.assetOptions;
         const { taxCalculationStrategy, taxZoneStrategy } = this.configService.taxOptions;
         const { taxCalculationStrategy, taxZoneStrategy } = this.configService.taxOptions;
         const { jobQueueStrategy } = this.configService.jobQueueOptions;
         const { jobQueueStrategy } = this.configService.jobQueueOptions;
-        const { mergeStrategy } = this.configService.orderOptions;
+        const { mergeStrategy, priceCalculationStrategy } = this.configService.orderOptions;
         const { entityIdStrategy } = this.configService;
         const { entityIdStrategy } = this.configService;
         return [
         return [
             assetNamingStrategy,
             assetNamingStrategy,
@@ -142,6 +142,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
             jobQueueStrategy,
             jobQueueStrategy,
             mergeStrategy,
             mergeStrategy,
             entityIdStrategy,
             entityIdStrategy,
+            priceCalculationStrategy,
         ];
         ];
     }
     }
 
 

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

@@ -12,6 +12,7 @@ import { defaultCollectionFilters } from './collection/default-collection-filter
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
 import { DefaultLogger } from './logger/default-logger';
 import { DefaultLogger } from './logger/default-logger';
 import { TypeOrmLogger } from './logger/typeorm-logger';
 import { TypeOrmLogger } from './logger/typeorm-logger';
+import { DefaultPriceCalculationStrategy } from './order/default-price-calculation-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
 import { defaultPromotionActions } from './promotion/default-promotion-actions';
 import { defaultPromotionActions } from './promotion/default-promotion-actions';
@@ -83,6 +84,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     },
     },
     orderOptions: {
     orderOptions: {
         orderItemsLimit: 999,
         orderItemsLimit: 999,
+        priceCalculationStrategy: new DefaultPriceCalculationStrategy(),
         mergeStrategy: new MergeOrdersStrategy(),
         mergeStrategy: new MergeOrdersStrategy(),
         checkoutMergeStrategy: new UseGuestStrategy(),
         checkoutMergeStrategy: new UseGuestStrategy(),
         process: {},
         process: {},

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

@@ -16,6 +16,7 @@ export * from './logger/noop-logger';
 export * from './logger/vendure-logger';
 export * from './logger/vendure-logger';
 export * from './merge-config';
 export * from './merge-config';
 export * from './order/order-merge-strategy';
 export * from './order/order-merge-strategy';
+export * from './order/price-calculation-strategy';
 export * from './payment-method/example-payment-method-handler';
 export * from './payment-method/example-payment-method-handler';
 export * from './payment-method/payment-method-handler';
 export * from './payment-method/payment-method-handler';
 export * from './promotion/default-promotion-actions';
 export * from './promotion/default-promotion-actions';

+ 14 - 0
packages/core/src/config/order/default-price-calculation-strategy.ts

@@ -0,0 +1,14 @@
+import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+
+import { CalculatedPrice, PriceCalculationStrategy } from './price-calculation-strategy';
+
+/**
+ * @description
+ * The default {@link PriceCalculationStrategy}, which simply passes through the price of
+ * the ProductVariant without performing any calculations
+ */
+export class DefaultPriceCalculationStrategy implements PriceCalculationStrategy {
+    calculateUnitPrice(productVariant: ProductVariant): CalculatedPrice | Promise<CalculatedPrice> {
+        return productVariant;
+    }
+}

+ 60 - 0
packages/core/src/config/order/price-calculation-strategy.ts

@@ -0,0 +1,60 @@
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+
+/**
+ * @description
+ * The result of the price calculation from the {@link PriceCalculationStrategy}.
+ *
+ * @docsCateogory Orders
+ */
+export type CalculatedPrice = {
+    price: number;
+    priceIncludesTax: boolean;
+};
+
+/**
+ * @description
+ * The PriceCalculationStrategy defines the price of an OrderItem when a ProductVariant gets added
+ * to an order via the `addItemToOrder` mutation. By default the {@link DefaultPriceCalculationStrategy}
+ * is used.
+ *
+ * ### PriceCalculationStrategy vs Promotions
+ * Both the PriceCalculationStrategy and Promotions can be used to alter the price paid for a product.
+ *
+ * Use PriceCalculationStrategy if:
+ *
+ * * The price is not dependent on quantity or on the other contents of the Order.
+ * * The price calculation is based on the properties of the ProductVariant and any CustomFields
+ *   specified on the OrderLine, for example via a product configurator.
+ * * The logic is a permanent part of your business requirements.
+ *
+ * Use Promotions if:
+ *
+ * * You want to implement "discounts" and "special offers"
+ * * The calculation is not a permanent part of your business requirements.
+ * * The price depends on dynamic aspects such as quantities and which other
+ *   ProductVariants are in the Order.
+ * * The configuration of the logic needs to be manipulated via the Admin UI.
+ *
+ * ### Example use-cases
+ *
+ * A custom PriceCalculationStrategy can be used to implement things like:
+ *
+ * * A gift-wrapping service, where a boolean custom field is defined on the OrderLine. If `true`,
+ *   a gift-wrapping surcharge would be added to the price.
+ * * A product-configurator where e.g. various finishes, colors, and materials can be selected and stored
+ *   as OrderLine custom fields.
+ *
+ * @docsCateogory Orders
+ */
+export interface PriceCalculationStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
+     * the price for a single unit.
+     */
+    calculateUnitPrice(
+        productVariant: ProductVariant,
+        orderLineCustomFields: { [key: string]: any },
+    ): CalculatedPrice | Promise<CalculatedPrice>;
+}

+ 42 - 0
packages/core/src/config/promotion/promotion-action.ts

@@ -17,12 +17,29 @@ import { PromotionUtils } from './promotion-condition';
 export type PromotionActionArgType = ConfigArgSubset<'int' | 'facetValueIds'>;
 export type PromotionActionArgType = ConfigArgSubset<'int' | 'facetValueIds'>;
 export type PromotionActionArgs = ConfigArgs<PromotionActionArgType>;
 export type PromotionActionArgs = ConfigArgs<PromotionActionArgType>;
 
 
+/**
+ * @description
+ * The function which is used by a PromotionItemAction to calculate the
+ * discount on the OrderItem.
+ *
+ * @docsCategory promotions
+ * @docsPage promotion-action
+ */
 export type ExecutePromotionItemActionFn<T extends PromotionActionArgs> = (
 export type ExecutePromotionItemActionFn<T extends PromotionActionArgs> = (
     orderItem: OrderItem,
     orderItem: OrderItem,
     orderLine: OrderLine,
     orderLine: OrderLine,
     args: ConfigArgValues<T>,
     args: ConfigArgValues<T>,
     utils: PromotionUtils,
     utils: PromotionUtils,
 ) => number | Promise<number>;
 ) => number | Promise<number>;
+
+/**
+ * @description
+ * The function which is used by a PromotionOrderAction to calculate the
+ * discount on the Order.
+ *
+ * @docsCategory promotions
+ * @docsPage promotion-action
+ */
 export type ExecutePromotionOrderActionFn<T extends PromotionActionArgs> = (
 export type ExecutePromotionOrderActionFn<T extends PromotionActionArgs> = (
     order: Order,
     order: Order,
     args: ConfigArgValues<T>,
     args: ConfigArgValues<T>,
@@ -33,10 +50,32 @@ export interface PromotionActionConfig<T extends PromotionActionArgs>
     extends ConfigurableOperationDefOptions<T> {
     extends ConfigurableOperationDefOptions<T> {
     priorityValue?: number;
     priorityValue?: number;
 }
 }
+
+/**
+ * @description
+ *
+ * @docsCategory promotions
+ * @docsPage promotion-action
+ */
 export interface PromotionItemActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
 export interface PromotionItemActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
+    /**
+     * @description
+     * The function which contains the promotion calculation logic.
+     */
     execute: ExecutePromotionItemActionFn<T>;
     execute: ExecutePromotionItemActionFn<T>;
 }
 }
+
+/**
+ * @description
+ *
+ * @docsCategory promotions
+ * @docsPage promotion-action
+ */
 export interface PromotionOrderActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
 export interface PromotionOrderActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
+    /**
+     * @description
+     * The function which contains the promotion calculation logic.
+     */
     execute: ExecutePromotionOrderActionFn<T>;
     execute: ExecutePromotionOrderActionFn<T>;
 }
 }
 
 
@@ -45,6 +84,7 @@ export interface PromotionOrderActionConfig<T extends PromotionActionArgs> exten
  * An abstract class which is extended by {@link PromotionItemAction} and {@link PromotionOrderAction}.
  * An abstract class which is extended by {@link PromotionItemAction} and {@link PromotionOrderAction}.
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-action
  */
  */
 export abstract class PromotionAction<T extends PromotionActionArgs = {}> extends ConfigurableOperationDef<
 export abstract class PromotionAction<T extends PromotionActionArgs = {}> extends ConfigurableOperationDef<
     T
     T
@@ -75,6 +115,7 @@ export abstract class PromotionAction<T extends PromotionActionArgs = {}> extend
  * ```
  * ```
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-action
  */
  */
 export class PromotionItemAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
 export class PromotionItemAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
     private readonly executeFn: ExecutePromotionItemActionFn<T>;
     private readonly executeFn: ExecutePromotionItemActionFn<T>;
@@ -107,6 +148,7 @@ export class PromotionItemAction<T extends PromotionActionArgs = {}> extends Pro
  * ```
  * ```
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-action
  */
  */
 export class PromotionOrderAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
 export class PromotionOrderAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
     private readonly executeFn: ExecutePromotionOrderActionFn<T>;
     private readonly executeFn: ExecutePromotionOrderActionFn<T>;

+ 3 - 0
packages/core/src/config/promotion/promotion-condition.ts

@@ -25,6 +25,7 @@ export type PromotionConditionArgs = ConfigArgs<PromotionConditionArgType>;
  * TODO: Remove this and use the new init() method to inject providers where needed.
  * TODO: Remove this and use the new init() method to inject providers where needed.
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-condition
  */
  */
 export interface PromotionUtils {
 export interface PromotionUtils {
     /**
     /**
@@ -41,6 +42,7 @@ export interface PromotionUtils {
  * A function which checks whether or not a given {@link Order} satisfies the {@link PromotionCondition}.
  * A function which checks whether or not a given {@link Order} satisfies the {@link PromotionCondition}.
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-condition
  */
  */
 export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
 export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
     order: Order,
     order: Order,
@@ -61,6 +63,7 @@ export interface PromotionConditionConfig<T extends PromotionConditionArgs>
  * `true` if the Order satisfies the condition, or `false` if it does not.
  * `true` if the Order satisfies the condition, or `false` if it does not.
  *
  *
  * @docsCategory promotions
  * @docsCategory promotions
+ * @docsPage promotion-condition
  */
  */
 export class PromotionCondition<T extends PromotionConditionArgs = {}> extends ConfigurableOperationDef<T> {
 export class PromotionCondition<T extends PromotionConditionArgs = {}> extends ConfigurableOperationDef<T> {
     readonly priorityValue: number;
     readonly priorityValue: number;

+ 9 - 0
packages/core/src/config/vendure-config.ts

@@ -21,6 +21,7 @@ import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
 import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { VendureLogger } from './logger/vendure-logger';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
+import { PriceCalculationStrategy } from './order/price-calculation-strategy';
 import { PaymentMethodHandler } from './payment-method/payment-method-handler';
 import { PaymentMethodHandler } from './payment-method/payment-method-handler';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionAction } from './promotion/promotion-action';
 import { PromotionCondition } from './promotion/promotion-condition';
 import { PromotionCondition } from './promotion/promotion-condition';
@@ -238,6 +239,14 @@ export interface OrderOptions {
      * @default 999
      * @default 999
      */
      */
     orderItemsLimit?: number;
     orderItemsLimit?: number;
+    /**
+     * @description
+     * Defines the logic used to calculate the unit price of an OrderItem when adding an
+     * item to an Order.
+     *
+     * @default DefaultPriceCalculationStrategy
+     */
+    priceCalculationStrategy?: PriceCalculationStrategy;
     /**
     /**
      * @description
      * @description
      * Defines custom states and transition logic for the order process state machine.
      * Defines custom states and transition logic for the order process state machine.

+ 39 - 33
packages/core/src/service/services/order.service.ts

@@ -113,7 +113,7 @@ export class OrderService {
             .addOrderBy('items.createdAt', 'ASC')
             .addOrderBy('items.createdAt', 'ASC')
             .getOne();
             .getOne();
         if (order) {
         if (order) {
-            order.lines.forEach(line => {
+            order.lines.forEach((line) => {
                 line.productVariant = translateDeep(
                 line.productVariant = translateDeep(
                     this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
                     this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
                     ctx.languageCode,
                     ctx.languageCode,
@@ -145,8 +145,8 @@ export class OrderService {
             .andWhere('order.customer.id = :customerId', { customerId })
             .andWhere('order.customer.id = :customerId', { customerId })
             .getManyAndCount()
             .getManyAndCount()
             .then(([items, totalItems]) => {
             .then(([items, totalItems]) => {
-                items.forEach(item => {
-                    item.lines.forEach(line => {
+                items.forEach((item) => {
+                    item.lines.forEach((line) => {
                         line.productVariant = translateDeep(line.productVariant, ctx.languageCode, [
                         line.productVariant = translateDeep(line.productVariant, ctx.languageCode, [
                             'options',
                             'options',
                         ]);
                         ]);
@@ -231,7 +231,7 @@ export class OrderService {
         this.assertAddingItemsState(order);
         this.assertAddingItemsState(order);
         this.assertNotOverOrderItemsLimit(order, quantity);
         this.assertNotOverOrderItemsLimit(order, quantity);
         const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
         const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
-        let orderLine = order.lines.find(line => {
+        let orderLine = order.lines.find((line) => {
             return (
             return (
                 idsAreEqual(line.productVariant.id, productVariantId) &&
                 idsAreEqual(line.productVariant.id, productVariantId) &&
                 JSON.stringify(line.customFields) === JSON.stringify(customFields)
                 JSON.stringify(line.customFields) === JSON.stringify(customFields)
@@ -254,11 +254,15 @@ export class OrderService {
         quantity?: number | null,
         quantity?: number | null,
         customFields?: { [key: string]: any },
         customFields?: { [key: string]: any },
     ): Promise<Order> {
     ): Promise<Order> {
+        const { priceCalculationStrategy } = this.configService.orderOptions;
         const order =
         const order =
             orderIdOrOrder instanceof Order
             orderIdOrOrder instanceof Order
                 ? orderIdOrOrder
                 ? orderIdOrOrder
                 : await this.getOrderOrThrow(ctx, orderIdOrOrder);
                 : await this.getOrderOrThrow(ctx, orderIdOrOrder);
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
+        if (customFields != null) {
+            orderLine.customFields = customFields;
+        }
         this.assertAddingItemsState(order);
         this.assertAddingItemsState(order);
         if (quantity != null) {
         if (quantity != null) {
             this.assertQuantityIsPositive(quantity);
             this.assertQuantityIsPositive(quantity);
@@ -269,12 +273,16 @@ export class OrderService {
                     orderLine.items = [];
                     orderLine.items = [];
                 }
                 }
                 const productVariant = orderLine.productVariant;
                 const productVariant = orderLine.productVariant;
+                const calculatedPrice = await priceCalculationStrategy.calculateUnitPrice(
+                    productVariant,
+                    orderLine.customFields || {},
+                );
                 for (let i = currentQuantity; i < quantity; i++) {
                 for (let i = currentQuantity; i < quantity; i++) {
                     const orderItem = await this.connection.getRepository(OrderItem).save(
                     const orderItem = await this.connection.getRepository(OrderItem).save(
                         new OrderItem({
                         new OrderItem({
-                            unitPrice: productVariant.price,
+                            unitPrice: calculatedPrice.price,
                             pendingAdjustments: [],
                             pendingAdjustments: [],
-                            unitPriceIncludesTax: productVariant.priceIncludesTax,
+                            unitPriceIncludesTax: calculatedPrice.priceIncludesTax,
                             taxRate: productVariant.priceIncludesTax
                             taxRate: productVariant.priceIncludesTax
                                 ? productVariant.taxRateApplied.value
                                 ? productVariant.taxRateApplied.value
                                 : 0,
                                 : 0,
@@ -286,9 +294,6 @@ export class OrderService {
                 orderLine.items = orderLine.items.slice(0, quantity);
                 orderLine.items = orderLine.items.slice(0, quantity);
             }
             }
         }
         }
-        if (customFields != null) {
-            orderLine.customFields = customFields;
-        }
         await this.connection.getRepository(OrderLine).save(orderLine, { reload: false });
         await this.connection.getRepository(OrderLine).save(orderLine, { reload: false });
         return this.applyPriceAdjustments(ctx, order, orderLine);
         return this.applyPriceAdjustments(ctx, order, orderLine);
     }
     }
@@ -297,7 +302,7 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         this.assertAddingItemsState(order);
         this.assertAddingItemsState(order);
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
-        order.lines = order.lines.filter(line => !idsAreEqual(line.id, orderLineId));
+        order.lines = order.lines.filter((line) => !idsAreEqual(line.id, orderLineId));
         const updatedOrder = await this.applyPriceAdjustments(ctx, order);
         const updatedOrder = await this.applyPriceAdjustments(ctx, order);
         await this.connection.getRepository(OrderLine).remove(orderLine);
         await this.connection.getRepository(OrderLine).remove(orderLine);
         return updatedOrder;
         return updatedOrder;
@@ -325,7 +330,7 @@ export class OrderService {
     async removeCouponCode(ctx: RequestContext, orderId: ID, couponCode: string) {
     async removeCouponCode(ctx: RequestContext, orderId: ID, couponCode: string) {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.couponCodes.includes(couponCode)) {
         if (order.couponCodes.includes(couponCode)) {
-            order.couponCodes = order.couponCodes.filter(cc => cc !== couponCode);
+            order.couponCodes = order.couponCodes.filter((cc) => cc !== couponCode);
             await this.historyService.createHistoryEntryForOrder({
             await this.historyService.createHistoryEntryForOrder({
                 ctx,
                 ctx,
                 orderId: order.id,
                 orderId: order.id,
@@ -359,7 +364,7 @@ export class OrderService {
     async getEligibleShippingMethods(ctx: RequestContext, orderId: ID): Promise<ShippingMethodQuote[]> {
     async getEligibleShippingMethods(ctx: RequestContext, orderId: ID): Promise<ShippingMethodQuote[]> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
-        return eligibleMethods.map(eligible => ({
+        return eligibleMethods.map((eligible) => ({
             id: eligible.method.id as string,
             id: eligible.method.id as string,
             price: eligible.result.price,
             price: eligible.result.price,
             priceWithTax: eligible.result.priceWithTax,
             priceWithTax: eligible.result.priceWithTax,
@@ -372,7 +377,7 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         this.assertAddingItemsState(order);
         this.assertAddingItemsState(order);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
-        const selectedMethod = eligibleMethods.find(m => idsAreEqual(m.method.id, shippingMethodId));
+        const selectedMethod = eligibleMethods.find((m) => idsAreEqual(m.method.id, shippingMethodId));
         if (!selectedMethod) {
         if (!selectedMethod) {
             throw new UserInputError(`error.shipping-method-unavailable`);
             throw new UserInputError(`error.shipping-method-unavailable`);
         }
         }
@@ -413,7 +418,7 @@ export class OrderService {
 
 
         function totalIsCovered(state: PaymentState): boolean {
         function totalIsCovered(state: PaymentState): boolean {
             return (
             return (
-                order.payments.filter(p => p.state === state).reduce((sum, p) => sum + p.amount, 0) ===
+                order.payments.filter((p) => p.state === state).reduce((sum, p) => sum + p.amount, 0) ===
                 order.total
                 order.total
             );
             );
         }
         }
@@ -456,7 +461,7 @@ export class OrderService {
         }
         }
         const { items, orders } = await this.getOrdersAndItemsFromLines(
         const { items, orders } = await this.getOrdersAndItemsFromLines(
             input.lines,
             input.lines,
-            i => !i.fulfillment,
+            (i) => !i.fulfillment,
             'error.create-fulfillment-items-already-fulfilled',
             'error.create-fulfillment-items-already-fulfilled',
         );
         );
 
 
@@ -491,8 +496,8 @@ export class OrderService {
             }
             }
             const allOrderItemsFulfilled = orderWithFulfillments.lines
             const allOrderItemsFulfilled = orderWithFulfillments.lines
                 .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
                 .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
-                .filter(orderItem => !orderItem.cancelled)
-                .every(orderItem => {
+                .filter((orderItem) => !orderItem.cancelled)
+                .every((orderItem) => {
                     return !!orderItem.fulfillment;
                     return !!orderItem.fulfillment;
                 });
                 });
             if (allOrderItemsFulfilled) {
             if (allOrderItemsFulfilled) {
@@ -522,7 +527,7 @@ export class OrderService {
             });
             });
         }
         }
         const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
         const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
-        return unique(items.map(i => i.fulfillment).filter(notNullOrUndefined), 'id');
+        return unique(items.map((i) => i.fulfillment).filter(notNullOrUndefined), 'id');
     }
     }
 
 
     async getFulfillmentOrderItems(id: ID): Promise<OrderItem[]> {
     async getFulfillmentOrderItems(id: ID): Promise<OrderItem[]> {
@@ -550,7 +555,7 @@ export class OrderService {
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
             return true;
             return true;
         } else {
         } else {
-            const lines: OrderLineInput[] = order.lines.map(l => ({
+            const lines: OrderLineInput[] = order.lines.map((l) => ({
                 orderLineId: l.id as string,
                 orderLineId: l.id as string,
                 quantity: l.quantity,
                 quantity: l.quantity,
             }));
             }));
@@ -568,7 +573,7 @@ export class OrderService {
         }
         }
         const { items, orders } = await this.getOrdersAndItemsFromLines(
         const { items, orders } = await this.getOrdersAndItemsFromLines(
             lines,
             lines,
-            i => !i.cancelled,
+            (i) => !i.cancelled,
             'error.cancel-order-lines-quantity-too-high',
             'error.cancel-order-lines-quantity-too-high',
         );
         );
         if (1 < orders.length) {
         if (1 < orders.length) {
@@ -586,7 +591,7 @@ export class OrderService {
 
 
         // Perform the cancellation
         // Perform the cancellation
         await this.stockMovementService.createCancellationsForOrderItems(items);
         await this.stockMovementService.createCancellationsForOrderItems(items);
-        items.forEach(i => (i.cancelled = true));
+        items.forEach((i) => (i.cancelled = true));
         await this.connection.getRepository(OrderItem).save(items, { reload: false });
         await this.connection.getRepository(OrderItem).save(items, { reload: false });
 
 
         const orderWithItems = await this.connection.getRepository(Order).findOne(order.id, {
         const orderWithItems = await this.connection.getRepository(Order).findOne(order.id, {
@@ -600,13 +605,13 @@ export class OrderService {
             orderId: order.id,
             orderId: order.id,
             type: HistoryEntryType.ORDER_CANCELLATION,
             type: HistoryEntryType.ORDER_CANCELLATION,
             data: {
             data: {
-                orderItemIds: items.map(i => i.id),
+                orderItemIds: items.map((i) => i.id),
                 reason: input.reason || undefined,
                 reason: input.reason || undefined,
             },
             },
         });
         });
         const allOrderItemsCancelled = orderWithItems.lines
         const allOrderItemsCancelled = orderWithItems.lines
             .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
             .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
-            .every(orderItem => orderItem.cancelled);
+            .every((orderItem) => orderItem.cancelled);
         return allOrderItemsCancelled;
         return allOrderItemsCancelled;
     }
     }
 
 
@@ -621,7 +626,7 @@ export class OrderService {
         }
         }
         const { items, orders } = await this.getOrdersAndItemsFromLines(
         const { items, orders } = await this.getOrdersAndItemsFromLines(
             input.lines,
             input.lines,
-            i => !i.cancelled,
+            (i) => !i.cancelled,
             'error.refund-order-lines-quantity-too-high',
             'error.refund-order-lines-quantity-too-high',
         );
         );
         if (1 < orders.length) {
         if (1 < orders.length) {
@@ -643,7 +648,7 @@ export class OrderService {
                 state: order.state,
                 state: order.state,
             });
             });
         }
         }
-        if (items.some(i => !!i.refundId)) {
+        if (items.some((i) => !!i.refundId)) {
             throw new IllegalOperationError('error.refund-order-item-already-refunded');
             throw new IllegalOperationError('error.refund-order-item-already-refunded');
         }
         }
 
 
@@ -680,7 +685,7 @@ export class OrderService {
                 try {
                 try {
                     await this.promotionService.validateCouponCode(couponCode, customer.id);
                     await this.promotionService.validateCouponCode(couponCode, customer.id);
                 } catch (err) {
                 } catch (err) {
-                    order.couponCodes = order.couponCodes.filter(c => c !== couponCode);
+                    order.couponCodes = order.couponCodes.filter((c) => c !== couponCode);
                     codesRemoved = true;
                     codesRemoved = true;
                 }
                 }
             }
             }
@@ -761,7 +766,7 @@ export class OrderService {
     }
     }
 
 
     private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
     private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
-        const orderItem = order.lines.find(line => idsAreEqual(line.id, orderLineId));
+        const orderItem = order.lines.find((line) => idsAreEqual(line.id, orderLineId));
         if (!orderItem) {
         if (!orderItem) {
             throw new UserInputError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
             throw new UserInputError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
         }
         }
@@ -841,14 +846,15 @@ export class OrderService {
         const orders = new Map<ID, Order>();
         const orders = new Map<ID, Order>();
         const items = new Map<ID, OrderItem>();
         const items = new Map<ID, OrderItem>();
 
 
-        const lines = await this.connection
-            .getRepository(OrderLine)
-            .findByIds(orderLinesInput.map(l => l.orderLineId), {
+        const lines = await this.connection.getRepository(OrderLine).findByIds(
+            orderLinesInput.map((l) => l.orderLineId),
+            {
                 relations: ['order', 'items', 'items.fulfillment'],
                 relations: ['order', 'items', 'items.fulfillment'],
                 order: { id: 'ASC' },
                 order: { id: 'ASC' },
-            });
+            },
+        );
         for (const line of lines) {
         for (const line of lines) {
-            const inputLine = orderLinesInput.find(l => idsAreEqual(l.orderLineId, line.id));
+            const inputLine = orderLinesInput.find((l) => idsAreEqual(l.orderLineId, line.id));
             if (!inputLine) {
             if (!inputLine) {
                 continue;
                 continue;
             }
             }
@@ -860,7 +866,7 @@ export class OrderService {
             if (matchingItems.length < inputLine.quantity) {
             if (matchingItems.length < inputLine.quantity) {
                 throw new IllegalOperationError(noMatchesError);
                 throw new IllegalOperationError(noMatchesError);
             }
             }
-            matchingItems.slice(0, inputLine.quantity).forEach(item => {
+            matchingItems.slice(0, inputLine.quantity).forEach((item) => {
                 items.set(item.id, item);
                 items.set(item.id, item);
             });
             });
         }
         }