diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3b2f03f89..a73d6d3d8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.0.9 (2023-09-29)
+
+
+#### Fixes
+
+* **core** Fix discount calculation error edge-case ([7549aad](https://github.com/vendure-ecommerce/vendure/commit/7549aad)), closes [#2385](https://github.com/vendure-ecommerce/vendure/issues/2385)
+
## 2.0.8 (2023-09-27)
diff --git a/packages/core/e2e/order-promotion.e2e-spec.ts b/packages/core/e2e/order-promotion.e2e-spec.ts
index 6241535566..669e5301fb 100644
--- a/packages/core/e2e/order-promotion.e2e-spec.ts
+++ b/packages/core/e2e/order-promotion.e2e-spec.ts
@@ -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]
@@ -1943,78 +1944,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(
+ 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(
+ GET_ACTIVE_ORDER,
+ );
+ expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
+ expect(check2!.totalWithTax).toBe(0);
});
- const { activeOrder: check1 } = await shopClient.query(
- 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(
- GET_ACTIVE_ORDER,
- );
- expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
- expect(check2!.totalWithTax).toBe(0);
+ const { activeOrder: check1 } = await shopClient.query(
+ 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(
+ GET_ACTIVE_ORDER,
+ );
+ expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
+ expect(check2!.totalWithTax).toBe(0);
+ });
});
async function getProducts() {
diff --git a/packages/core/src/config/promotion/actions/product-percentage-discount-action.ts b/packages/core/src/config/promotion/actions/product-percentage-discount-action.ts
index 10a6b92e64..2f1bb55ee0 100644
--- a/packages/core/src/config/promotion/actions/product-percentage-discount-action.ts
+++ b/packages/core/src/config/promotion/actions/product-percentage-discount-action.ts
@@ -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;
diff --git a/packages/core/src/entity/order-line/order-line.entity.ts b/packages/core/src/entity/order-line/order-line.entity.ts
index 198d75df8e..990d01e03b 100644
--- a/packages/core/src/entity/order-line/order-line.entity.ts
+++ b/packages/core/src/entity/order-line/order-line.entity.ts
@@ -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),