diff --git a/packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts b/packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts index f10c148fc5..d0122a4356 100644 --- a/packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts +++ b/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 { + order: Order, + quantity: number, + ): PriceCalculationResult | Promise { let price = productVariant.price; if (orderLineCustomFields.giftWrap) { price += 500; } + if (quantity > 3) { + price = roundMoney(price / 2); + } return { price, priceIncludesTax: productVariant.listPriceIncludesTax, diff --git a/packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts b/packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts index 886a9a7df0..d06dccbf26 100644 --- a/packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts +++ b/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` diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index e9967498f8..e3167fac56 100644 --- a/packages/core/src/common/index.ts +++ b/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'; diff --git a/packages/core/src/common/round-money.ts b/packages/core/src/common/round-money.ts index c07dd0727e..2fe3084b1a 100644 --- a/packages/core/src/common/round-money.ts +++ b/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; diff --git a/packages/core/src/config/order/order-item-price-calculation-strategy.ts b/packages/core/src/config/order/order-item-price-calculation-strategy.ts index 2c4dad2dcb..f72e9b1e9c 100644 --- a/packages/core/src/config/order/order-item-price-calculation-strategy.ts +++ b/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; } diff --git a/packages/core/src/service/services/order-testing.service.ts b/packages/core/src/service/services/order-testing.service.ts index e19c4c8553..56603b46c2 100644 --- a/packages/core/src/service/services/order-testing.service.ts +++ b/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; diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts index ee9addff94..4ec439ab90 100644 --- a/packages/core/src/service/services/order.service.ts +++ b/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) {