Skip to content

Commit 75cb08c

Browse files
committed
ACP2E-3491: Cart rule sku condition is failing for invoice.
1 parent 7ead0dc commit 75cb08c

File tree

3 files changed

+373
-61
lines changed

3 files changed

+373
-61
lines changed

app/code/Magento/SalesRule/Model/RulesApplier.php

Lines changed: 88 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2014 Adobe
4+
* All Rights Reserved.
55
*/
6+
67
namespace Magento\SalesRule\Model;
78

89
use Magento\Framework\Event\ManagerInterface;
10+
use Magento\Framework\Pricing\PriceCurrencyInterface;
911
use Magento\Quote\Model\Quote\Address;
1012
use Magento\Quote\Model\Quote\Item\AbstractItem;
1113
use Magento\SalesRule\Model\Data\RuleDiscount;
@@ -72,6 +74,11 @@ class RulesApplier
7274
*/
7375
private $discountAggregator;
7476

77+
/**
78+
* @var PriceCurrencyInterface
79+
*/
80+
private $priceCurrency;
81+
7582
/**
7683
* @param CalculatorFactory $calculatorFactory
7784
* @param ManagerInterface $eventManager
@@ -81,6 +88,7 @@ class RulesApplier
8188
* @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory
8289
* @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory
8390
* @param SelectRuleCoupon|null $selectRuleCoupon
91+
* @param PriceCurrencyInterface|null $priceCurrency
8492
*/
8593
public function __construct(
8694
CalculatorFactory $calculatorFactory,
@@ -90,7 +98,8 @@ public function __construct(
9098
DataFactory $discountDataFactory = null,
9199
RuleDiscountInterfaceFactory $discountInterfaceFactory = null,
92100
DiscountDataInterfaceFactory $discountDataInterfaceFactory = null,
93-
SelectRuleCoupon $selectRuleCoupon = null
101+
SelectRuleCoupon $selectRuleCoupon = null,
102+
?PriceCurrencyInterface $priceCurrency = null
94103
) {
95104
$this->calculatorFactory = $calculatorFactory;
96105
$this->validatorUtility = $utility;
@@ -104,6 +113,7 @@ public function __construct(
104113
?: ObjectManager::getInstance()->get(DiscountDataInterfaceFactory::class);
105114
$this->selectRuleCoupon = $selectRuleCoupon
106115
?: ObjectManager::getInstance()->get(SelectRuleCoupon::class);
116+
$this->priceCurrency = $priceCurrency ?: ObjectManager::getInstance()->get(PriceCurrencyInterface::class);
107117
}
108118

109119
/**
@@ -237,21 +247,28 @@ protected function applyRule($item, $rule, $address, array $couponCodes = [])
237247
{
238248
if ($item->getChildren() && $item->isChildrenCalculated()) {
239249
$cloneItem = clone $item;
240-
241-
$applyToChildren = false;
242-
foreach ($item->getChildren() as $childItem) {
243-
if ($rule->getActions()->validate($childItem)) {
244-
$discountData = $this->getDiscountData($childItem, $rule, $address, $couponCodes);
245-
$this->setDiscountData($discountData, $childItem);
246-
$applyToChildren = true;
247-
}
248-
}
249250
/**
250-
* validate without children
251+
* Validates item without children to check whether the rule can be applied to the item itself
252+
* If the rule can be applied to the item, the discount is applied to the item itself and
253+
* distributed among its children
251254
*/
252-
if (!$applyToChildren && $rule->getActions()->validate($cloneItem)) {
255+
if ($rule->getActions()->validate($cloneItem)) {
256+
// Aggregate discount data from children
257+
$discountData = $this->getDiscountDataFromChildren($item);
258+
$this->setDiscountData($discountData, $item);
259+
// Calculate discount data based on parent item
253260
$discountData = $this->getDiscountData($item, $rule, $address, $couponCodes);
261+
$this->distributeDiscount($discountData, $item);
262+
// reset discount data in parent item after distributing discount to children
263+
$discountData = $this->discountFactory->create();
254264
$this->setDiscountData($discountData, $item);
265+
} else {
266+
foreach ($item->getChildren() as $childItem) {
267+
if ($rule->getActions()->validate($childItem)) {
268+
$discountData = $this->getDiscountData($childItem, $rule, $address, $couponCodes);
269+
$this->setDiscountData($discountData, $childItem);
270+
}
271+
}
255272
}
256273
} else {
257274
$discountData = $this->getDiscountData($item, $rule, $address, $couponCodes);
@@ -264,6 +281,63 @@ protected function applyRule($item, $rule, $address, array $couponCodes = [])
264281
return $this;
265282
}
266283

284+
/**
285+
* Get discount data from children
286+
*
287+
* @param AbstractItem $item
288+
* @return Data
289+
*/
290+
private function getDiscountDataFromChildren(AbstractItem $item): Data
291+
{
292+
$discountData = $this->discountFactory->create();
293+
294+
foreach ($item->getChildren() as $child) {
295+
$discountData->setAmount($discountData->getAmount() + $child->getDiscountAmount());
296+
$discountData->setBaseAmount($discountData->getBaseAmount() + $child->getBaseDiscountAmount());
297+
$discountData->setOriginalAmount($discountData->getOriginalAmount() + $child->getOriginalDiscountAmount());
298+
$discountData->setBaseOriginalAmount(
299+
$discountData->getBaseOriginalAmount() + $child->getBaseOriginalDiscountAmount()
300+
);
301+
}
302+
303+
return $discountData;
304+
}
305+
306+
/**
307+
* Distributes discount applied from parent item to its children items
308+
*
309+
* This method originates from \Magento\SalesRule\Model\Quote\Discount::distributeDiscount()
310+
*
311+
* @param Data $discountData
312+
* @param AbstractItem $item
313+
* @see \Magento\SalesRule\Model\Quote\Discount::distributeDiscount()
314+
*/
315+
private function distributeDiscount(Data $discountData, AbstractItem $item): void
316+
{
317+
$data = [
318+
'discount_amount' => $discountData->getAmount() - $item->getDiscountAmount(),
319+
'base_discount_amount' => $discountData->getBaseAmount() - $item->getBaseDiscountAmount(),
320+
];
321+
322+
$parentBaseRowTotal = max(0, $item->getBaseRowTotal() - $item->getBaseDiscountAmount());
323+
$keys = array_keys($data);
324+
$roundingDelta = [];
325+
foreach ($keys as $key) {
326+
//Initialize the rounding delta to a tiny number to avoid floating point precision problem
327+
$roundingDelta[$key] = 0.0000001;
328+
}
329+
foreach ($item->getChildren() as $child) {
330+
$childBaseRowTotalWithDiscount = max(0, $child->getBaseRowTotal() - $child->getBaseDiscountAmount());
331+
$ratio = min(1, $parentBaseRowTotal != 0 ? $childBaseRowTotalWithDiscount / $parentBaseRowTotal : 0);
332+
foreach ($keys as $key) {
333+
$value = $data[$key] * $ratio;
334+
$roundedValue = $this->priceCurrency->round($value + $roundingDelta[$key]);
335+
$roundingDelta[$key] += $value - $roundedValue;
336+
$child->setData($key, $child->getData($key) + $roundedValue);
337+
}
338+
}
339+
}
340+
267341
/**
268342
* Get discount Data
269343
*

dev/tests/integration/testsuite/Magento/Sales/Model/Service/InvoiceServiceTest.php

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2019 Adobe
4+
* All Rights Reserved.
55
*/
6+
67
declare(strict_types=1);
78

89
namespace Magento\Sales\Model\Service;
910

11+
use Magento\Bundle\Test\Fixture\AddProductToCart as AddBundleProductToCartFixture;
12+
use Magento\Bundle\Test\Fixture\Option as BundleOptionFixture;
13+
use Magento\Bundle\Test\Fixture\Product as BundleProductFixture;
14+
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
15+
use Magento\Checkout\Test\Fixture\PlaceOrder as PlaceOrderFixture;
16+
use Magento\Checkout\Test\Fixture\SetBillingAddress as SetBillingAddressFixture;
17+
use Magento\Checkout\Test\Fixture\SetDeliveryMethod as SetDeliveryMethodFixture;
18+
use Magento\Checkout\Test\Fixture\SetGuestEmail as SetGuestEmailFixture;
19+
use Magento\Checkout\Test\Fixture\SetPaymentMethod as SetPaymentMethodFixture;
20+
use Magento\Checkout\Test\Fixture\SetShippingAddress as SetShippingAddressFixture;
21+
use Magento\Quote\Test\Fixture\GuestCart as GuestCartFixture;
1022
use Magento\Sales\Api\Data\OrderInterface;
23+
use Magento\Sales\Api\OrderRepositoryInterface;
1124
use Magento\Sales\Model\Order;
25+
use Magento\SalesRule\Model\Rule;
26+
use Magento\SalesRule\Test\Fixture\ProductCondition as ProductConditionFixture;
27+
use Magento\SalesRule\Test\Fixture\Rule as RuleFixture;
28+
use Magento\TestFramework\Fixture\DataFixture;
29+
use Magento\TestFramework\Fixture\DataFixtureStorage;
30+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
1231
use Magento\TestFramework\Helper\Bootstrap;
1332

1433
/**
1534
* Tests \Magento\Sales\Model\Service\InvoiceService
35+
*
36+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
1637
*/
1738
class InvoiceServiceTest extends \PHPUnit\Framework\TestCase
1839
{
@@ -21,12 +42,24 @@ class InvoiceServiceTest extends \PHPUnit\Framework\TestCase
2142
*/
2243
private $invoiceService;
2344

45+
/**
46+
* @var OrderRepositoryInterface|null
47+
*/
48+
private ?OrderRepositoryInterface $orderRepository;
49+
50+
/**
51+
* @var DataFixtureStorage|null
52+
*/
53+
private ?DataFixtureStorage $fixtures;
54+
2455
/**
2556
* @inheritdoc
2657
*/
2758
protected function setUp(): void
2859
{
2960
$this->invoiceService = Bootstrap::getObjectManager()->create(InvoiceService::class);
61+
$this->orderRepository = Bootstrap::getObjectManager()->create(OrderRepositoryInterface::class);
62+
$this->fixtures = DataFixtureStorageManager::getStorage();
3063
}
3164

3265
/**
@@ -40,6 +73,7 @@ public function testPrepareInvoiceConfigurableProduct(int $invoiceQty): void
4073
/** @var OrderInterface $order */
4174
$order = Bootstrap::getObjectManager()->create(Order::class)->load('100000001', 'increment_id');
4275
$orderItems = $order->getItems();
76+
$parentItemId = 0;
4377
foreach ($orderItems as $orderItem) {
4478
if ($orderItem->getParentItemId()) {
4579
$parentItemId = $orderItem->getParentItemId();
@@ -164,6 +198,55 @@ public static function bundleProductQtyOrderedDataProvider(): array
164198
];
165199
}
166200

201+
#[
202+
DataFixture(ProductFixture::class, ['price' => 10], as: 'p1'),
203+
DataFixture(ProductFixture::class, ['price' => 20], as: 'p2'),
204+
DataFixture(BundleOptionFixture::class, ['product_links' => ['$p1$']], 'opt1'),
205+
DataFixture(BundleOptionFixture::class, ['product_links' => ['$p2$']], 'opt2'),
206+
DataFixture(BundleProductFixture::class, ['_options' => ['$opt1$', '$opt2$']], 'bp1'),
207+
DataFixture(ProductConditionFixture::class, ['attribute' => 'sku', 'value' => '$bp1.sku$'], 'cond1'),
208+
DataFixture(
209+
RuleFixture::class,
210+
[
211+
'simple_action' => Rule::BY_PERCENT_ACTION,
212+
'discount_amount' => 20,
213+
'actions' => ['$cond1$'],
214+
'simple_free_shipping' => \Magento\OfflineShipping\Model\SalesRule\Rule::FREE_SHIPPING_ITEM
215+
]
216+
),
217+
DataFixture(GuestCartFixture::class, as: 'cart'),
218+
DataFixture(
219+
AddBundleProductToCartFixture::class,
220+
[
221+
'cart_id' => '$cart.id$',
222+
'product_id' => '$bp1.id$',
223+
'selections' => [['$p1.id$'], ['$p2.id$']],
224+
'qty' => 1
225+
],
226+
),
227+
DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
228+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
229+
DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']),
230+
DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']),
231+
DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']),
232+
DataFixture(PlaceOrderFixture::class, ['cart_id' => '$cart.id$'], 'order'),
233+
]
234+
public function testPrepareInvoiceBundleProductDynamicPriceWithDiscount(): void
235+
{
236+
$order = $this->fixtures->get('order');
237+
$order = $this->orderRepository->get($order->getId());
238+
$qtyToInvoice = [];
239+
foreach ($order->getAllItems() as $item) {
240+
if (!$item->getParentItemId()) {
241+
$qtyToInvoice[$item->getId()] = 1;
242+
}
243+
}
244+
$this->assertNotEmpty($qtyToInvoice);
245+
$invoice = $this->invoiceService->prepareInvoice($order, $qtyToInvoice);
246+
$this->assertEquals(-6, $invoice->getBaseDiscountAmount());
247+
$this->assertEquals(24, $invoice->getBaseGrandTotal());
248+
}
249+
167250
/**
168251
* Associate product qty to invoice to order item id.
169252
*

0 commit comments

Comments
 (0)