Skip to content

Commit 16f1eef

Browse files
ENGCOM-2111: #14020-Cart-Sales-Rule-with-negated-condition-over-special-price-does… #16342
2 parents 4afa4d1 + a239900 commit 16f1eef

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition;
9+
10+
use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
11+
12+
/**
13+
* Class Product
14+
*
15+
* @package Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition
16+
*/
17+
class Product
18+
{
19+
/**
20+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
21+
* @param \Magento\Framework\Model\AbstractModel $model
22+
*/
23+
public function beforeValidate(
24+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
25+
\Magento\Framework\Model\AbstractModel $model
26+
) {
27+
$product = $this->getProductToValidate($subject, $model);
28+
if ($model->getProduct() !== $product) {
29+
// We need to replace product only for validation and keep original product for all other cases.
30+
$clone = clone $model;
31+
$clone->setProduct($product);
32+
$model = $clone;
33+
}
34+
35+
return [$model];
36+
}
37+
38+
/**
39+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
40+
* @param \Magento\Framework\Model\AbstractModel $model
41+
*
42+
* @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product
43+
*/
44+
private function getProductToValidate(
45+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
46+
\Magento\Framework\Model\AbstractModel $model
47+
) {
48+
/** @var \Magento\Catalog\Model\Product $product */
49+
$product = $model->getProduct();
50+
51+
$attrCode = $subject->getAttribute();
52+
53+
/* Check for attributes which are not available for configurable products */
54+
if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) {
55+
/** @var \Magento\Catalog\Model\AbstractModel $childProduct */
56+
$childProduct = current($model->getChildren())->getProduct();
57+
if ($childProduct->hasData($attrCode)) {
58+
$product = $childProduct;
59+
}
60+
}
61+
62+
return $product;
63+
}
64+
}
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/etc/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@
228228
<type name="Magento\Catalog\Model\Product">
229229
<plugin name="product_identities_extender" type="Magento\ConfigurableProduct\Model\Plugin\ProductIdentitiesExtender" />
230230
</type>
231+
<type name="Magento\SalesRule\Model\Rule\Condition\Product">
232+
<plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" />
233+
</type>
231234
<type name="Magento\Catalog\Model\Product\Configuration\Item\ItemResolverComposite">
232235
<arguments>
233236
<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 <core@magentocommerce.com>
13+
*
14+
* @method string getAttribute()
1315
*/
1416
class Product extends \Magento\Rule\Model\Condition\Product\AbstractProduct
1517
{
@@ -161,7 +163,9 @@ public function asArray(array $arrAttributes = [])
161163
* Validate Product Rule Condition
162164
*
163165
* @param \Magento\Framework\Model\AbstractModel $model
166+
*
164167
* @return bool
168+
* @throws \Magento\Framework\Exception\NoSuchEntityException
165169
*/
166170
public function validate(\Magento\Framework\Model\AbstractModel $model)
167171
{

0 commit comments

Comments
 (0)