Skip to content

Commit ecb242f

Browse files
committed
ACP2E-225: Magento2 bug - The bundle products are not applicable to a certain cart price rule
- With test
1 parent f3e8f25 commit ecb242f

File tree

2 files changed

+293
-1
lines changed

2 files changed

+293
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ public function validate(\Magento\Framework\Model\AbstractModel $model)
164164
}
165165
}
166166
if ($hasValidChild || parent::validate($item)) {
167-
$total += ($hasValidChild && $useChildrenTotal)
167+
$total += ($hasValidChild && $useChildrenTotal && $childrenAttrTotal > 0)
168168
? $childrenAttrTotal * $item->getQty()
169169
: $item->getData($attr);
170170
}
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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\SalesRule\Test\Unit\Model\Rule\Condition\Product;
9+
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Framework\DataObject;
12+
use Magento\Framework\Model\AbstractModel;
13+
use Magento\Quote\Model\Quote;
14+
use Magento\Quote\Model\Quote\Item;
15+
use Magento\Quote\Model\Quote\Item\AbstractItem;
16+
use Magento\Rule\Model\Condition\Context;
17+
use Magento\Catalog\Model\Product\Type as ProductType;
18+
use Magento\SalesRule\Model\Rule\Condition\Product as SalesRuleProduct;
19+
use Magento\SalesRule\Model\Rule\Condition\Product\Subselect as SalesRuleProductSubselect;
20+
use PHPUnit\Framework\MockObject\MockObject;
21+
use PHPUnit\Framework\TestCase;
22+
23+
/**
24+
* Test class for checking sub select validation
25+
*/
26+
class SubselectTest extends TestCase
27+
{
28+
/** @var SalesRuleProductSubselect */
29+
private $model;
30+
31+
/** @var SalesRuleProduct|MockObject */
32+
private $ruleConditionMock;
33+
34+
/** @var MockObject */
35+
private $abstractModel;
36+
37+
/** @var Product|MockObject */
38+
private $productMock;
39+
40+
/** @var Quote|MockObject */
41+
private $quoteMock;
42+
43+
/** @var Item|MockObject */
44+
private $quoteItemMock;
45+
46+
protected function setUp(): void
47+
{
48+
$contextMock = $this->getMockBuilder(Context::class)
49+
->disableOriginalConstructor()
50+
->getMock();
51+
$this->ruleConditionMock = $this->getMockBuilder(SalesRuleProduct::class)
52+
->onlyMethods(['getAttribute', 'getValueParsed', 'getOperatorForValidate'])
53+
->addMethods(['setName', 'setAttributeScope'])
54+
->disableOriginalConstructor()
55+
->getMock();
56+
$this->abstractModel = $this->getMockBuilder(AbstractModel::class)
57+
->disableOriginalConstructor()
58+
->addMethods(['getQuote', 'getProduct'])
59+
->getMockForAbstractClass();
60+
$this->productMock = $this->getMockBuilder(Product::class)
61+
->onlyMethods(['getData', 'getResource', 'hasData'])
62+
->addMethods(['getOperatorForValidate', 'getValueParsed'])
63+
->disableOriginalConstructor()
64+
->getMock();
65+
$this->quoteMock = $this->getMockBuilder(Quote::class)
66+
->disableOriginalConstructor()
67+
->onlyMethods(['getAllVisibleItems'])
68+
->getMock();
69+
$this->quoteItemMock = $this->getMockBuilder(Item::class)
70+
->addMethods(['getHasChildren', 'getProductId'])
71+
->onlyMethods(
72+
[
73+
'getData',
74+
'getProduct',
75+
'getProductType',
76+
'getChildren',
77+
'getQuote',
78+
'getAddress',
79+
'getOptionByCode'
80+
]
81+
)
82+
->disableOriginalConstructor()
83+
->getMock();
84+
$this->quoteMock->expects($this->any())
85+
->method('getAllVisibleItems')
86+
->willReturn([$this->quoteItemMock]);
87+
$this->abstractModel->expects($this->any())
88+
->method('getQuote')
89+
->willReturn($this->quoteMock);
90+
$this->abstractModel->expects($this->any())
91+
->method('getProduct')
92+
->willReturn($this->productMock);
93+
$this->model = new SalesRuleProductSubselect(
94+
$contextMock,
95+
$this->ruleConditionMock,
96+
[]
97+
);
98+
}
99+
100+
/**
101+
* Tests validate for fixed bundle product
102+
*
103+
* @param array|null $attributeDetails
104+
* @param array $productDetails
105+
* @param bool $expectedResult
106+
* @return void
107+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
108+
* @dataProvider dataProviderForFixedBundleProduct
109+
*/
110+
public function testValidateForFixedBundleProduct(
111+
?array $attributeDetails,
112+
array $productDetails,
113+
bool $expectedResult
114+
): void {
115+
$attributeResource = new DataObject();
116+
if ($attributeDetails) {
117+
$attributeResource->setAttribute($attributeDetails['id']);
118+
$this->ruleConditionMock->expects($this->any())
119+
->method('setName')
120+
->willReturn($attributeDetails['name']);
121+
$this->ruleConditionMock->expects($this->any())
122+
->method('setAttributeScope')
123+
->willReturn($attributeDetails['attributeScope']);
124+
$this->ruleConditionMock->expects($this->any())
125+
->method('getAttribute')
126+
->willReturn($attributeDetails['id']);
127+
$this->model->setData('conditions', [$this->ruleConditionMock]);
128+
$this->model->setData('attribute', $attributeDetails['id']);
129+
$this->model->setData('value', $productDetails['valueParsed']);
130+
$this->model->setData('operator', $attributeDetails['attributeOperator']);
131+
$this->productMock->expects($this->any())
132+
->method('hasData')
133+
->with($attributeDetails['id'])
134+
->willReturn(!empty($productDetails));
135+
$this->productMock->expects($this->any())
136+
->method('getData')
137+
->with($attributeDetails['id'])
138+
->willReturn($productDetails['price']);
139+
$this->ruleConditionMock->expects($this->any())
140+
->method('getValueParsed')
141+
->willReturn($productDetails['valueParsed']);
142+
$this->ruleConditionMock->expects($this->any())->method('getOperatorForValidate')
143+
->willReturn($attributeDetails['attributeOperator']);
144+
}
145+
$this->quoteItemMock->expects($this->any())
146+
->method('getProductType')
147+
->willReturn($productDetails['type']);
148+
149+
/* @var AbstractItem|MockObject $quoteItemMock */
150+
$childQuoteItemMock = $this->getMockBuilder(AbstractItem::class)
151+
->onlyMethods(['getProduct', 'getData', 'getPrice', 'getQty'])
152+
->addMethods(['getBaseRowTotal'])
153+
->disableOriginalConstructor()
154+
->getMockForAbstractClass();
155+
$childQuoteItemMock->expects($this->any())
156+
->method('getProduct')
157+
->willReturn($this->productMock);
158+
$childQuoteItemMock->expects($this->any())
159+
->method('getQty')
160+
->willReturn($productDetails['qty']);
161+
$childQuoteItemMock->expects($this->any())
162+
->method('getPrice')
163+
->willReturn($productDetails['price']);
164+
$childQuoteItemMock->expects($this->any())
165+
->method('getBaseRowTotal')
166+
->willReturn($productDetails['baseRowTotal']);
167+
$this->productMock->expects($this->any())
168+
->method('getResource')
169+
->willReturn($attributeResource);
170+
$this->quoteItemMock->expects($this->any())
171+
->method('getProduct')
172+
->willReturn($this->productMock);
173+
$this->quoteItemMock->expects($this->any())
174+
->method('getHasChildren')
175+
->willReturn($productDetails['hasChildren']);
176+
$this->quoteItemMock->expects($this->any())
177+
->method('getChildren')
178+
->willReturn([$childQuoteItemMock]);
179+
$this->quoteItemMock->expects($this->any())
180+
->method('getProductId')
181+
->willReturn($productDetails['id']);
182+
$this->quoteItemMock->expects($this->any())
183+
->method('getChildren')
184+
->willReturn([$childQuoteItemMock]);
185+
$this->quoteItemMock->expects($this->any())
186+
->method('getData')
187+
->willReturn($productDetails['baseRowTotal']);
188+
$this->assertEquals($expectedResult, $this->model->validate($this->abstractModel));
189+
}
190+
191+
/**
192+
* Get data provider array for validate bundle product
193+
*
194+
* @return array
195+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
196+
*/
197+
public function dataProviderForFixedBundleProduct(): array
198+
{
199+
return [
200+
'validate true for bundle product data with conditions' =>
201+
[
202+
[
203+
'id' => 'attribute_set_id',
204+
'name' => 'test conditions',
205+
'attributeScope' => 'frontend',
206+
'attributeOperator' => '=='
207+
],
208+
[
209+
'id'=> 1,
210+
'type' => ProductType::TYPE_BUNDLE,
211+
'qty' => 1,
212+
'price' => 100,
213+
'hasChildren' => true,
214+
'baseRowTotal' => 100,
215+
'valueParsed' => 100
216+
],
217+
true
218+
],
219+
'validate false for bundle product data with conditions' =>
220+
[
221+
[
222+
'id' => 'attribute_set_id',
223+
'name' => 'test conditions',
224+
'attributeScope' => 'frontend',
225+
'attributeOperator' => '=='
226+
],
227+
[
228+
'id'=> 1,
229+
'type' => ProductType::TYPE_BUNDLE,
230+
'qty' => 1,
231+
'price' => 100,
232+
'hasChildren' => true ,
233+
'baseRowTotal' => 100,
234+
'valueParsed' => 50
235+
],
236+
false
237+
],
238+
'validate product data without conditions with bundle product' =>
239+
[
240+
null,
241+
[
242+
'id'=> 1,
243+
'type' => ProductType::TYPE_BUNDLE,
244+
'qty' => 1,
245+
'price' => 100,
246+
'hasChildren' => true ,
247+
'baseRowTotal' => 100,
248+
'valueParsed' => 100
249+
],
250+
false
251+
],
252+
'validate true for simple product data with conditions' =>
253+
[
254+
[
255+
'id' => 'attribute_set_id',
256+
'name' => 'test conditions',
257+
'attributeScope' => 'frontend',
258+
'attributeOperator' => '=='
259+
],
260+
[
261+
'id'=> 1,
262+
'type' => ProductType::TYPE_SIMPLE,
263+
'qty' => 1,
264+
'price' => 100,
265+
'hasChildren' => false ,
266+
'baseRowTotal' => 100,
267+
'valueParsed' => 100
268+
],
269+
true
270+
],
271+
'validate false for simple product data with conditions' =>
272+
[
273+
[
274+
'id' => 'attribute_set_id',
275+
'name' => 'test conditions',
276+
'attributeScope' => 'frontend',
277+
'attributeOperator' => '=='
278+
],
279+
[
280+
'id'=> 1,
281+
'type' => ProductType::TYPE_SIMPLE,
282+
'qty' => 1,
283+
'price' => 100,
284+
'hasChildren' => false,
285+
'baseRowTotal' => 100,
286+
'valueParsed' => 50
287+
],
288+
false
289+
]
290+
];
291+
}
292+
}

0 commit comments

Comments
 (0)