Skip to content

Commit df814c9

Browse files
authored
ENGCOM-3674: [Forwardport] Cart-Sales-Rule-with-negated-condition-over-special-price-does-not-work-for-configurable-products. #19343
2 parents c4498aa + ce2ef84 commit df814c9

File tree

5 files changed

+315
-9
lines changed

5 files changed

+315
-9
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
* Prepare configurable product for validation.
21+
*
22+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
23+
* @param \Magento\Framework\Model\AbstractModel $model
24+
* @return array
25+
*/
26+
public function beforeValidate(
27+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
28+
\Magento\Framework\Model\AbstractModel $model
29+
) {
30+
$product = $this->getProductToValidate($subject, $model);
31+
if ($model->getProduct() !== $product) {
32+
// We need to replace product only for validation and keep original product for all other cases.
33+
$clone = clone $model;
34+
$clone->setProduct($product);
35+
$model = $clone;
36+
}
37+
38+
return [$model];
39+
}
40+
41+
/**
42+
* Select proper product for validation.
43+
*
44+
* @param \Magento\SalesRule\Model\Rule\Condition\Product $subject
45+
* @param \Magento\Framework\Model\AbstractModel $model
46+
*
47+
* @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product
48+
*/
49+
private function getProductToValidate(
50+
\Magento\SalesRule\Model\Rule\Condition\Product $subject,
51+
\Magento\Framework\Model\AbstractModel $model
52+
) {
53+
/** @var \Magento\Catalog\Model\Product $product */
54+
$product = $model->getProduct();
55+
56+
$attrCode = $subject->getAttribute();
57+
58+
/* Check for attributes which are not available for configurable products */
59+
if ($product->getTypeId() == Configurable::TYPE_CODE && !$product->hasData($attrCode)) {
60+
/** @var \Magento\Catalog\Model\AbstractModel $childProduct */
61+
$childProduct = current($model->getChildren())->getProduct();
62+
if ($childProduct->hasData($attrCode)) {
63+
$product = $childProduct;
64+
}
65+
}
66+
67+
return $product;
68+
}
69+
}

app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,9 @@
153153
<argument name="sortBy" value="price"/>
154154
<argument name="sort" value="desc"/>
155155
</actionGroup>
156-
<see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/>
157-
<see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/>
158-
<see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/>
156+
<see selector="{{StorefrontCategoryMainSection.lineProductName('1')}}" userInput="$$createConfigProduct.name$$" stepKey="seeConfigurableProduct2"/>
157+
<see selector="{{StorefrontCategoryMainSection.lineProductName('2')}}" userInput="$$createSimpleProduct2.name$$" stepKey="seeSimpleProductTwo2"/>
158+
<see selector="{{StorefrontCategoryMainSection.lineProductName('3')}}" userInput="$$createSimpleProduct.name$$" stepKey="seeSimpleProduct2"/>
159159

160160
<!-- Delete the rule -->
161161
<amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/>
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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($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+
$this->validator->setAttribute('special_price');
223+
224+
$this->validatorPlugin->beforeValidate($this->validator, $item);
225+
}
226+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,7 @@
242242
</argument>
243243
</arguments>
244244
</type>
245+
<type name="Magento\SalesRule\Model\Rule\Condition\Product">
246+
<plugin name="apply_rule_on_configurable_children" type="Magento\ConfigurableProduct\Plugin\SalesRule\Model\Rule\Condition\Product" />
247+
</type>
245248
</config>

0 commit comments

Comments
 (0)