Skip to content

Commit 8f535ac

Browse files
committed
Merge branch '2.4-develop' of github.com:magento-commerce/magento2ce into ACP2E-4132
2 parents 7aa6fcb + a3b1abc commit 8f535ac

File tree

21 files changed

+2048
-56
lines changed

21 files changed

+2048
-56
lines changed

app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/UrlTest.php

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,21 @@ protected function setUp(): void
7878
);
7979
}
8080

81-
public function testGet()
81+
public function testCollectWithNullButtons()
8282
{
8383
$product = $this->getMockBuilder(Product::class)
8484
->disableOriginalConstructor()
8585
->getMock();
8686
$productRenderInfoDto = $this->getMockForAbstractClass(ProductRenderInterface::class);
87+
88+
// Mock both getAddToCartButton and getAddToCompareButton returning null (line 81-82)
89+
$productRenderInfoDto->expects($this->once())
90+
->method('getAddToCartButton')
91+
->willReturn(null);
92+
$productRenderInfoDto->expects($this->once())
93+
->method('getAddToCompareButton')
94+
->willReturn(null);
95+
8796
$this->catalogProductHelperMock
8897
->expects($this->once())
8998
->method('getPostDataParams')
@@ -92,6 +101,15 @@ public function testGet()
92101
$product->expects($this->once())
93102
->method('getId')
94103
->willReturn(1);
104+
$product->expects($this->once())
105+
->method('getData')
106+
->with('has_options')
107+
->willReturn(true);
108+
$product->expects($this->once())
109+
->method('getProductUrl')
110+
->willReturn('http://example.com/product/1');
111+
112+
// Expect buttonFactory to be called twice (once for cart, once for compare) since both are null
95113
$this->buttonFactoryMock->expects($this->exactly(2))
96114
->method('create')
97115
->willReturn($this->buttonMock);
@@ -113,6 +131,174 @@ public function testGet()
113131
)
114132
->willReturn(['some cart url post data']);
115133

134+
// Verify buttons are set back to productRender
135+
$productRenderInfoDto->expects($this->once())
136+
->method('setAddToCartButton')
137+
->with($this->buttonMock);
138+
$productRenderInfoDto->expects($this->once())
139+
->method('setAddToCompareButton')
140+
->with($this->buttonMock);
141+
$productRenderInfoDto->expects($this->once())
142+
->method('setUrl')
143+
->with('http://example.com/product/1');
144+
145+
$this->model->collect($product, $productRenderInfoDto);
146+
}
147+
148+
public function testCollectWithExistingAddToCompareButton()
149+
{
150+
$product = $this->getMockBuilder(Product::class)
151+
->disableOriginalConstructor()
152+
->getMock();
153+
$productRenderInfoDto = $this->getMockForAbstractClass(ProductRenderInterface::class);
154+
155+
$existingCompareButton = $this->getMockForAbstractClass(ButtonInterface::class);
156+
157+
// Test line 82: getAddToCompareButton returns existing button
158+
$productRenderInfoDto->expects($this->once())
159+
->method('getAddToCartButton')
160+
->willReturn(null);
161+
$productRenderInfoDto->expects($this->once())
162+
->method('getAddToCompareButton')
163+
->willReturn($existingCompareButton);
164+
165+
$this->catalogProductHelperMock
166+
->expects($this->once())
167+
->method('getPostDataParams')
168+
->with($product)
169+
->willReturn(['Some compare Data']);
170+
$product->expects($this->once())
171+
->method('getId')
172+
->willReturn(1);
173+
$product->expects($this->once())
174+
->method('getData')
175+
->with('has_options')
176+
->willReturn(false);
177+
$product->expects($this->once())
178+
->method('getProductUrl')
179+
->willReturn('http://example.com/product/1');
180+
181+
// Expect buttonFactory to be called only once (for cart button) since compare button exists
182+
$this->buttonFactoryMock->expects($this->once())
183+
->method('create')
184+
->willReturn($this->buttonMock);
185+
$this->abstractProductMock->expects($this->exactly(2))
186+
->method('getAddToCartUrl')
187+
->with(
188+
$product,
189+
['useUencPlaceholder' => true]
190+
)
191+
->willReturn('some:url');
192+
$this->postHelperMock->expects($this->once())
193+
->method('getPostData')
194+
->with(
195+
'some:url',
196+
[
197+
'product' => 1,
198+
ActionInterface::PARAM_NAME_URL_ENCODED => "%uenc%"
199+
]
200+
)
201+
->willReturn(['some cart url post data']);
202+
203+
// Verify the existing compare button is used and configured
204+
$existingCompareButton->expects($this->once())
205+
->method('setUrl')
206+
->with(['Some compare Data']);
207+
208+
// Verify buttons are set back to productRender
209+
$productRenderInfoDto->expects($this->once())
210+
->method('setAddToCartButton')
211+
->with($this->buttonMock);
212+
$productRenderInfoDto->expects($this->once())
213+
->method('setAddToCompareButton')
214+
->with($existingCompareButton);
215+
$productRenderInfoDto->expects($this->once())
216+
->method('setUrl')
217+
->with('http://example.com/product/1');
218+
219+
$this->model->collect($product, $productRenderInfoDto);
220+
}
221+
222+
public function testCollectWithExistingButtons()
223+
{
224+
$product = $this->getMockBuilder(Product::class)
225+
->disableOriginalConstructor()
226+
->getMock();
227+
$productRenderInfoDto = $this->getMockForAbstractClass(ProductRenderInterface::class);
228+
229+
$existingCartButton = $this->getMockForAbstractClass(ButtonInterface::class);
230+
$existingCompareButton = $this->getMockForAbstractClass(ButtonInterface::class);
231+
232+
// Test both buttons already exist
233+
$productRenderInfoDto->expects($this->once())
234+
->method('getAddToCartButton')
235+
->willReturn($existingCartButton);
236+
$productRenderInfoDto->expects($this->once())
237+
->method('getAddToCompareButton')
238+
->willReturn($existingCompareButton);
239+
240+
$this->catalogProductHelperMock
241+
->expects($this->once())
242+
->method('getPostDataParams')
243+
->with($product)
244+
->willReturn(['Some compare Data']);
245+
$product->expects($this->once())
246+
->method('getId')
247+
->willReturn(1);
248+
$product->expects($this->once())
249+
->method('getData')
250+
->with('has_options')
251+
->willReturn(true);
252+
$product->expects($this->once())
253+
->method('getProductUrl')
254+
->willReturn('http://example.com/product/1');
255+
256+
// Expect buttonFactory to NOT be called since both buttons exist
257+
$this->buttonFactoryMock->expects($this->never())
258+
->method('create');
259+
$this->abstractProductMock->expects($this->exactly(2))
260+
->method('getAddToCartUrl')
261+
->with(
262+
$product,
263+
['useUencPlaceholder' => true]
264+
)
265+
->willReturn('some:url');
266+
$this->postHelperMock->expects($this->once())
267+
->method('getPostData')
268+
->with(
269+
'some:url',
270+
[
271+
'product' => 1,
272+
ActionInterface::PARAM_NAME_URL_ENCODED => "%uenc%"
273+
]
274+
)
275+
->willReturn(['some cart url post data']);
276+
277+
// Verify both existing buttons are used and configured
278+
$existingCartButton->expects($this->once())
279+
->method('setPostData')
280+
->with(['some cart url post data']);
281+
$existingCartButton->expects($this->once())
282+
->method('setRequiredOptions')
283+
->with(true);
284+
$existingCartButton->expects($this->once())
285+
->method('setUrl')
286+
->with('some:url');
287+
$existingCompareButton->expects($this->once())
288+
->method('setUrl')
289+
->with(['Some compare Data']);
290+
291+
// Verify buttons are set back to productRender
292+
$productRenderInfoDto->expects($this->once())
293+
->method('setAddToCartButton')
294+
->with($existingCartButton);
295+
$productRenderInfoDto->expects($this->once())
296+
->method('setAddToCompareButton')
297+
->with($existingCompareButton);
298+
$productRenderInfoDto->expects($this->once())
299+
->method('setUrl')
300+
->with('http://example.com/product/1');
301+
116302
$this->model->collect($product, $productRenderInfoDto);
117303
}
118304
}

app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Url.php

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,6 @@
2020
*/
2121
class Url implements ProductRenderCollectorInterface
2222
{
23-
/** Compare Data key */
24-
const KEY_COMPARE_URL_POST_DATA = "compare_url_post_data";
25-
26-
/** Add to cart url key post data */
27-
const KEY_ADD_TO_CART_URL_POST_DATA = "add_to_cart_url_post_data";
28-
29-
/** Add to cart url key */
30-
const KEY_ADD_TO_CART_URL = "add_to_cart_url";
31-
32-
/** Product Url */
33-
const KEY_URL = "url";
34-
35-
/** Has Required options key */
36-
const KEY_HAS_REQUIRED_OPTIONS = "has_required_options";
37-
3823
/**
3924
* @var AbstractProduct
4025
*/
@@ -79,7 +64,7 @@ public function __construct(
7964
public function collect(ProductInterface $product, ProductRenderInterface $productRender)
8065
{
8166
$addToCart = $productRender->getAddToCartButton();
82-
$addToCompare = $productRender->getAddToCartButton();
67+
$addToCompare = $productRender->getAddToCompareButton();
8368

8469
if (!$addToCart) {
8570
$addToCart = $this->buttonFactory->create();

app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurableRegularPrice.php

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2015 Adobe
4+
* All Rights Reserved.
55
*/
66

77
namespace Magento\ConfigurableProduct\Pricing\Price;
@@ -60,6 +60,11 @@ class ConfigurableRegularPrice extends AbstractPrice implements
6060
*/
6161
private $configurableMaxPriceCalculator;
6262

63+
/**
64+
* @var array<int, bool>
65+
*/
66+
private $equalFinalPriceCache = [];
67+
6368
/**
6469
* @param \Magento\Framework\Pricing\SaleableInterface $saleableItem
6570
* @param float $quantity
@@ -193,29 +198,71 @@ private function getConfigurableOptionsProvider()
193198
public function _resetState(): void
194199
{
195200
$this->values = [];
201+
$this->equalFinalPriceCache = [];
196202
}
197203

198204
/**
199205
* Check whether Configurable Product have more than one children products
200206
*
201207
* @param SaleableInterface $product
202208
* @return bool
209+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
210+
* @SuppressWarnings(PHPMD.NPathComplexity)
203211
*/
204212
public function isChildProductsOfEqualPrices(SaleableInterface $product): bool
205213
{
206-
$minPrice = $this->getMinRegularAmount()->getValue();
207-
$final_price = $product->getFinalPrice();
208-
$productId = $product->getId();
209-
if ($final_price < $minPrice) {
210-
return false;
214+
$storeId = (int) ($product->getStoreId() ?: 0);
215+
$cacheKey = (int) $product->getId() . ':' . $storeId;
216+
if (isset($this->equalFinalPriceCache[$cacheKey])) {
217+
return $this->equalFinalPriceCache[$cacheKey];
218+
}
219+
220+
$memoKey = '_children_final_prices_equal_store_' . $storeId;
221+
$memoized = $product->getData($memoKey);
222+
if ($memoized !== null) {
223+
return (bool) $memoized;
224+
}
225+
226+
// Listing fast-path: if index fields are present, rely on them and avoid any child loading
227+
$minIndexed = $product->getData('minimal_price');
228+
$maxIndexed = $product->getData('max_price');
229+
if (is_numeric($minIndexed) && is_numeric($maxIndexed)) {
230+
$result = ((float)$minIndexed === (float)$maxIndexed);
231+
$this->equalFinalPriceCache[$cacheKey] = $result;
232+
$product->setData($memoKey, $result);
233+
return $result;
211234
}
212-
$attributes = $product->getTypeInstance()->getConfigurableAttributes($product);
213-
$items = $attributes->getItems();
214-
$options = reset($items);
215-
$maxPrice = $this->configurableMaxPriceCalculator->getMaxPriceForConfigurableProduct($productId);
216-
if ($maxPrice == 0) {
217-
$maxPrice = $this->getMaxRegularAmount()->getValue();
235+
236+
$children = $product->getTypeInstance()->getUsedProducts($product);
237+
$firstFinal = null;
238+
$saleableChildrenCount = 0;
239+
$allEqual = true;
240+
foreach ($children as $child) {
241+
if (!$child->isSalable()) {
242+
continue;
243+
}
244+
$saleableChildrenCount++;
245+
$value = $child->getPriceInfo()->getPrice('final_price')->getAmount()->getValue();
246+
if ($firstFinal === null) {
247+
$firstFinal = $value;
248+
continue;
249+
}
250+
if ($value != $firstFinal) {
251+
$allEqual = false;
252+
break;
253+
}
218254
}
219-
return (count($options->getOptions()) > 1) && $minPrice == $maxPrice;
255+
256+
if ($saleableChildrenCount < 1 || $firstFinal === null || !$allEqual) {
257+
$product->setData($memoKey, false);
258+
$this->equalFinalPriceCache[$cacheKey] = false;
259+
return false;
260+
}
261+
262+
// Guard against parent-level extra discounts (compute only when children are equal)
263+
$result = !($product->getFinalPrice() < $firstFinal);
264+
$this->equalFinalPriceCache[$cacheKey] = $result;
265+
$product->setData($memoKey, $result);
266+
return $result;
220267
}
221268
}

0 commit comments

Comments
 (0)