Skip to content

Commit

Permalink
fix(core): Add a return type to the PromotionAction execute method an…
Browse files Browse the repository at this point in the history
…d write test code for it
  • Loading branch information
Feelw00 committed Jul 23, 2024
1 parent f6bdc5f commit 92c2655
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 44 deletions.
153 changes: 123 additions & 30 deletions packages/core/e2e/order-promotion.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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 { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
import { CurrencyCode, HistoryEntryType, LanguageCode } from './graphql/generated-e2e-admin-types';
Expand Down Expand Up @@ -925,9 +926,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 +986,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 +1533,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 +1625,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 +1880,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 +1895,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 +2038,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 +2049,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 +2073,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 +2084,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: 2,
});
orderResultGuard.assertSuccess(addItemToOrder);
expect(addItemToOrder.total).toBe(2000);
expect(addItemToOrder.totalWithTax).toBe(2400);
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(1200);
expect(applyCouponCode.totalWithTax).toBe(1440);
});

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: 2,
});
orderResultGuard.assertSuccess(addItemToOrder);
expect(addItemToOrder.total).toBe(2000);
expect(addItemToOrder.totalWithTax).toBe(2400);
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(1400);
expect(applyCouponCode.totalWithTax).toBe(1680);
});
});

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
20 changes: 17 additions & 3 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 @@ -281,7 +293,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 Expand Up @@ -362,7 +376,7 @@ export class PromotionItemAction<
promotion,
);
}

/**
* @description
* The maximum discount amount that can be applied to this action.
Expand Down
20 changes: 9 additions & 11 deletions packages/core/src/entity/promotion/promotion.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,15 @@ export class Promotion
if (promotionAction instanceof PromotionItemAction) {
if (this.isOrderItemArg(args)) {
const { orderLine } = args;
const discountAmount = roundMoney(
await promotionAction.execute(ctx, orderLine, action.args, state, this),
) * orderLine.quantity;
const maxDiscountAmount =
roundMoney(
await promotionAction.maxDiscountAmount(ctx, orderLine, action.args, state, this),
);
amount +=
maxDiscountAmount && maxDiscountAmount > discountAmount
? maxDiscountAmount
: discountAmount;
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

0 comments on commit 92c2655

Please sign in to comment.