Skip to content

Commit afa280c

Browse files
committed
Merge remote-tracking branch 'origin/MAGETWO-65458' into 2.3-develop-pr23
2 parents 1d0ef5b + b72f5c3 commit afa280c

File tree

11 files changed

+417
-4
lines changed

11 files changed

+417
-4
lines changed

app/code/Magento/Bundle/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"suggest": {
2727
"magento/module-webapi": "*",
28-
"magento/module-bundle-sample-data": "*"
28+
"magento/module-bundle-sample-data": "*",
29+
"magento/module-sales-rule": "*"
2930
},
3031
"type": "magento2-module",
3132
"license": [

app/code/Magento/Bundle/etc/di.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,11 @@
207207
</argument>
208208
</arguments>
209209
</type>
210+
<type name="Magento\SalesRule\Model\Quote\ChildrenValidationLocator">
211+
<arguments>
212+
<argument name="productTypeChildrenValidationMap" xsi:type="array">
213+
<item name="bundle" xsi:type="boolean">false</item>
214+
</argument>
215+
</arguments>
216+
</type>
210217
</config>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\SalesRule\Model\Quote;
10+
11+
use Magento\Quote\Model\Quote\Item\AbstractItem as QuoteItem;
12+
13+
/**
14+
* Used to determine necessity to validate rule on item's children that may depends on product type.
15+
*/
16+
class ChildrenValidationLocator
17+
{
18+
/**
19+
* @var array
20+
*/
21+
private $productTypeChildrenValidationMap;
22+
23+
/**
24+
* @param array $productTypeChildrenValidationMap
25+
* <pre>
26+
* [
27+
* 'ProductType1' => true,
28+
* 'ProductType2' => false
29+
* ]
30+
* </pre>
31+
*/
32+
public function __construct(
33+
array $productTypeChildrenValidationMap = []
34+
) {
35+
$this->productTypeChildrenValidationMap = $productTypeChildrenValidationMap;
36+
}
37+
38+
/**
39+
* Checks necessity to validate rule on item's children.
40+
*
41+
* @param QuoteItem $item
42+
* @return bool
43+
*/
44+
public function isChildrenValidationRequired(QuoteItem $item): bool
45+
{
46+
$type = $item->getProduct()->getTypeId();
47+
if (isset($this->productTypeChildrenValidationMap[$type])) {
48+
return (bool)$this->productTypeChildrenValidationMap[$type];
49+
}
50+
51+
return true;
52+
}
53+
}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
namespace Magento\SalesRule\Model;
77

88
use Magento\Quote\Model\Quote\Address;
9+
use Magento\SalesRule\Model\Quote\ChildrenValidationLocator;
10+
use Magento\Framework\App\ObjectManager;
11+
use Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory;
912

1013
/**
1114
* Class RulesApplier
@@ -25,19 +28,33 @@ class RulesApplier
2528
*/
2629
protected $validatorUtility;
2730

31+
/**
32+
* @var ChildrenValidationLocator
33+
*/
34+
private $childrenValidationLocator;
35+
36+
/**
37+
* @var CalculatorFactory
38+
*/
39+
private $calculatorFactory;
40+
2841
/**
2942
* @param \Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory
3043
* @param \Magento\Framework\Event\ManagerInterface $eventManager
3144
* @param \Magento\SalesRule\Model\Utility $utility
45+
* @param ChildrenValidationLocator|null $childrenValidationLocator
3246
*/
3347
public function __construct(
3448
\Magento\SalesRule\Model\Rule\Action\Discount\CalculatorFactory $calculatorFactory,
3549
\Magento\Framework\Event\ManagerInterface $eventManager,
36-
\Magento\SalesRule\Model\Utility $utility
50+
\Magento\SalesRule\Model\Utility $utility,
51+
ChildrenValidationLocator $childrenValidationLocator = null
3752
) {
3853
$this->calculatorFactory = $calculatorFactory;
3954
$this->validatorUtility = $utility;
4055
$this->_eventManager = $eventManager;
56+
$this->childrenValidationLocator = $childrenValidationLocator
57+
?: ObjectManager::getInstance()->get(ChildrenValidationLocator::class);
4158
}
4259

4360
/**
@@ -61,6 +78,9 @@ public function applyRules($item, $rules, $skipValidation, $couponCode)
6178
}
6279

6380
if (!$skipValidation && !$rule->getActions()->validate($item)) {
81+
if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) {
82+
continue;
83+
}
6484
$childItems = $item->getChildren();
6585
$isContinue = true;
6686
if (!empty($childItems)) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\SalesRule\Test\Unit\Model\Quote;
10+
11+
use Magento\SalesRule\Model\Quote\ChildrenValidationLocator;
12+
use Magento\Quote\Model\Quote\Item\AbstractItem as QuoteItem;
13+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
14+
use Magento\Catalog\Model\Product;
15+
16+
/**
17+
* Test for Magento\SalesRule\Model\Quote\ChildrenValidationLocator
18+
*/
19+
class ChildrenValidationLocatorTest extends \PHPUnit\Framework\TestCase
20+
{
21+
/**
22+
* @var array
23+
*/
24+
private $productTypeChildrenValidationMap;
25+
26+
/**
27+
* @var ObjectManager
28+
*/
29+
private $objectManager;
30+
31+
/**
32+
* @var ChildrenValidationLocator
33+
*/
34+
private $model;
35+
36+
/**
37+
* @var QuoteItem|\PHPUnit_Framework_MockObject_MockObject
38+
*/
39+
private $quoteItemMock;
40+
41+
/**
42+
* @var Product|\PHPUnit_Framework_MockObject_MockObject
43+
*/
44+
private $productMock;
45+
46+
protected function setUp()
47+
{
48+
$this->objectManager = new ObjectManager($this);
49+
50+
$this->productTypeChildrenValidationMap = [
51+
'type1' => true,
52+
'type2' => false,
53+
];
54+
55+
$this->quoteItemMock = $this->getMockBuilder(QuoteItem::class)
56+
->disableOriginalConstructor()
57+
->setMethods(['getProduct'])
58+
->getMockForAbstractClass();
59+
60+
$this->productMock = $this->getMockBuilder(Product::class)
61+
->disableOriginalConstructor()
62+
->setMethods(['getTypeId'])
63+
->getMock();
64+
65+
$this->model = $this->objectManager->getObject(
66+
ChildrenValidationLocator::class,
67+
[
68+
'productTypeChildrenValidationMap' => $this->productTypeChildrenValidationMap,
69+
]
70+
);
71+
}
72+
73+
/**
74+
* @dataProvider productTypeDataProvider
75+
* @param string $type
76+
* @param bool $expected
77+
*
78+
* @return void
79+
*/
80+
public function testIsChildrenValidationRequired(string $type, bool $expected): void
81+
{
82+
$this->quoteItemMock->expects($this->once())
83+
->method('getProduct')
84+
->willReturn($this->productMock);
85+
86+
$this->productMock->expects($this->once())
87+
->method('getTypeId')
88+
->willReturn($type);
89+
90+
$this->assertEquals($this->model->isChildrenValidationRequired($this->quoteItemMock), $expected);
91+
}
92+
93+
/**
94+
* @return array
95+
*/
96+
public function productTypeDataProvider(): array
97+
{
98+
return [
99+
['type1', true],
100+
['type2', false],
101+
['type3', true],
102+
];
103+
}
104+
}

app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
namespace Magento\SalesRule\Test\Unit\Model;
88

9+
/**
10+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
11+
*/
912
class RulesApplierTest extends \PHPUnit\Framework\TestCase
1013
{
1114
/**
@@ -28,6 +31,11 @@ class RulesApplierTest extends \PHPUnit\Framework\TestCase
2831
*/
2932
protected $validatorUtility;
3033

34+
/**
35+
* @var \Magento\SalesRule\Model\Quote\ChildrenValidationLocator|\PHPUnit_Framework_MockObject_MockObject
36+
*/
37+
protected $childrenValidationLocator;
38+
3139
protected function setUp()
3240
{
3341
$this->calculatorFactory = $this->createMock(
@@ -38,11 +46,15 @@ protected function setUp()
3846
\Magento\SalesRule\Model\Utility::class,
3947
['canProcessRule', 'minFix', 'deltaRoundingFix', 'getItemQty']
4048
);
41-
49+
$this->childrenValidationLocator = $this->createPartialMock(
50+
\Magento\SalesRule\Model\Quote\ChildrenValidationLocator::class,
51+
['isChildrenValidationRequired']
52+
);
4253
$this->rulesApplier = new \Magento\SalesRule\Model\RulesApplier(
4354
$this->calculatorFactory,
4455
$this->eventManager,
45-
$this->validatorUtility
56+
$this->validatorUtility,
57+
$this->childrenValidationLocator
4658
);
4759
}
4860

@@ -84,6 +96,10 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren,
8496
$item->setDiscountCalculationPrice($positivePrice);
8597
$item->setData('calculation_price', $positivePrice);
8698

99+
$this->childrenValidationLocator->expects($this->any())
100+
->method('isChildrenValidationRequired')
101+
->willReturn(true);
102+
87103
$this->validatorUtility->expects($this->atLeastOnce())
88104
->method('canProcessRule')
89105
->will($this->returnValue(true));
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Magento\TestFramework\Helper\Bootstrap;
10+
11+
require __DIR__ . 'product_with_multiple_options.php';
12+
13+
$objectManager = Bootstrap::getObjectManager();
14+
15+
/** @var $product \Magento\Catalog\Model\Product */
16+
$product = $objectManager->create(\Magento\Catalog\Model\Product::class);
17+
$product->load(3);
18+
19+
/** @var $typeInstance \Magento\Bundle\Model\Product\Type */
20+
$typeInstance = $product->getTypeInstance();
21+
$typeInstance->setStoreFilter($product->getStoreId(), $product);
22+
$optionCollection = $typeInstance->getOptionsCollection($product);
23+
24+
$bundleOptions = [];
25+
$bundleOptionsQty = [];
26+
foreach ($optionCollection as $option) {
27+
/** @var $option \Magento\Bundle\Model\Option */
28+
$selectionsCollection = $typeInstance->getSelectionsCollection([$option->getId()], $product);
29+
if ($option->isMultiSelection()) {
30+
$bundleOptions[$option->getId()] = array_column($selectionsCollection->toArray(), 'selection_id');
31+
} else {
32+
$bundleOptions[$option->getId()] = $selectionsCollection->getFirstItem()->getSelectionId();
33+
}
34+
$bundleOptionsQty[$option->getId()] = 1;
35+
}
36+
37+
$requestInfo = new \Magento\Framework\DataObject(
38+
[
39+
'product' => $product->getId(),
40+
'bundle_option' => $bundleOptions,
41+
'bundle_option_qty' => $bundleOptionsQty,
42+
'qty' => 1,
43+
]
44+
);
45+
46+
/** @var $cart \Magento\Checkout\Model\Cart */
47+
$cart = Bootstrap::getObjectManager()->create(\Magento\Checkout\Model\Cart::class);
48+
$cart->addProduct($product, $requestInfo);
49+
$cart->getQuote()->setReservedOrderId('test_cart_with_bundle_and_options');
50+
$cart->save();
51+
52+
/** @var $objectManager \Magento\TestFramework\ObjectManager */
53+
$objectManager = Bootstrap::getObjectManager();
54+
$objectManager->removeSharedInstance(\Magento\Checkout\Model\Session::class);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
/** @var \Magento\Framework\Registry $registry */
10+
$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class);
11+
12+
$registry->unregister('isSecureArea');
13+
$registry->register('isSecureArea', true);
14+
15+
/** @var $objectManager \Magento\TestFramework\ObjectManager */
16+
$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
17+
$quote = $objectManager->create(\Magento\Quote\Model\Quote::class);
18+
$quote->load('test_cart_with_bundle_and_options', 'reserved_order_id');
19+
$quote->delete();
20+
21+
/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */
22+
$quoteIdMask = $objectManager->create(\Magento\Quote\Model\QuoteIdMask::class);
23+
$quoteIdMask->delete($quote->getId());
24+
25+
require __DIR__ . 'product_with_multiple_options_rollback.php';
26+
27+
$registry->unregister('isSecureArea');
28+
$registry->register('isSecureArea', false);

dev/tests/integration/testsuite/Magento/SalesRule/Model/Rule/Condition/ProductTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ public function testValidateCategorySalesRuleIncludesChildren($categoryId, $expe
5858
$this->assertEquals($expectedResult, $rule->validate($quote));
5959
}
6060

61+
/**
62+
* @magentoDbIsolation disabled
63+
* @magentoDataFixture Magento/Bundle/_files/order_item_with_bundle_and_options.php
64+
* @magentoDataFixture Magento/SalesRule/_files/rules_sku_exclude.php
65+
*
66+
* @return void
67+
*/
68+
public function testValidateSalesRuleExcludesBundleChildren(): void
69+
{
70+
// Load the quote that contains a child of a bundle product
71+
/** @var \Magento\Quote\Model\Quote $quote */
72+
$quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class)
73+
->load('test_cart_with_bundle_and_options', 'reserved_order_id');
74+
75+
// Load the SalesRule looking for excluding products with selected sku
76+
/** @var $rule \Magento\SalesRule\Model\Rule */
77+
$rule = $this->objectManager->get(\Magento\Framework\Registry::class)
78+
->registry('_fixture/Magento_SalesRule_Sku_Exclude');
79+
80+
$this->assertEquals(false, $rule->validate($quote));
81+
}
82+
6183
/**
6284
* @return array
6385
*/

0 commit comments

Comments
 (0)