Skip to content

Commit 74e8a6e

Browse files
ENGCOM-2111: #14020-Cart-Sales-Rule-with-negated-condition-over-special-price-does… #16342
- Merge Pull Request #16342 from novikor/magento2:#14020-Cart-Sales-Rule-with-negated-condition-over-special-price-does-not-work-for-configurable-products - Merged commits: 1. 90b6803 2. 2717cb1 3. d2a0de8 4. fae98c0 5. 9a35b45 6. 887ee4a 7. 0c9aa81 8. 4e68337 9. 5b95b22 10. 618f408 11. 7c8482b 12. e7130bf 13. 5a7e78a 14. 8d417ac 15. 90ff989 16. c52e0e8 17. 5c3154b 18. 51adb9d
2 parents 34ee561 + 51adb9d commit 74e8a6e

File tree

5 files changed

+291
-0
lines changed

5 files changed

+291
-0
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition;
4+
5+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
6+
7+
/**
8+
* Class Product
9+
*
10+
* @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition
11+
*/
12+
class Product
13+
{
14+
/**
15+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
16+
* @param \Magento\Framework\Model\AbstractModel $model
17+
*/
18+
public function beforeValidate(
19+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
20+
\Magento\Framework\Model\AbstractModel $model
21+
) {
22+
$model->setProduct(
23+
$this->getProductToValidate($subject, $model)
24+
);
25+
}
26+
27+
/**
28+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
29+
* @param \Magento\Framework\Model\AbstractModel $model
30+
*
31+
* @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product
32+
*/
33+
private function getProductToValidate(
34+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
35+
\Magento\Framework\Model\AbstractModel $model
36+
) {
37+
/** @var \Magento\Catalog\Model\Product $product */
38+
$product = $model->getProduct();
39+
40+
$attrCode = $subject->getAttribute();
41+
42+
/* Check for attributes which are not available for configurable products */
43+
if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) {
44+
/** @var \Magento\Catalog\Model\AbstractModel $childProduct */
45+
$childProduct = current($model->getChildren())->getProduct();
46+
if ($childProduct->hasData($attrCode)) {
47+
$product = $childProduct;
48+
}
49+
}
50+
51+
return $product;
52+
}
53+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
<?php declare(strict_types=1);
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\ConfigurableProduct\Test\Unit\Plugin\SalesRule\Model\Rule\Condition;
8+
9+
use Magento\Backend\Helper\Data;
10+
use Magento\Catalog\Api\ProductRepositoryInterface;
11+
use Magento\Catalog\Model\Product\Type;
12+
use Magento\Catalog\Model\ProductFactory;
13+
use Magento\Catalog\Model\ResourceModel\Product;
14+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
15+
use Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product as ValidatorPlugin;
16+
use Magento\Directory\Model\CurrencyFactory;
17+
use Magento\Eav\Model\Config;
18+
use Magento\Eav\Model\Entity\AbstractEntity;
19+
use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection;
20+
use Magento\Framework\App\ScopeResolverInterface;
21+
use Magento\Framework\Locale\Format;
22+
use Magento\Framework\Locale\FormatInterface;
23+
use Magento\Framework\Locale\ResolverInterface;
24+
use Magento\Quote\Model\Quote\Item\AbstractItem;
25+
use Magento\Rule\Model\Condition\Context;
26+
use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct;
27+
28+
/**
29+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
30+
* @SuppressWarnings(PHPMD.LongVariable)
31+
*/
32+
class ProductTest extends \PHPUnit\Framework\TestCase
33+
{
34+
/**
35+
* @var \Magento\Framework\TestFramework\Unit\Helper\ObjectManager
36+
*/
37+
private $objectManager;
38+
39+
/**
40+
* @var SalesRuleProduct
41+
*/
42+
private $validator;
43+
44+
/**
45+
* @var \Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product
46+
*/
47+
private $validatorPlugin;
48+
49+
public function setUp()
50+
{
51+
$this->objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
52+
$this->validator = $this->createValidator();
53+
$this->validatorPlugin = $this->objectManager->getObject(ValidatorPlugin::class);
54+
}
55+
56+
/**
57+
* @return \Magento\SalesRule\Model\Rule\Condition\Product
58+
*/
59+
private function createValidator(): SalesRuleProduct
60+
{
61+
/** @var Context|\PHPUnit_Framework_MockObject_MockObject $contextMock */
62+
$contextMock = $this->getMockBuilder(Context::class)
63+
->disableOriginalConstructor()
64+
->getMock();
65+
/** @var Data|\PHPUnit_Framework_MockObject_MockObject $backendHelperMock */
66+
$backendHelperMock = $this->getMockBuilder(Data::class)
67+
->disableOriginalConstructor()
68+
->getMock();
69+
/** @var Config|\PHPUnit_Framework_MockObject_MockObject $configMock */
70+
$configMock = $this->getMockBuilder(Config::class)
71+
->disableOriginalConstructor()
72+
->getMock();
73+
/** @var ProductFactory|\PHPUnit_Framework_MockObject_MockObject $productFactoryMock */
74+
$productFactoryMock = $this->getMockBuilder(ProductFactory::class)
75+
->disableOriginalConstructor()
76+
->getMock();
77+
/** @var ProductRepositoryInterface|\PHPUnit_Framework_MockObject_MockObject $productRepositoryMock */
78+
$productRepositoryMock = $this->getMockBuilder(ProductRepositoryInterface::class)
79+
->getMockForAbstractClass();
80+
$attributeLoaderInterfaceMock = $this->getMockBuilder(AbstractEntity::class)
81+
->disableOriginalConstructor()
82+
->setMethods(['getAttributesByCode'])
83+
->getMock();
84+
$attributeLoaderInterfaceMock
85+
->expects($this->any())
86+
->method('getAttributesByCode')
87+
->willReturn([]);
88+
/** @var Product|\PHPUnit_Framework_MockObject_MockObject $productMock */
89+
$productMock = $this->getMockBuilder(Product::class)
90+
->disableOriginalConstructor()
91+
->setMethods(['loadAllAttributes', 'getConnection', 'getTable'])
92+
->getMock();
93+
$productMock->expects($this->any())
94+
->method('loadAllAttributes')
95+
->willReturn($attributeLoaderInterfaceMock);
96+
/** @var Collection|\PHPUnit_Framework_MockObject_MockObject $collectionMock */
97+
$collectionMock = $this->getMockBuilder(Collection::class)
98+
->disableOriginalConstructor()
99+
->getMock();
100+
/** @var FormatInterface|\PHPUnit_Framework_MockObject_MockObject $formatMock */
101+
$formatMock = new Format(
102+
$this->getMockBuilder(ScopeResolverInterface::class)->disableOriginalConstructor()->getMock(),
103+
$this->getMockBuilder(ResolverInterface::class)->disableOriginalConstructor()->getMock(),
104+
$this->getMockBuilder(CurrencyFactory::class)->disableOriginalConstructor()->getMock()
105+
);
106+
107+
return new SalesRuleProduct(
108+
$contextMock,
109+
$backendHelperMock,
110+
$configMock,
111+
$productFactoryMock,
112+
$productRepositoryMock,
113+
$productMock,
114+
$collectionMock,
115+
$formatMock
116+
);
117+
}
118+
119+
public function testChildIsUsedForValidation()
120+
{
121+
$configurableProductMock = $this->createProductMock();
122+
$configurableProductMock
123+
->expects($this->any())
124+
->method('getTypeId')
125+
->willReturn(Configurable::TYPE_CODE);
126+
$configurableProductMock
127+
->expects($this->any())
128+
->method('hasData')
129+
->with($this->equalTo('special_price'))
130+
->willReturn(false);
131+
132+
/* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */
133+
$item = $this->getMockBuilder(AbstractItem::class)
134+
->disableOriginalConstructor()
135+
->setMethods(['setProduct', 'getProduct', 'getChildren'])
136+
->getMockForAbstractClass();
137+
$item->expects($this->any())
138+
->method('getProduct')
139+
->willReturn($configurableProductMock);
140+
141+
$simpleProductMock = $this->createProductMock();
142+
$simpleProductMock
143+
->expects($this->any())
144+
->method('getTypeId')
145+
->willReturn(Type::TYPE_SIMPLE);
146+
$simpleProductMock
147+
->expects($this->any())
148+
->method('hasData')
149+
->with($this->equalTo('special_price'))
150+
->willReturn(true);
151+
152+
$childItem = $this->getMockBuilder(AbstractItem::class)
153+
->disableOriginalConstructor()
154+
->setMethods(['getProduct'])
155+
->getMockForAbstractClass();
156+
$childItem->expects($this->any())
157+
->method('getProduct')
158+
->willReturn($simpleProductMock);
159+
160+
$item->expects($this->any())
161+
->method('getChildren')
162+
->willReturn([$childItem]);
163+
$item->expects($this->once())
164+
->method('setProduct')
165+
->with($this->identicalTo($simpleProductMock));
166+
167+
$this->validator->setAttribute('special_price');
168+
169+
$this->validatorPlugin->beforeValidate($this->validator, $item);
170+
}
171+
172+
/**
173+
* @return Product|\PHPUnit_Framework_MockObject_MockObject
174+
*/
175+
private function createProductMock(): \PHPUnit_Framework_MockObject_MockObject
176+
{
177+
$productMock = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
178+
->disableOriginalConstructor()
179+
->setMethods([
180+
'getAttribute',
181+
'getId',
182+
'setQuoteItemQty',
183+
'setQuoteItemPrice',
184+
'getTypeId',
185+
'hasData',
186+
])
187+
->getMock();
188+
$productMock
189+
->expects($this->any())
190+
->method('setQuoteItemQty')
191+
->willReturnSelf();
192+
$productMock
193+
->expects($this->any())
194+
->method('setQuoteItemPrice')
195+
->willReturnSelf();
196+
197+
return $productMock;
198+
}
199+
200+
public function testChildIsNotUsedForValidation()
201+
{
202+
$simpleProductMock = $this->createProductMock();
203+
$simpleProductMock
204+
->expects($this->any())
205+
->method('getTypeId')
206+
->willReturn(Type::TYPE_SIMPLE);
207+
$simpleProductMock
208+
->expects($this->any())
209+
->method('hasData')
210+
->with($this->equalTo('special_price'))
211+
->willReturn(true);
212+
213+
/* @var AbstractItem|\PHPUnit_Framework_MockObject_MockObject $item */
214+
$item = $this->getMockBuilder(AbstractItem::class)
215+
->disableOriginalConstructor()
216+
->setMethods(['setProduct', 'getProduct'])
217+
->getMockForAbstractClass();
218+
$item->expects($this->any())
219+
->method('getProduct')
220+
->willReturn($simpleProductMock);
221+
222+
$item->expects($this->once())
223+
->method('setProduct')
224+
->with($this->identicalTo($simpleProductMock));
225+
226+
$this->validator->setAttribute('special_price');
227+
228+
$this->validatorPlugin->beforeValidate($this->validator, $item);
229+
}
230+
}

app/code/Magento/ConfigurableProduct/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"suggest": {
2020
"magento/module-webapi": "100.2.*",
2121
"magento/module-sales": "101.0.*",
22+
"magento/module-sales-rule": "101.0.*",
2223
"magento/module-product-video": "100.2.*",
2324
"magento/module-configurable-sample-data": "Sample Data version:100.2.*",
2425
"magento/module-product-links-sample-data": "Sample Data version:100.2.*"

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@
221221
<type name="Magento\Catalog\Model\Product">
222222
<plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\ProductIdentitiesExtender" />
223223
</type>
224+
<type name="Magento\SalesRule\Model\Rule\Condition\Product">
225+
<plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" />
226+
</type>
224227
<type name="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite">
225228
<arguments>
226229
<argument name="itemResolvers" xsi:type="array">

app/code/Magento/SalesRule/Model/Rule/Condition/Product.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
* Product rule condition data model
1111
*
1212
* @author Magento Core Team <[email protected]>
13+
*
14+
* @method string getAttribute()
1315
*/
1416
class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct
1517
{
@@ -31,7 +33,9 @@ protected function _addSpecialAttributes(array &$attributes)
3133
* Validate Product Rule Condition
3234
*
3335
* @param \Magento\Framework\Model\AbstractModel $model
36+
*
3437
* @return bool
38+
* @throws \Magento\Framework\Exception\NoSuchEntityException
3539
*/
3640
public function validate(\Magento\Framework\Model\AbstractModel $model)
3741
{

0 commit comments

Comments
 (0)