Browse Source

fix(core): Fix order totals calculation with order % discount

Michael Bromley 6 years ago
parent
commit
a4fea5954a

+ 3 - 1
packages/core/src/entity/order-item/order-item.entity.ts

@@ -124,7 +124,9 @@ export class OrderItem extends VendureEntity {
         if (!type) {
             this.pendingAdjustments = [];
         } else {
-            this.pendingAdjustments = this.pendingAdjustments.filter(a => a.type !== type);
+            this.pendingAdjustments = this.pendingAdjustments
+                ? this.pendingAdjustments.filter(a => a.type !== type)
+                : [];
         }
     }
 }

+ 1 - 2
packages/core/src/entity/order/order.entity.ts

@@ -113,7 +113,7 @@ export class Order extends VendureEntity implements HasCustomFields {
     }
 
     /**
-     * Clears Adjustments from all OrderItems of the given type. If no type
+     * Clears Adjustments of the given type. If no type
      * is specified, then all adjustments are removed.
      */
     clearAdjustments(type?: AdjustmentType) {
@@ -122,7 +122,6 @@ export class Order extends VendureEntity implements HasCustomFields {
         } else {
             this.pendingAdjustments = this.pendingAdjustments.filter(a => a.type !== type);
         }
-        this.lines.forEach(line => line.clearAdjustments(type));
     }
 
     getOrderItems(): OrderItem[] {

+ 137 - 34
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -75,6 +75,8 @@ describe('OrderCalculator', () => {
         );
 
         return new Order({
+            couponCodes: [],
+            pendingAdjustments: [],
             lines,
         });
     }
@@ -344,7 +346,7 @@ describe('OrderCalculator', () => {
             expect(order.total).toBe(50);
         });
 
-        it('interaction between promotions', async () => {
+        describe('interaction amongst promotion actions', () => {
             const orderQuantityCondition = new PromotionCondition({
                 args: { minimum: { type: 'int' } },
                 code: 'order_quantity_condition',
@@ -364,16 +366,7 @@ describe('OrderCalculator', () => {
                 },
             });
 
-            const orderPercentageDiscount = new PromotionOrderAction({
-                code: 'order_percentage_discount',
-                args: { discount: { type: 'int' } },
-                execute(_order, args) {
-                    return -_order.subTotal * (args.discount / 100);
-                },
-                description: [{ languageCode: LanguageCode.en, value: 'Discount order by { discount }%' }],
-            });
-
-            const promotion1 = new Promotion({
+            const buy3Get10pcOffOrder = new Promotion({
                 id: 1,
                 name: 'Buy 3 Get 50% off order',
                 conditions: [
@@ -385,15 +378,15 @@ describe('OrderCalculator', () => {
                 promotionConditions: [orderQuantityCondition],
                 actions: [
                     {
-                        code: orderPercentageDiscount.code,
+                        code: percentageOrderAction.code,
                         args: [{ name: 'discount', type: 'int', value: '50' }],
                     },
                 ],
-                promotionActions: [orderPercentageDiscount],
+                promotionActions: [percentageOrderAction],
             });
 
-            const promotion2 = new Promotion({
-                id: 1,
+            const spend100Get10pcOffOrder = new Promotion({
+                id: 2,
                 name: 'Spend $100 Get 10% off order',
                 conditions: [
                     {
@@ -404,36 +397,146 @@ describe('OrderCalculator', () => {
                 promotionConditions: [orderTotalCondition],
                 actions: [
                     {
-                        code: orderPercentageDiscount.code,
+                        code: percentageOrderAction.code,
                         args: [{ name: 'discount', type: 'int', value: '10' }],
                     },
                 ],
-                promotionActions: [orderPercentageDiscount],
+                promotionActions: [percentageOrderAction],
             });
 
-            const ctx = createRequestContext(true);
-            const order = createOrder({
-                lines: [{ unitPrice: 50, taxCategory: taxCategoryStandard, quantity: 2 }],
+            it('two order-level percentage discounts (tax included in prices)', async () => {
+                const ctx = createRequestContext(true);
+                const order = createOrder({
+                    lines: [{ unitPrice: 50, taxCategory: taxCategoryStandard, quantity: 2 }],
+                });
+
+                // initially the order is $100, so the second promotion applies
+                await orderCalculator.applyPriceAdjustments(ctx, order, [
+                    buy3Get10pcOffOrder,
+                    spend100Get10pcOffOrder,
+                ]);
+
+                expect(order.subTotal).toBe(100);
+                expect(order.adjustments.length).toBe(1);
+                expect(order.adjustments[0].description).toBe(spend100Get10pcOffOrder.name);
+                expect(order.total).toBe(90);
+
+                // increase the quantity to 3, which will trigger the first promotion and thus
+                // bring the order total below the threshold for the second promotion.
+                order.lines[0].items.push(new OrderItem({ unitPrice: 50 }));
+
+                await orderCalculator.applyPriceAdjustments(ctx, order, [
+                    buy3Get10pcOffOrder,
+                    spend100Get10pcOffOrder,
+                ]);
+
+                expect(order.subTotal).toBe(150);
+                expect(order.adjustments.length).toBe(1);
+                expect(order.total).toBe(75);
             });
 
-            // initially the order is $100, so the second promotion applies
-            await orderCalculator.applyPriceAdjustments(ctx, order, [promotion1, promotion2]);
+            it('two order-level percentage discounts (tax excluded from prices)', async () => {
+                const ctx = createRequestContext(false);
+                const order = createOrder({
+                    lines: [{ unitPrice: 42, taxCategory: taxCategoryStandard, quantity: 2 }],
+                });
+
+                // initially the order is $100, so the second promotion applies
+                await orderCalculator.applyPriceAdjustments(ctx, order, [
+                    buy3Get10pcOffOrder,
+                    spend100Get10pcOffOrder,
+                ]);
+
+                expect(order.subTotal).toBe(100);
+                expect(order.adjustments.length).toBe(1);
+                expect(order.adjustments[0].description).toBe(spend100Get10pcOffOrder.name);
+                expect(order.total).toBe(90);
+
+                // increase the quantity to 3, which will trigger the first promotion and thus
+                // bring the order total below the threshold for the second promotion.
+                order.lines[0].items.push(new OrderItem({ unitPrice: 42 }));
+
+                await orderCalculator.applyPriceAdjustments(ctx, order, [
+                    buy3Get10pcOffOrder,
+                    spend100Get10pcOffOrder,
+                ]);
+
+                expect(order.subTotal).toBe(150);
+                expect(order.adjustments.length).toBe(1);
+                expect(order.total).toBe(75);
+            });
 
-            expect(order.subTotal).toBe(100);
-            expect(order.adjustments.length).toBe(1);
-            expect(order.adjustments[0].description).toBe(promotion2.name);
-            expect(order.total).toBe(90);
+            const orderPromo = new Promotion({
+                id: 1,
+                name: '10% off order',
+                couponCode: 'ORDER10',
+                conditions: [],
+                promotionConditions: [],
+                actions: [
+                    {
+                        code: percentageOrderAction.code,
+                        args: [{ name: 'discount', type: 'int', value: '10' }],
+                    },
+                ],
+                promotionActions: [percentageOrderAction],
+            });
 
-            // increase the quantity to 3, which will trigger the first promotion and thus
-            // bring the order total below the threshold for the second promotion.
-            order.lines[0].items.push(new OrderItem({ unitPrice: 50 }));
+            const itemPromo = new Promotion({
+                id: 2,
+                name: '10% off item',
+                couponCode: 'ITEM10',
+                conditions: [],
+                promotionConditions: [],
+                actions: [
+                    {
+                        code: percentageItemAction.code,
+                        args: [{ name: 'discount', type: 'int', value: '10' }],
+                    },
+                ],
+                promotionActions: [percentageItemAction],
+            });
 
-            await orderCalculator.applyPriceAdjustments(ctx, order, [promotion1, promotion2]);
+            it('item-level & order-level percentage discounts', async () => {
+                const ctx = createRequestContext(true);
+                const order = createOrder({
+                    lines: [{ unitPrice: 155880, taxCategory: taxCategoryStandard, quantity: 1 }],
+                });
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
 
-            expect(order.subTotal).toBe(150);
-            expect(order.adjustments.length).toBe(1);
-            // expect(order.adjustments[0].description).toBe(promotion1.name);
-            expect(order.total).toBe(75);
+                expect(order.total).toBe(155880);
+
+                // Apply the item-level discount
+                order.couponCodes.push('ITEM10');
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
+                expect(order.total).toBe(140292);
+
+                // Apply the order-level discount
+                order.couponCodes.push('ORDER10');
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
+                expect(order.total).toBe(126263);
+            });
+
+            it('item-level & order-level percentage (tax not included)', async () => {
+                const ctx = createRequestContext(false);
+                const order = createOrder({
+                    lines: [{ unitPrice: 129900, taxCategory: taxCategoryStandard, quantity: 1 }],
+                });
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
+
+                expect(order.total).toBe(155880);
+
+                // Apply the item-level discount
+                order.couponCodes.push('ITEM10');
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
+                expect(order.total).toBe(140292);
+                expect(order.adjustments.length).toBe(0);
+
+                // Apply the order-level discount
+                order.couponCodes.push('ORDER10');
+                await orderCalculator.applyPriceAdjustments(ctx, order, [orderPromo, itemPromo]);
+                expect(order.total).toBe(126263);
+                expect(order.adjustments.length).toBe(1);
+            });
         });
     });
 });

+ 8 - 0
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -88,6 +88,11 @@ export class OrderCalculator {
      */
     private async applyPromotions(order: Order, promotions: Promotion[]) {
         const utils = this.createPromotionUtils();
+        await this.applyOrderItemPromotions(order, promotions, utils);
+        await this.applyOrderPromotions(order, promotions, utils);
+    }
+
+    private async applyOrderItemPromotions(order: Order, promotions: Promotion[], utils: PromotionUtils) {
         for (const line of order.lines) {
             // Must be re-calculated for each line, since the previous lines may have triggered promotions
             // which affected the order price.
@@ -113,7 +118,10 @@ export class OrderCalculator {
                 this.calculateOrderTotals(order);
             }
         }
+    }
 
+    private async applyOrderPromotions(order: Order, promotions: Promotion[], utils: PromotionUtils) {
+        order.clearAdjustments(AdjustmentType.PROMOTION);
         const applicableOrderPromotions = await filterAsync(promotions, p => p.test(order, utils));
         if (applicableOrderPromotions.length) {
             for (const promotion of applicableOrderPromotions) {