From 011f13db26acdf9e50f7941ece4d2760cdc9388a Mon Sep 17 00:00:00 2001 From: Marvin Besselsen Date: Mon, 3 Aug 2026 15:30:30 +0200 Subject: [PATCH] Apply item-level discount before tax calculation Channable sends per-product discount fields but these were ignored. Discounts were only applied at order level after tax was already calculated, causing tax to be computed on the undiscounted price. Now subtracts item discount from OriginalCustomPrice so Magento calculates tax on the correct (discounted) amount. Falls back to order-level discount when no item discounts are present. Adds E2E tests for item-level discount tax calculation. Ref: https://github.com/channable/magento-plugin/issues/28 --- Service/Order/Import.php | 39 +++++++++++++------ Service/Order/Items/Add.php | 6 ++- .../support/services/ChannableApi.ts | 5 +++ .../tests/order/order-import.spec.ts | 37 ++++++++++++++++++ 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/Service/Order/Import.php b/Service/Order/Import.php index 4c954ce5..9110907b 100755 --- a/Service/Order/Import.php +++ b/Service/Order/Import.php @@ -156,18 +156,20 @@ public function execute(ChannableOrderData $orderData): OrderInterface $order->setTransactionFee($quote->getTransactionFee()); if (isset($orderData['price']['discount']) && !empty((float)$orderData['price']['discount'])) { - $orderCurrency = $orderData['price']['currency'] ?? ''; - $discountAmount = abs((float)$orderData['price']['discount']); - $baseDiscountAmount = $this->currencyConverter->convertToBase( - $discountAmount, - $orderCurrency, - $storeId - ); - $order->setDiscountDescription($orderData['channel_name']); - $order->setBaseDiscountAmount($baseDiscountAmount * -1); - $order->setDiscountAmount($discountAmount * -1); - $order->setGrandTotal($order->getGrandTotal() - $discountAmount); - $order->setBaseGrandTotal($order->getBaseGrandTotal() - $baseDiscountAmount); + if (!$this->hasItemLevelDiscounts($orderData)) { + $orderCurrency = $orderData['price']['currency'] ?? ''; + $discountAmount = abs((float)$orderData['price']['discount']); + $baseDiscountAmount = $this->currencyConverter->convertToBase( + $discountAmount, + $orderCurrency, + $storeId + ); + $order->setDiscountDescription($orderData['channel_name']); + $order->setBaseDiscountAmount($baseDiscountAmount * -1); + $order->setDiscountAmount($discountAmount * -1); + $order->setGrandTotal($order->getGrandTotal() - $discountAmount); + $order->setBaseGrandTotal($order->getBaseGrandTotal() - $baseDiscountAmount); + } } $store->setCurrentCurrencyCode($store->getBaseCurrencyCode()); @@ -198,6 +200,19 @@ public function execute(ChannableOrderData $orderData): OrderInterface } } + /** + * Check if any product in the order data has an item-level discount. + */ + private function hasItemLevelDiscounts(array $orderData): bool + { + foreach ($orderData['products'] ?? [] as $item) { + if (isset($item['discount']) && (float)$item['discount'] > 0) { + return true; + } + } + return false; + } + /** * Add shipping info to the checkout-session * diff --git a/Service/Order/Items/Add.php b/Service/Order/Items/Add.php index 71d7fbfd..618a5b1b 100644 --- a/Service/Order/Items/Add.php +++ b/Service/Order/Items/Add.php @@ -131,7 +131,11 @@ public function execute(Quote $quote, array $data, StoreInterface $store, bool $ throw new CouldNotImportOrder(__($addedItem)); } - $addedItem->setOriginalCustomPrice($price); + $itemDiscount = (isset($item['discount']) && (float)$item['discount'] > 0) + ? (float)$item['discount'] + : 0.0; + + $addedItem->setOriginalCustomPrice($price - $itemDiscount); $addedItem->setOriginalPrice($channableBasePrice); $this->itemResourceModel->save($addedItem); $qty += (int)$item['quantity']; diff --git a/Test/End-2-end/support/services/ChannableApi.ts b/Test/End-2-end/support/services/ChannableApi.ts index 070f90a9..1b76b9af 100644 --- a/Test/End-2-end/support/services/ChannableApi.ts +++ b/Test/End-2-end/support/services/ChannableApi.ts @@ -107,6 +107,7 @@ export default class ChannableApi extends BaseApi { businessOrder?: boolean; shipping?: number; discount?: number; + itemDiscount?: number; companyName?: string; channelName?: string; shipmentMethod?: string; @@ -161,6 +162,10 @@ export default class ChannableApi extends BaseApi { data.products[0].price_tax = priceTax; } + if (overrides.itemDiscount !== undefined) { + data.products[0].discount = overrides.itemDiscount; + } + if (overrides.orderStatus) { data.order_status = overrides.orderStatus; } diff --git a/Test/End-2-end/tests/order/order-import.spec.ts b/Test/End-2-end/tests/order/order-import.spec.ts index 54489c9f..9e3f7896 100644 --- a/Test/End-2-end/tests/order/order-import.spec.ts +++ b/Test/End-2-end/tests/order/order-import.spec.ts @@ -188,6 +188,43 @@ const testCases = [ expect(displayedId).toBeTruthy(); }, }, + { + title: 'Item-level discount: original price vs discounted price', + config: {}, + orderOverrides: { price: 24.99, itemDiscount: 2.50, discount: 2.50 }, + assert: async (page, incrementId) => { + // Original price should be the full Channable price (24.99) + const originalPriceStr = await orderViewPage.getOriginalPrice(page); + const originalPrice = parsePrice(originalPriceStr); + expect(originalPrice).toBeCloseTo(24.99, 1); + + // Item price should be lower than original (discount applied) + const itemPriceStr = await orderViewPage.getItemPrice(page); + const itemPrice = parsePrice(itemPriceStr); + expect(itemPrice).toBeLessThan(originalPrice); + + // Grand total must equal discounted price (22.49), not full price (24.99) + const grandTotalStr = await orderViewPage.getGrandTotal(page); + const grandTotal = parsePrice(grandTotalStr); + expect(grandTotal).toBeCloseTo(22.49, 1); + }, + }, + { + title: 'Item-level discount: multi-qty grand total', + config: {}, + orderOverrides: { price: 24.99, quantity: 3, itemDiscount: 2.50, discount: 7.50 }, + assert: async (page, incrementId) => { + // Grand total should be 3 * (24.99 - 2.50) = 67.47 + const grandTotalStr = await orderViewPage.getGrandTotal(page); + const grandTotal = parsePrice(grandTotalStr); + expect(grandTotal).toBeCloseTo(67.47, 1); + + // Row total must be less than undiscounted (3 * 24.99 = 74.97) + const rowTotalStr = await orderViewPage.getRowTotal(page); + const rowTotal = parsePrice(rowTotalStr); + expect(rowTotal).toBeLessThan(74.97); + }, + }, { title: 'Multi-currency order (PLN)', config: {