Bladeren bron

fix(core): Fix discount calculation error edge-case

Relates to #2385. This error only occurs if the discount amount is _greater_ than the tax-exclusive
price of the variant.
Michael Bromley 2 jaren geleden
bovenliggende
commit
7549aad738

+ 143 - 60
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]
@@ -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() {

+ 0 - 1
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;

+ 2 - 1
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),