Skip to content

Commit

Permalink
feat(core): Create PromotionLineAction (#2971)
Browse files Browse the repository at this point in the history
Closes #2956
  • Loading branch information
Feelw00 authored Jul 25, 2024
1 parent a835b3f commit 0ff8288
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 34 deletions.
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);
}
}
107 changes: 77 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 @@ -834,6 +839,58 @@ describe('Promotions applied to Orders', () => {
});
});

describe('orderLineFixedDiscount', () => {
const couponCode = '1000_off_order_line';
let promotion: Codegen.PromotionFragment;

beforeAll(async () => {
promotion = await createPromotion({
enabled: true,
name: '$1000 discount on order line',
couponCode,
conditions: [],
actions: [
{
code: orderLineFixedDiscount.code,
arguments: [{ name: 'discount', value: '1000' }],
},
],
});
});

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

it('prices exclude tax', async () => {
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.discounts.length).toBe(0);
expect(addItemToOrder.lines[0].discounts.length).toBe(0);
expect(addItemToOrder.total).toBe(3000);
expect(addItemToOrder.totalWithTax).toBe(3600);

const { applyCouponCode } = await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, {
couponCode,
});
orderResultGuard.assertSuccess(applyCouponCode);

expect(applyCouponCode.total).toBe(2000);
expect(applyCouponCode.totalWithTax).toBe(2400);
expect(applyCouponCode.lines[0].discounts.length).toBe(1);
});
});

describe('discountOnItemWithFacets', () => {
const couponCode = '50%_off_sale_items';
let promotion: Codegen.PromotionFragment;
Expand Down Expand Up @@ -925,9 +982,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 +1042,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 +1589,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 +1681,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 +1936,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 +1951,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 +2094,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 +2105,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 +2129,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,9 +2140,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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LanguageCode } from '@vendure/common/lib/generated-types';

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

export const orderLineFixedDiscount = new PromotionLineAction({
code: 'order_line_fixed_discount',
args: {
discount: {
type: 'int',
ui: {
component: 'currency-form-input',
},
},
},
execute(ctx, orderLine, args) {
return -args.discount;
},
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
91 changes: 90 additions & 1 deletion packages/core/src/config/promotion/promotion-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type ConditionState<
/**
* @description
* The function which is used by a PromotionItemAction to calculate the
* discount on the OrderLine.
* discount on the OrderLine for each item.
*
* @docsCategory promotions
* @docsPage promotion-action
Expand All @@ -77,6 +77,22 @@ export type ExecutePromotionItemActionFn<T extends ConfigArgs, U extends Array<P
promotion: Promotion,
) => number | Promise<number>;

/**
* @description
* The function which is used by a PromotionLineAction to calculate the
* discount on the OrderLine.
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export type ExecutePromotionLineActionFn<T extends ConfigArgs, U extends Array<PromotionCondition<any>>> = (
ctx: RequestContext,
orderLine: OrderLine,
args: ConfigArgValues<T>,
state: ConditionState<U>,
promotion: Promotion,
) => number | Promise<number>;

/**
* @description
* The function which is used by a PromotionOrderAction to calculate the
Expand Down Expand Up @@ -201,6 +217,24 @@ export interface PromotionItemActionConfig<T extends ConfigArgs, U extends Promo
execute: ExecutePromotionItemActionFn<T, U>;
}

/**
* @description
* Configuration for a {@link PromotionLineAction}
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export interface PromotionLineActionConfig<T extends ConfigArgs, U extends PromotionCondition[]>
extends PromotionActionConfig<T, U> {
/**
* @description
* The function which contains the promotion calculation logic.
* Should resolve to a number which represents the amount by which to discount
* the OrderLine, i.e. the number should be negative.
*/
execute: ExecutePromotionLineActionFn<T, U>;
}

/**
* @description
*
Expand Down Expand Up @@ -351,6 +385,61 @@ export class PromotionItemAction<
}
}

/**
* @description
* Represents a PromotionAction which applies to individual {@link OrderLine}s.
* The difference from PromotionItemAction is that it applies regardless of the Quantity of the OrderLine.
*
* @example
* ```ts
* // Applies a percentage discount to each OrderLine
* const linePercentageDiscount = new PromotionLineAction({
* code: 'line_percentage_discount',
* args: { discount: 'percentage' },
* execute(ctx, orderLine, args) {
* return -orderLine.linePrice * (args.discount / 100);
* },
* description: 'Discount every line by { discount }%',
* });
* ```
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export class PromotionLineAction<
T extends ConfigArgs = ConfigArgs,
U extends Array<PromotionCondition<any>> = [],
> extends PromotionAction<T, U> {
private readonly executeFn: ExecutePromotionLineActionFn<T, U>;
constructor(config: PromotionLineActionConfig<T, U>) {
super(config);
this.executeFn = config.execute;
}

/** @internal */
execute(
ctx: RequestContext,
orderLine: OrderLine,
args: ConfigArg[],
state: PromotionState,
promotion: Promotion,
) {
const actionState = this.conditions
? pick(
state,
this.conditions.map(c => c.code),
)
: {};
return this.executeFn(
ctx,
orderLine,
this.argsArrayToHash(args),
actionState as ConditionState<U>,
promotion,
);
}
}

/**
* @description
* Represents a PromotionAction which applies to the {@link Order} as a whole.
Expand Down
Loading

0 comments on commit 0ff8288

Please sign in to comment.