Skip to content

Commit 293228d

Browse files
committed
Merge remote-tracking branch 'origin/AC-13594' into spartans_pr_29102025
2 parents de5be1a + 1cd0957 commit 293228d

File tree

4 files changed

+418
-0
lines changed

4 files changed

+418
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
7+
namespace Magento\Catalog\Plugin\Model;
8+
9+
use Magento\Catalog\Model\Config as Subject;
10+
use Magento\Catalog\Model\Product;
11+
use Magento\Eav\Model\Config as EavConfig;
12+
13+
/**
14+
* Plugin to ensure special_price attribute is always loaded in product listings
15+
* even when used_in_product_listing = 0
16+
*/
17+
class ConfigPlugin
18+
{
19+
private const SPECIAL_PRICE_ATTR_CODE = 'special_price';
20+
21+
/**
22+
* @param EavConfig $eavConfig
23+
*/
24+
public function __construct(
25+
private readonly EavConfig $eavConfig
26+
) {
27+
}
28+
29+
/**
30+
* Add special_price attribute to the list of attributes used in product listing
31+
* regardless of its used_in_product_listing setting
32+
*
33+
* @param Subject $subject
34+
* @param array $result
35+
* @return array
36+
*/
37+
public function afterGetAttributesUsedInProductListing(Subject $subject, array $result): array
38+
{
39+
// Check if special_price is already in the result
40+
if (!isset($result[self::SPECIAL_PRICE_ATTR_CODE])) {
41+
// Get the special_price attribute
42+
$specialPriceAttribute = $this->eavConfig->getAttribute(
43+
Product::ENTITY,
44+
self::SPECIAL_PRICE_ATTR_CODE
45+
);
46+
47+
if ($specialPriceAttribute && $specialPriceAttribute->getId()) {
48+
// Add it to the result
49+
$result[self::SPECIAL_PRICE_ATTR_CODE] = $specialPriceAttribute;
50+
}
51+
}
52+
return $result;
53+
}
54+
}
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Plugin\Model;
9+
10+
use Magento\Catalog\Model\Config as Subject;
11+
use Magento\Catalog\Model\Product;
12+
use Magento\Catalog\Plugin\Model\ConfigPlugin;
13+
use Magento\Eav\Model\Config as EavConfig;
14+
use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
15+
use PHPUnit\Framework\MockObject\MockObject;
16+
use PHPUnit\Framework\TestCase;
17+
18+
/**
19+
* Unit test for ConfigPlugin class
20+
*/
21+
class ConfigPluginTest extends TestCase
22+
{
23+
/**
24+
* @var ConfigPlugin
25+
*/
26+
private $plugin;
27+
28+
/**
29+
* @var EavConfig|MockObject
30+
*/
31+
private $eavConfigMock;
32+
33+
/**
34+
* @var Subject|MockObject
35+
*/
36+
private $subjectMock;
37+
38+
/**
39+
* @var AbstractAttribute|MockObject
40+
*/
41+
private $specialPriceAttributeMock;
42+
43+
protected function setUp(): void
44+
{
45+
$this->eavConfigMock = $this->createMock(EavConfig::class);
46+
$this->subjectMock = $this->createMock(Subject::class);
47+
$this->specialPriceAttributeMock = $this->createMock(AbstractAttribute::class);
48+
49+
$this->plugin = new ConfigPlugin($this->eavConfigMock);
50+
}
51+
52+
/**
53+
* Test that special_price attribute is added when not present in result
54+
*/
55+
public function testAfterGetAttributesUsedInProductListingAddsSpecialPriceWhenNotPresent(): void
56+
{
57+
$existingAttributes = [
58+
'name' => $this->createMock(AbstractAttribute::class),
59+
'price' => $this->createMock(AbstractAttribute::class)
60+
];
61+
62+
$this->specialPriceAttributeMock->expects($this->once())
63+
->method('getId')
64+
->willReturn(123);
65+
66+
$this->eavConfigMock->expects($this->once())
67+
->method('getAttribute')
68+
->with(Product::ENTITY, 'special_price')
69+
->willReturn($this->specialPriceAttributeMock);
70+
71+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
72+
$this->subjectMock,
73+
$existingAttributes
74+
);
75+
76+
$this->assertArrayHasKey('special_price', $result);
77+
$this->assertSame($this->specialPriceAttributeMock, $result['special_price']);
78+
$this->assertArrayHasKey('name', $result);
79+
$this->assertArrayHasKey('price', $result);
80+
$this->assertCount(3, $result);
81+
}
82+
83+
/**
84+
* Test that special_price attribute is not added when already present in result
85+
*/
86+
public function testAfterGetAttributesUsedInProductListingDoesNotAddSpecialPriceWhenAlreadyPresent(): void
87+
{
88+
$existingAttributes = [
89+
'name' => $this->createMock(AbstractAttribute::class),
90+
'price' => $this->createMock(AbstractAttribute::class),
91+
'special_price' => $this->createMock(AbstractAttribute::class)
92+
];
93+
94+
$this->eavConfigMock->expects($this->never())
95+
->method('getAttribute');
96+
97+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
98+
$this->subjectMock,
99+
$existingAttributes
100+
);
101+
102+
$this->assertSame($existingAttributes, $result);
103+
$this->assertCount(3, $result);
104+
}
105+
106+
/**
107+
* Test that special_price attribute is not added when attribute is not found
108+
*/
109+
public function testAfterGetAttributesUsedInProductListingDoesNotAddSpecialPriceWhenAttributeNotFound(): void
110+
{
111+
$existingAttributes = [
112+
'name' => $this->createMock(AbstractAttribute::class),
113+
'price' => $this->createMock(AbstractAttribute::class)
114+
];
115+
116+
$this->eavConfigMock->expects($this->once())
117+
->method('getAttribute')
118+
->with(Product::ENTITY, 'special_price')
119+
->willReturn(null);
120+
121+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
122+
$this->subjectMock,
123+
$existingAttributes
124+
);
125+
126+
$this->assertArrayNotHasKey('special_price', $result);
127+
$this->assertSame($existingAttributes, $result);
128+
$this->assertCount(2, $result);
129+
}
130+
131+
/**
132+
* Test that special_price attribute is not added when attribute has no ID
133+
*/
134+
public function testAfterGetAttributesUsedInProductListingDoesNotAddSpecialPriceWhenAttributeHasNoId(): void
135+
{
136+
$existingAttributes = [
137+
'name' => $this->createMock(AbstractAttribute::class),
138+
'price' => $this->createMock(AbstractAttribute::class)
139+
];
140+
141+
$this->specialPriceAttributeMock->expects($this->once())
142+
->method('getId')
143+
->willReturn(null);
144+
145+
$this->eavConfigMock->expects($this->once())
146+
->method('getAttribute')
147+
->with(Product::ENTITY, 'special_price')
148+
->willReturn($this->specialPriceAttributeMock);
149+
150+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
151+
$this->subjectMock,
152+
$existingAttributes
153+
);
154+
155+
$this->assertArrayNotHasKey('special_price', $result);
156+
$this->assertSame($existingAttributes, $result);
157+
$this->assertCount(2, $result);
158+
}
159+
160+
/**
161+
* Test that special_price attribute is not added when attribute ID is empty string
162+
*/
163+
public function testAfterGetAttributesUsedInProductListingDoesNotAddSpecialPriceWhenAttributeIdIsEmptyString(): void
164+
{
165+
$existingAttributes = [
166+
'name' => $this->createMock(AbstractAttribute::class),
167+
'price' => $this->createMock(AbstractAttribute::class)
168+
];
169+
170+
$this->specialPriceAttributeMock->expects($this->once())
171+
->method('getId')
172+
->willReturn('');
173+
174+
$this->eavConfigMock->expects($this->once())
175+
->method('getAttribute')
176+
->with(Product::ENTITY, 'special_price')
177+
->willReturn($this->specialPriceAttributeMock);
178+
179+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
180+
$this->subjectMock,
181+
$existingAttributes
182+
);
183+
184+
$this->assertArrayNotHasKey('special_price', $result);
185+
$this->assertSame($existingAttributes, $result);
186+
$this->assertCount(2, $result);
187+
}
188+
189+
/**
190+
* Test that special_price attribute is not added when attribute ID is zero
191+
*/
192+
public function testAfterGetAttributesUsedInProductListingDoesNotAddSpecialPriceWhenAttributeIdIsZero(): void
193+
{
194+
$existingAttributes = [
195+
'name' => $this->createMock(AbstractAttribute::class),
196+
'price' => $this->createMock(AbstractAttribute::class)
197+
];
198+
199+
$this->specialPriceAttributeMock->expects($this->once())
200+
->method('getId')
201+
->willReturn(0);
202+
203+
$this->eavConfigMock->expects($this->once())
204+
->method('getAttribute')
205+
->with(Product::ENTITY, 'special_price')
206+
->willReturn($this->specialPriceAttributeMock);
207+
208+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
209+
$this->subjectMock,
210+
$existingAttributes
211+
);
212+
213+
$this->assertArrayNotHasKey('special_price', $result);
214+
$this->assertSame($existingAttributes, $result);
215+
$this->assertCount(2, $result);
216+
}
217+
218+
/**
219+
* Test with empty attributes array
220+
*/
221+
public function testAfterGetAttributesUsedInProductListingWithEmptyAttributesArray(): void
222+
{
223+
$existingAttributes = [];
224+
225+
$this->specialPriceAttributeMock->expects($this->once())
226+
->method('getId')
227+
->willReturn(123);
228+
229+
$this->eavConfigMock->expects($this->once())
230+
->method('getAttribute')
231+
->with(Product::ENTITY, 'special_price')
232+
->willReturn($this->specialPriceAttributeMock);
233+
234+
$result = $this->plugin->afterGetAttributesUsedInProductListing(
235+
$this->subjectMock,
236+
$existingAttributes
237+
);
238+
239+
$this->assertArrayHasKey('special_price', $result);
240+
$this->assertSame($this->specialPriceAttributeMock, $result['special_price']);
241+
$this->assertCount(1, $result);
242+
}
243+
244+
/**
245+
* Test that the constant SPECIAL_PRICE_ATTR_CODE is used correctly
246+
*/
247+
public function testSpecialPriceAttributeCodeConstant(): void
248+
{
249+
$reflection = new \ReflectionClass(ConfigPlugin::class);
250+
$constant = $reflection->getConstant('SPECIAL_PRICE_ATTR_CODE');
251+
252+
$this->assertEquals('special_price', $constant);
253+
}
254+
255+
/**
256+
* Test that the plugin uses the correct entity type
257+
*/
258+
public function testUsesCorrectEntityType(): void
259+
{
260+
$existingAttributes = [];
261+
262+
$this->specialPriceAttributeMock->expects($this->once())
263+
->method('getId')
264+
->willReturn(123);
265+
266+
$this->eavConfigMock->expects($this->once())
267+
->method('getAttribute')
268+
->with(Product::ENTITY, 'special_price')
269+
->willReturn($this->specialPriceAttributeMock);
270+
271+
$this->plugin->afterGetAttributesUsedInProductListing(
272+
$this->subjectMock,
273+
$existingAttributes
274+
);
275+
276+
// The test passes if the method is called with the correct parameters
277+
$this->addToAssertionCount(1);
278+
}
279+
}

app/code/Magento/Catalog/etc/frontend/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,7 @@
124124
</argument>
125125
</arguments>
126126
</type>
127+
<type name="Magento\Catalog\Model\Config">
128+
<plugin name="special_price_listing_plugin" type="Magento\Catalog\Plugin\Model\ConfigPlugin" sortOrder="10" />
129+
</type>
127130
</config>

0 commit comments

Comments
 (0)