Ver código fonte

feat(core): Add quantity arg to OrderItemPriceCalculationStrategy

Relates to #1920. This feature is the fundamental piece that allows price lists /
tiered pricing to be implemented. The exact implementation will depend on the
project requirements.
Michael Bromley 2 anos atrás
pai
commit
02a08648da

+ 10 - 3
packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts

@@ -1,23 +1,30 @@
 import {
-    CalculatedPrice,
+    PriceCalculationResult,
+    Order,
     OrderItemPriceCalculationStrategy,
     ProductVariant,
     RequestContext,
+    roundMoney,
 } from '@vendure/core';
 
 /**
- * Adds $5 for items with gift wrapping.
+ * Adds $5 for items with gift wrapping, halves the price when buying 3 or more
  */
 export class TestOrderItemPriceCalculationStrategy implements OrderItemPriceCalculationStrategy {
     calculateUnitPrice(
         ctx: RequestContext,
         productVariant: ProductVariant,
         orderLineCustomFields: { [p: string]: any },
-    ): CalculatedPrice | Promise<CalculatedPrice> {
+        order: Order,
+        quantity: number,
+    ): PriceCalculationResult | Promise<PriceCalculationResult> {
         let price = productVariant.price;
         if (orderLineCustomFields.giftWrap) {
             price += 500;
         }
+        if (quantity > 3) {
+            price = roundMoney(price / 2);
+        }
         return {
             price,
             priceIncludesTax: productVariant.listPriceIncludesTax,

+ 14 - 0
packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts

@@ -98,6 +98,20 @@ describe('custom OrderItemPriceCalculationStrategy', () => {
         expect(adjustOrderLine.lines[1].unitPrice).toEqual(variantPrice);
         expect(adjustOrderLine.subTotal).toEqual(variantPrice + variantPrice);
     });
+
+    it('applies discount for quantity greater than 3', async () => {
+        const { adjustOrderLine } = await shopClient.query(ADJUST_ORDER_LINE_CUSTOM_FIELDS, {
+            orderLineId: secondOrderLineId,
+            quantity: 4,
+            customFields: {
+                giftWrap: false,
+            },
+        });
+
+        const variantPrice = (variants[0].price as SinglePrice).value;
+        expect(adjustOrderLine.lines[1].unitPrice).toEqual(variantPrice / 2);
+        expect(adjustOrderLine.subTotal).toEqual(variantPrice + (variantPrice / 2) * 4);
+    });
 });
 
 const ORDER_WITH_LINES_AND_ITEMS_FRAGMENT = gql`

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

@@ -8,6 +8,7 @@ export * from './injector';
 export * from './permission-definition';
 export * from './ttl-cache';
 export * from './self-refreshing-cache';
+export * from './round-money';
 export * from './types/common-types';
 export * from './types/entity-relation-paths';
 export * from './types/injectable-strategy';

+ 7 - 0
packages/core/src/common/round-money.ts

@@ -3,6 +3,13 @@ import { MoneyStrategy } from '../config/entity/money-strategy';
 
 let moneyStrategy: MoneyStrategy;
 
+/**
+ * @description
+ * Rounds a monetary value according to the configured {@link MoneyStrategy}.
+ *
+ * @docsCategory money
+ * @since 2.0.0
+ */
 export function roundMoney(value: number, quantity = 1): number {
     if (!moneyStrategy) {
         moneyStrategy = getConfig().entityOptions.moneyStrategy;

+ 12 - 3
packages/core/src/config/order/order-item-price-calculation-strategy.ts

@@ -18,9 +18,13 @@ import { ProductVariant } from '../../entity/product-variant/product-variant.ent
  * ### OrderItemPriceCalculationStrategy vs Promotions
  * Both the OrderItemPriceCalculationStrategy and Promotions can be used to alter the price paid for a product.
  *
+ * The main difference is when a Promotion is applied, it adds a `discount` line to the Order, and the regular
+ * price is used for the value of `OrderLine.listPrice` property, whereas
+ * the OrderItemPriceCalculationStrategy actually alters the value of `OrderLine.listPrice` itself, and does not
+ * add any discounts to the Order.
+ *
  * Use OrderItemPriceCalculationStrategy 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.
@@ -41,6 +45,8 @@ import { ProductVariant } from '../../entity/product-variant/product-variant.ent
  *   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 (see [Customizing models](/docs/developer-guide/customizing-models/#configurable-order-products).
+ * * Price lists or bulk pricing, where different price bands are stored e.g. in a customField on the ProductVariant, and this
+ *   is used to calculate the price based on the current quantity.
  *
  * @docsCategory Orders
  */
@@ -51,13 +57,16 @@ export interface OrderItemPriceCalculationStrategy extends InjectableStrategy {
      * the price for a single unit.
      *
      * Note: if you have any `relation` type custom fields defined on the OrderLine entity, they will only be
-     * passed in to this method if they are set to `eager: true`.
+     * passed in to this method if they are set to `eager: true`. Otherwise, you can use the {@link EntityHydrator}
+     * to join the missing relations.
+     *
+     * Note: the `quantity` argument was added in v2.0.0
      */
     calculateUnitPrice(
         ctx: RequestContext,
         productVariant: ProductVariant,
         orderLineCustomFields: { [key: string]: any },
         order: Order,
-        // TODO: v2 - pass the quantity to allow bulk discounts
+        quantity: number,
     ): PriceCalculationResult | Promise<PriceCalculationResult>;
 }

+ 1 - 0
packages/core/src/service/services/order-testing.service.ts

@@ -138,6 +138,7 @@ export class OrderTestingService {
                 productVariant,
                 orderLine.customFields || {},
                 mockOrder,
+                orderLine.quantity,
             );
             const taxRate = productVariant.taxRateApplied;
             orderLine.listPrice = price;

+ 1 - 0
packages/core/src/service/services/order.service.ts

@@ -1670,6 +1670,7 @@ export class OrderService {
                     variant,
                     updatedOrderLine.customFields || {},
                     order,
+                    updatedOrderLine.quantity,
                 );
                 const initialListPrice = updatedOrderLine.initialListPrice ?? priceResult.price;
                 if (initialListPrice !== priceResult.price) {