Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Add maxDiscountAmount to PromotionItemAction #2964

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/e2e/fixtures/test-money-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DefaultMoneyStrategy } from '@vendure/core';

export class TestMoneyStrategy extends DefaultMoneyStrategy {
round(value: number, quantity = 1): number {
return Math.round(value * quantity);
}
}
157 changes: 127 additions & 30 deletions packages/core/e2e/order-promotion.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import { initialData } from '../../../e2e-common/e2e-initial-data';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
import { freeShipping } from '../src/config/promotion/actions/free-shipping-action';
import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
import { orderLineFixedDiscount } from '../src/config/promotion/actions/order-line-fixed-discount-action';

import { TestMoneyStrategy } from './fixtures/test-money-strategy';
import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
import { CurrencyCode, HistoryEntryType, LanguageCode } from './graphql/generated-e2e-admin-types';
import * as Codegen from './graphql/generated-e2e-admin-types';
Expand Down Expand Up @@ -66,6 +68,9 @@ describe('Promotions applied to Orders', () => {
paymentOptions: {
paymentMethodHandlers: [testSuccessfulPaymentMethod],
},
entityOptions: {
moneyStrategy: new TestMoneyStrategy(),
},
}),
);

Expand Down Expand Up @@ -925,9 +930,8 @@ describe('Promotions applied to Orders', () => {
expect(removeCouponCode!.total).toBe(2200);
expect(removeCouponCode!.totalWithTax).toBe(2640);

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
expect(activeOrder!.total).toBe(2200);
expect(activeOrder!.totalWithTax).toBe(2640);
Expand Down Expand Up @@ -986,9 +990,8 @@ describe('Promotions applied to Orders', () => {
expect(removeCouponCode!.total).toBe(2200);
expect(removeCouponCode!.totalWithTax).toBe(2640);

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
expect(activeOrder!.total).toBe(2200);
expect(activeOrder!.totalWithTax).toBe(2640);
Expand Down Expand Up @@ -1534,9 +1537,8 @@ describe('Promotions applied to Orders', () => {

await addGuestCustomerToOrder();

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(activeOrder!.couponCodes).toEqual([]);
expect(activeOrder!.totalWithTax).toBe(6000);
});
Expand Down Expand Up @@ -1627,9 +1629,8 @@ describe('Promotions applied to Orders', () => {

await logInAsRegisteredCustomer();

const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(activeOrder!.totalWithTax).toBe(6000);
expect(activeOrder!.couponCodes).toEqual([]);
});
Expand Down Expand Up @@ -1883,9 +1884,8 @@ describe('Promotions applied to Orders', () => {
expect(addItemToOrder.discounts.length).toBe(1);
expect(addItemToOrder.discounts[0].description).toBe('Test Promo');

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check1!.discounts.length).toBe(1);
expect(check1!.discounts[0].description).toBe('Test Promo');

Expand All @@ -1899,9 +1899,8 @@ describe('Promotions applied to Orders', () => {
orderResultGuard.assertSuccess(removeOrderLine);
expect(removeOrderLine.discounts.length).toBe(0);

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.discounts.length).toBe(0);
});

Expand Down Expand Up @@ -2043,9 +2042,8 @@ describe('Promotions applied to Orders', () => {
quantity: 1,
});

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);
Expand All @@ -2055,9 +2053,8 @@ describe('Promotions applied to Orders', () => {
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check2!.totalWithTax).toBe(0);
});
Expand All @@ -2080,9 +2077,8 @@ describe('Promotions applied to Orders', () => {
quantity: 1,
});

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check1 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);
Expand All @@ -2092,14 +2088,115 @@ describe('Promotions applied to Orders', () => {
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

const { activeOrder: check2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
const { activeOrder: check2 } =
await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check2!.totalWithTax).toBe(0);
});
});

// https://github.com/vendure-ecommerce/vendure/issues/2956
describe('Order promotion with ExecutePromotionActionResult', () => {
const lineDiscountCouponCode = 'LINE_DISCOUNT';
const unitDiscountCouponCode = 'UNIT_DISCOUNT';
let lineDiscountPromotion: Codegen.PromotionFragment;
let unitDiscountPromotion: Codegen.PromotionFragment;

beforeAll(async () => {
lineDiscountPromotion = await createPromotion({
enabled: true,
name: 'Line discount promotion',
couponCode: lineDiscountCouponCode,
conditions: [],
actions: [
{
code: orderLineFixedDiscount.code,
arguments: [
{ name: 'discount', value: '800' },
{ name: 'discountMode', value: 'line' },
],
},
],
});
unitDiscountPromotion = await createPromotion({
enabled: true,
name: 'Unit discount promotion',
couponCode: unitDiscountCouponCode,
conditions: [],
actions: [
{
code: orderLineFixedDiscount.code,
arguments: [
{ name: 'discount', value: '300' },
{ name: 'discountMode', value: 'unit' },
],
},
],
});
});

afterAll(async () => {
await deletePromotion(lineDiscountPromotion.id);
await deletePromotion(unitDiscountPromotion.id);
});

it('should apply line discount promotion correctly', async () => {
shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
await shopClient.asAnonymousUser();
const { addItemToOrder } = await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: getVariantBySlug('item-1000').id,
quantity: 3,
});
orderResultGuard.assertSuccess(addItemToOrder);
expect(addItemToOrder.total).toBe(3000);
expect(addItemToOrder.totalWithTax).toBe(3600);
expect(addItemToOrder.discounts.length).toBe(0);

const { applyCouponCode } = await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, {
couponCode: lineDiscountCouponCode,
});
orderResultGuard.assertSuccess(applyCouponCode);
expect(applyCouponCode.discounts.length).toBe(1);
expect(applyCouponCode.discounts[0].description).toBe('Line discount promotion');
expect(applyCouponCode.total).toBe(2200);
expect(applyCouponCode.totalWithTax).toBe(2640);
});

it('should apply unit discount promotion correctly', async () => {
shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
await shopClient.asAnonymousUser();
const { addItemToOrder } = await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: getVariantBySlug('item-1000').id,
quantity: 3,
});
orderResultGuard.assertSuccess(addItemToOrder);
expect(addItemToOrder.total).toBe(3000);
expect(addItemToOrder.totalWithTax).toBe(3600);
expect(addItemToOrder.discounts.length).toBe(0);

const { applyCouponCode } = await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, {
couponCode: unitDiscountCouponCode,
});
orderResultGuard.assertSuccess(applyCouponCode);
expect(applyCouponCode.discounts.length).toBe(1);
expect(applyCouponCode.discounts[0].description).toBe('Unit discount promotion');
expect(applyCouponCode.total).toBe(2100);
expect(applyCouponCode.totalWithTax).toBe(2520);
});
});

async function getProducts() {
const result = await adminClient.query<Codegen.GetProductsWithVariantPricesQuery>(
GET_PRODUCTS_WITH_VARIANT_PRICES,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';

import { PromotionItemAction } from '../promotion-action';

export const orderLineFixedDiscount = new PromotionItemAction({
code: 'order_line_fixed_discount',
args: {
discount: {
type: 'int',
ui: {
component: 'currency-form-input',
},
},
discountMode: {
type: 'string',
ui: {
component: 'select-form-input',
options: [
{ label: 'Unit', value: 'unit' },
{ label: 'Line', value: 'line' },
],
},
},
},
execute(ctx, orderLine, args) {
return {
amount: -args.discount,
discountMode: (args.discountMode as 'unit' | 'line') || 'line',
};
},
description: [{ languageCode: LanguageCode.en, value: 'Discount orderLine by fixed amount' }],
});
2 changes: 2 additions & 0 deletions packages/core/src/config/promotion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { buyXGetYFreeAction } from './actions/buy-x-get-y-free-action';
import { discountOnItemWithFacets } from './actions/facet-values-percentage-discount-action';
import { freeShipping } from './actions/free-shipping-action';
import { orderFixedDiscount } from './actions/order-fixed-discount-action';
import { orderLineFixedDiscount } from './actions/order-line-fixed-discount-action';
import { orderPercentageDiscount } from './actions/order-percentage-discount-action';
import { productsPercentageDiscount } from './actions/product-percentage-discount-action';
import { buyXGetYFreeCondition } from './conditions/buy-x-get-y-free-condition';
Expand All @@ -27,6 +28,7 @@ export * from './utils/facet-value-checker';

export const defaultPromotionActions = [
orderFixedDiscount,
orderLineFixedDiscount,
orderPercentageDiscount,
discountOnItemWithFacets,
productsPercentageDiscount,
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/config/promotion/promotion-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ export type ConditionState<
T extends [string, any] = TupleToUnion<CodesStateTuple<ConditionTuple<U>>>,
> = { [key in T[0]]: Extract<T, [key, any]>[1] };

/**
* @description
* The result of a PromotionItemAction's `execute` function.
*
* @docsCategory promotions
* @docsPage promotion-action
*/
type ExecutePromotionActionResult = {
amount: number;
discountMode: 'line' | 'unit';
};

/**
* @description
* The function which is used by a PromotionItemAction to calculate the
Expand All @@ -75,7 +87,7 @@ export type ExecutePromotionItemActionFn<T extends ConfigArgs, U extends Array<P
args: ConfigArgValues<T>,
state: ConditionState<U>,
promotion: Promotion,
) => number | Promise<number>;
) => number | Promise<number> | ExecutePromotionActionResult | Promise<ExecutePromotionActionResult>;

/**
* @description
Expand Down Expand Up @@ -273,7 +285,9 @@ export abstract class PromotionAction<
}

/** @internal */
abstract execute(...arg: any[]): number | Promise<number>;
abstract execute(
...arg: any[]
): number | Promise<number> | ExecutePromotionActionResult | Promise<ExecutePromotionActionResult>;

/** @internal */
onActivate(
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/entity/promotion/promotion.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,15 @@ export class Promotion
if (promotionAction instanceof PromotionItemAction) {
if (this.isOrderItemArg(args)) {
const { orderLine } = args;
amount += roundMoney(
await promotionAction.execute(ctx, orderLine, action.args, state, this),
);
const discount = await promotionAction.execute(ctx, orderLine, action.args, state, this);
if (typeof discount === 'number') {
amount += roundMoney(discount * orderLine.quantity);
} else {
amount +=
discount.discountMode === 'line'
? roundMoney(discount.amount)
: roundMoney(discount.amount * orderLine.quantity);
}
}
} else if (promotionAction instanceof PromotionOrderAction) {
if (this.isOrderArg(args)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ export class OrderCalculator {
// for (const item of line.items) {
const adjustment = await promotion.apply(ctx, { orderLine: line }, state);
if (adjustment) {
adjustment.amount = adjustment.amount * line.quantity;
line.addAdjustment(adjustment);
priceAdjusted = true;
}
Expand Down
Loading