From 3ea2129f7a83dba29d007d7c2e7f3e079b1f5053 Mon Sep 17 00:00:00 2001 From: Senthilkumar Muppidathi Date: Sun, 9 Nov 2025 15:37:58 +0530 Subject: [PATCH] #40276-Set discount amount in child and in parent for bundle products/configurable products --- .../SalesRule/Model/Quote/Discount.php | 32 +++---- .../Magento/SalesRule/Model/RulesApplier.php | 85 ++++++++++++------- .../Test/Unit/Model/Quote/DiscountTest.php | 8 +- .../SalesRule/Model/Quote/DiscountTest.php | 10 +-- 4 files changed, 75 insertions(+), 60 deletions(-) diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index 91e7ed75d78ae..dd841d6297413 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -181,6 +181,7 @@ public function collect( $child->setDiscountPercent(0); } } + $item->getAddress()->setDiscountAmount(0); $item->getAddress()->setBaseDiscountAmount(0); } $this->calculator->initFromQuote($quote); @@ -189,6 +190,8 @@ public function collect( $itemsToApplyRules = $items; $rules = $this->calculator->getRules($address); $address->setBaseDiscountAmount(0); + $address->setDiscountAmount(0); + $ruleTotalDiscount = $ruleBaseTotalDiscount = 0; /** @var Rule $rule */ foreach ($rules as $rule) { /** @var Item $item */ @@ -219,27 +222,12 @@ public function collect( if ($rule->getStopRulesProcessing() && in_array($rule->getId(), $appliedRuleIds)) { unset($itemsToApplyRules[$key]); } + + $ruleBaseTotalDiscount += $item->getBaseDiscountAmount(); + $ruleTotalDiscount += $item->getDiscountAmount(); } - $baseDiscountAmount = 0; - $discountAmount = 0; - // $itemsAggregate are items specific to the current shipping address - foreach ($itemsAggregate as $item) { - if ($item->getParentItem()) { - continue; - } - if ($item->getChildren() && $item->isChildrenCalculated()) { - foreach ($item->getChildren() as $child) { - $baseDiscountAmount += $child->getBaseDiscountAmount(); - $discountAmount += $child->getDiscountAmount(); - } - } - $baseDiscountAmount += $item->getBaseDiscountAmount(); - $discountAmount += $item->getDiscountAmount(); - } - $address->setBaseDiscountAmount(-$baseDiscountAmount); - $address->setDiscountAmount(-$discountAmount); - $address->setBaseSubtotalWithDiscount($address->getBaseSubtotal() - $baseDiscountAmount); - $address->setSubtotalWithDiscount($address->getSubtotal() - $discountAmount); + $address->setBaseDiscountAmount($ruleBaseTotalDiscount); + $address->setDiscountAmount($ruleTotalDiscount); } $this->calculator->initTotals($items, $address); foreach ($items as $item) { @@ -254,12 +242,14 @@ public function collect( $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); $this->aggregateItemDiscount($child, $total); } + } else { + $this->aggregateItemDiscount($item, $total); } - $this->aggregateItemDiscount($item, $total); if ($item->getExtensionAttributes()) { $this->aggregateDiscountPerRule($item, $address); } } + $this->calculator->prepareDescription($address); $total->setDiscountDescription($address->getDiscountDescription()); $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount()); diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index 9e1c0af24fac4..f937f45b64646 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -79,6 +79,11 @@ class RulesApplier */ private $priceCurrency; + /** + * @var DataFactory + */ + private $aggregateDiscount = null; + /** * @param CalculatorFactory $calculatorFactory * @param ManagerInterface $eventManager @@ -247,29 +252,31 @@ protected function applyRule($item, $rule, $address, array $couponCodes = []) { if ($item->getChildren() && $item->isChildrenCalculated()) { $cloneItem = clone $item; + $canApplytoParent = $rule->getActions()->validate($cloneItem); + $canApplyChild = false; /** * Validates item without children to check whether the rule can be applied to the item itself * If the rule can be applied to the item, the discount is applied to the item itself and * distributed among its children */ - if ($rule->getActions()->validate($cloneItem)) { - // Aggregate discount data from children - $discountData = $this->getDiscountDataFromChildren($item); - $this->setDiscountData($discountData, $item); - // Calculate discount data based on parent item - $discountData = $this->getDiscountData($item, $rule, $address, $couponCodes); - $this->distributeDiscount($discountData, $item); - // reset discount data in parent item after distributing discount to children - $discountData = $this->discountFactory->create(); - $this->setDiscountData($discountData, $item); - } else { - foreach ($item->getChildren() as $childItem) { - if ($rule->getActions()->validate($childItem)) { - $discountData = $this->getDiscountData($childItem, $rule, $address, $couponCodes); - $this->setDiscountData($discountData, $childItem); - } + foreach ($item->getChildren() as $childItem) { + if ($canApplytoParent || $rule->getActions()->validate($childItem)) { + // setting it to true if it was false and child in action conditions validated true + $canApplyChild = true; + // Cloning the items to find out rule specific discount + $childClone = clone $childItem; + $discountData = $this->getDiscountData($childItem, $rule, $address, $couponCodes); + $this->setDiscountData($discountData, $childItem); + $this->setAggregateDiscountData($childItem, $childClone); } } + /** + * apply discount to parent item if parent or children is valid + */ + if ($canApplytoParent || $canApplyChild) { + $discountData = $this->getDiscountDataFromChildren(); + $this->setDiscountData($discountData, $item); + } } else { $discountData = $this->getDiscountData($item, $rule, $address, $couponCodes); $this->setDiscountData($discountData, $item); @@ -282,25 +289,43 @@ protected function applyRule($item, $rule, $address, array $couponCodes = []) } /** - * Get discount data from children + * Set Sum of child items discount data * * @param AbstractItem $item - * @return Data + * @param AbstractItem $childClone + * @return void */ - private function getDiscountDataFromChildren(AbstractItem $item): Data + private function setAggregateDiscountData(AbstractItem $item, AbstractItem $childClone) { - $discountData = $this->discountFactory->create(); - - foreach ($item->getChildren() as $child) { - $discountData->setAmount($discountData->getAmount() + $child->getDiscountAmount()); - $discountData->setBaseAmount($discountData->getBaseAmount() + $child->getBaseDiscountAmount()); - $discountData->setOriginalAmount($discountData->getOriginalAmount() + $child->getOriginalDiscountAmount()); - $discountData->setBaseOriginalAmount( - $discountData->getBaseOriginalAmount() + $child->getBaseOriginalDiscountAmount() - ); - } + $this->aggregateDiscount ??= $this->discountFactory->create(); + $this->aggregateDiscount->setAmount( + ($this->aggregateDiscount->getAmount() - $childClone->getDiscountAmount()) + + $item->getDiscountAmount() + ); + $this->aggregateDiscount->setBaseAmount( + ($this->aggregateDiscount->getBaseAmount()-$childClone->getBaseDiscountAmount()) + + $item->getBaseDiscountAmount() + ); + $this->aggregateDiscount->setOriginalAmount( + ($this->aggregateDiscount->getOriginalAmount() + - $childClone->getOriginalDiscountAmount()) + + $item->getOriginalDiscountAmount() + ); + $this->aggregateDiscount->setBaseOriginalAmount( + ($this->aggregateDiscount->getBaseOriginalAmount() + - $childClone->getBaseOriginalDiscountAmount()) + + $item->getBaseOriginalDiscountAmount() + ); + } - return $discountData; + /** + * Get Aggregated discount data + * + * @return Data + */ + private function getDiscountDataFromChildren(): Data + { + return $this->aggregateDiscount; } /** diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index 2bd85936e2b3f..572735fe8f6ba 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -201,7 +201,7 @@ public function testCollectItemNoDiscount(): void $itemNoDiscount->expects($this->any())->method('getExtensionAttributes')->willReturn($itemExtension); $itemNoDiscount->expects($this->any())->method('getId')->willReturn(1); $itemNoDiscount->expects($this->once())->method('getNoDiscount')->willReturn(true); - $itemNoDiscount->expects($this->once())->method('getAddress')->willReturn($this->addressMock); + $itemNoDiscount->expects($this->any())->method('getAddress')->willReturn($this->addressMock); $this->validatorMock->expects($this->once())->method('sortItemsByPriority') ->with([$itemNoDiscount], $this->addressMock) ->willReturnArgument(0); @@ -256,7 +256,7 @@ public function testCollectItemHasParent(): void $itemWithParentId->expects($this->any())->method('getId')->willReturn(1); $itemWithParentId->expects($this->any())->method('getParentItem')->willReturn(true); $itemWithParentId->expects($this->any())->method('getExtensionAttributes')->willReturn(false); - $itemWithParentId->expects($this->once())->method('getAddress')->willReturn($this->addressMock); + $itemWithParentId->expects($this->any())->method('getAddress')->willReturn($this->addressMock); $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); $this->validatorMock->expects($this->any())->method('sortItemsByPriority') @@ -339,7 +339,7 @@ public function testCollectItemHasNoChildren(): void $itemWithChildren->expects($this->any())->method('getParentItem')->willReturn(false); $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(false); $itemWithChildren->expects($this->any())->method('getId')->willReturn(2); - $itemWithChildren->expects($this->once())->method('getAddress')->willReturn($this->addressMock); + $itemWithChildren->expects($this->any())->method('getAddress')->willReturn($this->addressMock); $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); $this->validatorMock->expects($this->once())->method('sortItemsByPriority') @@ -459,7 +459,7 @@ public function testCollectAddressBaseDiscountAmountIncludingItemChildren(): voi $item->expects($this->any())->method('getId')->willReturn(1); $item->expects($this->any())->method('getParentItem')->willReturn(false); $item->expects($this->any())->method('getExtensionAttributes')->willReturn(false); - $item->expects($this->once())->method('getAddress')->willReturn($this->addressMock); + $item->expects($this->any())->method('getAddress')->willReturn($this->addressMock); $child = $this->getMockBuilder(Item::class) ->addMethods(['getBaseDiscountAmount']) ->disableOriginalConstructor() diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php index 04a2aa81feee5..d7c6405a29936 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php @@ -244,7 +244,7 @@ public function testBundleProductDynamicPriceWithBundleDiscountAndChildDiscount( { $discounts = [ // bundle with dynamic price does not have discount on its own, instead it's distributed to children - 'bundle' => 0, + 'bundle' => 5.90, // rule1 = (20/100 * 21.98) * (5.99 / 21.98) = 1.198 // rule2 = 1.50 // D = rule1 + rule1 = 1.198 + 1.50 = 2.698 ~ 2.70 @@ -324,7 +324,7 @@ public function testBundleProductDynamicPriceWithChildDiscountAndBundleDiscount( { $discounts = [ // bundle with dynamic price does not have discount on its own, instead it's distributed to children - 'bundle' => 0, + 'bundle' => 5.60, // rule2 = 1.50 // rule1 = (20/100 * (21.98 - 1.50) * ((5.99 - 1.50) / (21.98 - 1.50)) = 0.898 // D = rule2 + rule1 = 1.50 + 0.898 = 2.398 ~ 2.40 @@ -360,7 +360,7 @@ public static function bundleProductWithDynamicPriceAndCartPriceRuleDataProvider 'bundle_cc', [ // bundle with dynamic price does not have discount on its own, instead it's distributed to children - 'bundle' => 0, + 'bundle' => 10.99, 'simple1' => 3, 'simple2' => 7.99, ], @@ -369,7 +369,7 @@ public static function bundleProductWithDynamicPriceAndCartPriceRuleDataProvider [ 'simple1_cc', [ - 'bundle' => 0, + 'bundle' => 3, 'simple1' => 3, 'simple2' => 0, ], @@ -378,7 +378,7 @@ public static function bundleProductWithDynamicPriceAndCartPriceRuleDataProvider [ 'simple2_cc', [ - 'bundle' => 0, + 'bundle' => 8, 'simple1' => 0, 'simple2' => 8, ],