Skip to content

Commit

Permalink
fix(core): Fix discount calculation error edge-case
Browse files Browse the repository at this point in the history
Relates to #2385. This error only occurs if the discount amount is _greater_ than the tax-exclusive
price of the variant.
  • Loading branch information
michaelbromley committed Sep 28, 2023
1 parent 6e826bf commit 7549aad
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 62 deletions.
203 changes: 143 additions & 60 deletions packages/core/e2e/order-promotion.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ describe('Promotions applied to Orders', () => {
describe('discountOnItemWithFacets', () => {
const couponCode = '50%_off_sale_items';
let promotion: Codegen.PromotionFragment;

function getItemSale1Line<
T extends Array<
| CodegenShop.UpdatedOrderFragment['lines'][number]
Expand Down Expand Up @@ -1756,78 +1757,160 @@ describe('Promotions applied to Orders', () => {
});

// https://github.com/vendure-ecommerce/vendure/issues/2385
it('prevents negative line price', async () => {
await shopClient.asAnonymousUser();
const item1000 = getVariantBySlug('item-1000')!;
describe('prevents negative line price', () => {
const TAX_INCLUDED_CHANNEL_TOKEN_2 = 'tax_included_channel_2';
const couponCode1 = '100%_off';
const couponCode2 = '100%_off';
await createPromotion({
enabled: true,
name: '100% discount ',
couponCode: couponCode1,
conditions: [],
actions: [
{
code: productsPercentageDiscount.code,
arguments: [
{ name: 'discount', value: '100' },
{
name: 'productVariantIds',
value: `["${item1000.id}"]`,
},
],
let taxIncludedChannel: Codegen.ChannelFragment;

beforeAll(async () => {
// Create a channel where the prices include tax, so we can ensure
// that PromotionActions are working as expected when taxes are included
const { createChannel } = await adminClient.query<
Codegen.CreateChannelMutation,
Codegen.CreateChannelMutationVariables
>(CREATE_CHANNEL, {
input: {
code: 'tax-included-channel-2',
currencyCode: CurrencyCode.GBP,
pricesIncludeTax: true,
defaultTaxZoneId: 'T_1',
defaultShippingZoneId: 'T_1',
defaultLanguageCode: LanguageCode.en,
token: TAX_INCLUDED_CHANNEL_TOKEN_2,
},
],
});
await createPromotion({
enabled: true,
name: '20% discount ',
couponCode: couponCode2,
conditions: [],
actions: [
{
code: productsPercentageDiscount.code,
arguments: [
{ name: 'discount', value: '20' },
{
name: 'productVariantIds',
value: `["${item1000.id}"]`,
},
],
});
taxIncludedChannel = createChannel as Codegen.ChannelFragment;
await adminClient.query<
Codegen.AssignProductsToChannelMutation,
Codegen.AssignProductsToChannelMutationVariables
>(ASSIGN_PRODUCT_TO_CHANNEL, {
input: {
channelId: taxIncludedChannel.id,
priceFactor: 1,
productIds: products.map(p => p.id),
},
],
});
const item1000 = getVariantBySlug('item-1000')!;
const promo100 = await createPromotion({
enabled: true,
name: '100% discount ',
couponCode: couponCode1,
conditions: [],
actions: [
{
code: productsPercentageDiscount.code,
arguments: [
{ name: 'discount', value: '100' },
{
name: 'productVariantIds',
value: `["${item1000.id}"]`,
},
],
},
],
});
const promo20 = await createPromotion({
enabled: true,
name: '20% discount ',
couponCode: couponCode2,
conditions: [],
actions: [
{
code: productsPercentageDiscount.code,
arguments: [
{ name: 'discount', value: '20' },
{
name: 'productVariantIds',
value: `["${item1000.id}"]`,
},
],
},
],
});
await adminClient.query<
Codegen.AssignPromotionToChannelMutation,
Codegen.AssignPromotionToChannelMutationVariables
>(ASSIGN_PROMOTIONS_TO_CHANNEL, {
input: {
promotionIds: [promo100.id, promo20.id],
channelId: taxIncludedChannel.id,
},
});
});

await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode1 });
it('prices exclude tax', async () => {
await shopClient.asAnonymousUser();
const item1000 = getVariantBySlug('item-1000')!;

await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: item1000.id,
quantity: 1,
await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode1 });

await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: item1000.id,
quantity: 1,
});

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

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);

await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

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

const { activeOrder: check1 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
GET_ACTIVE_ORDER,
);
it('prices include tax', async () => {
shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN_2);
await shopClient.asAnonymousUser();
const item1000 = getVariantBySlug('item-1000')!;

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);
await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode1 });

await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });
await shopClient.query<
CodegenShop.AddItemToOrderMutation,
CodegenShop.AddItemToOrderMutationVariables
>(ADD_ITEM_TO_ORDER, {
productVariantId: item1000.id,
quantity: 1,
});

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

expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
expect(check1!.totalWithTax).toBe(0);

await shopClient.query<
CodegenShop.ApplyCouponCodeMutation,
CodegenShop.ApplyCouponCodeMutationVariables
>(APPLY_COUPON_CODE, { couponCode: couponCode2 });

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

async function getProducts() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export const productsPercentageDiscount = new PromotionItemAction({
label: [{ languageCode: LanguageCode.en, value: 'Product variants' }],
},
},

execute(ctx, orderLine, args) {
if (lineContainsIds(args.productVariantIds, orderLine)) {
const unitPrice = ctx.channel.pricesIncludeTax ? orderLine.unitPriceWithTax : orderLine.unitPrice;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/entity/order-line/order-line.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
addAdjustment(adjustment: Adjustment) {
// We should not allow adding adjustments which would
// result in a negative unit price
const maxDiscount = this.proratedLinePrice * -1;
const maxDiscount =
(this.listPriceIncludesTax ? this.proratedLinePriceWithTax : this.proratedLinePrice) * -1;
const limitedAdjustment: Adjustment = {
...adjustment,
amount: Math.max(maxDiscount, adjustment.amount),
Expand Down

0 comments on commit 7549aad

Please sign in to comment.