Skip to content

Commit 4eae359

Browse files
authored
Merge branch '2.4-develop' into spartans_pr_29082025
2 parents 95c4325 + 36d4d6f commit 4eae359

File tree

158 files changed

+10322
-324
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

158 files changed

+10322
-324
lines changed

app/code/Magento/Amqp/Setup/ConfigOptionsList.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
*/
66
namespace Magento\Amqp\Setup;
77

8+
use Magento\Framework\App\DeploymentConfig;
89
use Magento\Framework\Config\Data\ConfigData;
910
use Magento\Framework\Config\File\ConfigFilePool;
1011
use Magento\Framework\Setup\ConfigOptionsListInterface;
1112
use Magento\Framework\Setup\Option\TextConfigOption;
12-
use Magento\Framework\App\DeploymentConfig;
1313

1414
/**
1515
* Deployment configuration options needed for Setup application
@@ -26,6 +26,7 @@ class ConfigOptionsList implements ConfigOptionsListInterface
2626
public const INPUT_KEY_QUEUE_AMQP_VIRTUAL_HOST = 'amqp-virtualhost';
2727
public const INPUT_KEY_QUEUE_AMQP_SSL = 'amqp-ssl';
2828
public const INPUT_KEY_QUEUE_AMQP_SSL_OPTIONS = 'amqp-ssl-options';
29+
public const INPUT_KEY_QUEUE_DEFAULT_CONNECTION ='queue-default-connection';
2930

3031
/**
3132
* Path to the values in the deployment config
@@ -203,6 +204,11 @@ public function validate(array $options, DeploymentConfig $deploymentConfig)
203204
if (!$result) {
204205
$errors[] = "Could not connect to the Amqp Server.";
205206
}
207+
208+
if (isset($options[self::INPUT_KEY_QUEUE_DEFAULT_CONNECTION])
209+
&& $options[self::INPUT_KEY_QUEUE_DEFAULT_CONNECTION] !== 'amqp') {
210+
$errors = [];
211+
}
206212
}
207213

208214
return $errors;

app/code/Magento/AsyncConfig/etc/queue_publisher.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd">
9-
<publisher topic="async_config.saveConfig"/>
9+
<publisher topic="async_config.saveConfig" queue="saveConfig"/>
1010
</config>

app/code/Magento/AsynchronousOperations/Model/MassConsumer.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
namespace Magento\AsynchronousOperations\Model;
99

10+
use Magento\Framework\App\ObjectManager;
1011
use Magento\Framework\MessageQueue\CallbackInvokerInterface;
12+
use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfig;
1113
use Magento\Framework\MessageQueue\ConsumerConfigurationInterface;
1214
use Magento\Framework\MessageQueue\ConsumerInterface;
1315
use Magento\Framework\MessageQueue\EnvelopeInterface;
@@ -41,24 +43,32 @@ class MassConsumer implements ConsumerInterface
4143
*/
4244
private $registry;
4345

46+
/**
47+
* @var ConsumerConfig
48+
*/
49+
private $consumerConfig;
50+
4451
/**
4552
* Initialize dependencies.
4653
*
4754
* @param CallbackInvokerInterface $invoker
4855
* @param ConsumerConfigurationInterface $configuration
4956
* @param MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback
5057
* @param Registry $registry
58+
* @param ConsumerConfig|null $consumerConfig
5159
*/
5260
public function __construct(
5361
CallbackInvokerInterface $invoker,
5462
ConsumerConfigurationInterface $configuration,
5563
MassConsumerEnvelopeCallbackFactory $massConsumerEnvelopeCallback,
56-
Registry $registry
64+
Registry $registry,
65+
?ConsumerConfig $consumerConfig = null
5766
) {
5867
$this->invoker = $invoker;
5968
$this->configuration = $configuration;
6069
$this->massConsumerEnvelopeCallback = $massConsumerEnvelopeCallback;
6170
$this->registry = $registry;
71+
$this->consumerConfig = $consumerConfig ?: ObjectManager::getInstance()->get(ConsumerConfig::class);
6272
}
6373

6474
/**

app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,22 @@ private function validateProductAttributes(array $attributesData): void
229229
$product = $this->productFactory->create();
230230
$product->setData($attributesData);
231231

232-
foreach (array_keys($attributesData) as $attributeCode) {
233-
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
234-
$attribute->getBackend()->validate($product);
232+
// Ensure Special Price From Date cannot exceed To Date during mass update
233+
if (array_key_exists('special_from_date', $attributesData)
234+
|| array_key_exists('special_to_date', $attributesData)) {
235+
$this->eavConfig
236+
->getAttribute(\Magento\Catalog\Model\Product::ENTITY, 'special_from_date')
237+
->setMaxValue($product->getSpecialToDate());
238+
}
239+
240+
try {
241+
foreach (array_keys($attributesData) as $attributeCode) {
242+
$attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode);
243+
$attribute->getBackend()->validate($product);
244+
}
245+
} catch (\Magento\Eav\Model\Entity\Attribute\Exception $e) {
246+
// Re-throw as LocalizedException so the specific validation message is displayed
247+
throw new LocalizedException(__($e->getMessage()));
235248
}
236249
}
237250

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Action\Attribute;
9+
10+
use Magento\Backend\App\Action\Context;
11+
use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save;
12+
use Magento\Catalog\Helper\Product\Edit\Action\Attribute as AttributeHelper;
13+
use Magento\Catalog\Model\ProductFactory;
14+
use Magento\Eav\Model\Config as EavConfig;
15+
use Magento\Framework\App\RequestInterface;
16+
use Magento\Framework\Bulk\BulkManagementInterface;
17+
use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
18+
use Magento\Framework\DataObject\IdentityGeneratorInterface;
19+
use Magento\Framework\Serialize\SerializerInterface;
20+
use Magento\Authorization\Model\UserContextInterface;
21+
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
22+
use Magento\Catalog\Model\Product\Filter\DateTime as DateTimeFilter;
23+
use Magento\Framework\Exception\LocalizedException;
24+
use Magento\Eav\Model\Entity\Attribute\Exception as EavAttributeException;
25+
use PHPUnit\Framework\TestCase;
26+
27+
/**
28+
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
29+
*/
30+
class SaveTest extends TestCase
31+
{
32+
/**
33+
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
34+
*/
35+
private function buildController(
36+
Context $context,
37+
AttributeHelper $attributeHelper,
38+
BulkManagementInterface $bulkManagement,
39+
OperationInterfaceFactory $operationFactory,
40+
IdentityGeneratorInterface $identityService,
41+
SerializerInterface $serializer,
42+
UserContextInterface $userContext,
43+
TimezoneInterface $timezone,
44+
EavConfig $eavConfig,
45+
ProductFactory $productFactory,
46+
DateTimeFilter $dateTimeFilter
47+
): Save {
48+
return new Save(
49+
$context,
50+
$attributeHelper,
51+
$bulkManagement,
52+
$operationFactory,
53+
$identityService,
54+
$serializer,
55+
$userContext,
56+
100,
57+
$timezone,
58+
$eavConfig,
59+
$productFactory,
60+
$dateTimeFilter
61+
);
62+
}
63+
64+
public function testValidateProductAttributesSetsMaxValueAndConvertsEavException(): void
65+
{
66+
$context = $this->createMock(Context::class);
67+
$attributeHelper = $this->createMock(AttributeHelper::class);
68+
$bulkManagement = $this->createMock(BulkManagementInterface::class);
69+
$operationFactory = $this->createMock(OperationInterfaceFactory::class);
70+
$identityService = $this->createMock(IdentityGeneratorInterface::class);
71+
$serializer = $this->createMock(SerializerInterface::class);
72+
$userContext = $this->createMock(UserContextInterface::class);
73+
$timezone = $this->createMock(TimezoneInterface::class);
74+
$eavConfig = $this->createMock(EavConfig::class);
75+
$productFactory = $this->createMock(ProductFactory::class);
76+
$dateTimeFilter = $this->createMock(DateTimeFilter::class);
77+
78+
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
79+
->disableOriginalConstructor()
80+
->onlyMethods(['setData', 'getSpecialToDate'])
81+
->getMock();
82+
$product->method('setData')->with([
83+
'special_from_date' => '2025-09-10 00:00:00',
84+
'special_to_date' => '2025-09-01 00:00:00',
85+
]);
86+
$product->method('getSpecialToDate')->willReturn('2025-09-01 00:00:00');
87+
88+
$productFactory->method('create')->willReturn($product);
89+
90+
// Attribute for special_from_date
91+
$fromAttrBackend = $this->getMockBuilder(\stdClass::class)
92+
->addMethods(['validate'])
93+
->getMock();
94+
$fromAttrBackend->method('validate')->willThrowException(
95+
new EavAttributeException(__('Make sure the To Date is later than or the same as the From Date.'))
96+
);
97+
98+
$fromAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
99+
->disableOriginalConstructor()
100+
->onlyMethods(['getBackend'])
101+
->addMethods(['setMaxValue'])
102+
->getMockForAbstractClass();
103+
$fromAttribute->expects($this->once())
104+
->method('setMaxValue')
105+
->with('2025-09-01 00:00:00');
106+
$fromAttribute->method('getBackend')->willReturn($fromAttrBackend);
107+
108+
// Attribute for special_to_date
109+
$toAttrBackend = $this->getMockBuilder(\stdClass::class)
110+
->addMethods(['validate'])
111+
->getMock();
112+
$toAttrBackend->method('validate')->willReturn(true);
113+
114+
$toAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
115+
->disableOriginalConstructor()
116+
->onlyMethods(['getBackend'])
117+
->getMockForAbstractClass();
118+
$toAttribute->method('getBackend')->willReturn($toAttrBackend);
119+
120+
// eavConfig should return attributes for 'special_from_date' and 'special_to_date'
121+
$eavConfig->method('getAttribute')
122+
->willReturnCallback(function ($entity, $code) use ($fromAttribute, $toAttribute) {
123+
unset($entity);
124+
return $code === 'special_from_date' ? $fromAttribute : $toAttribute;
125+
});
126+
127+
$controller = $this->buildController(
128+
$context,
129+
$attributeHelper,
130+
$bulkManagement,
131+
$operationFactory,
132+
$identityService,
133+
$serializer,
134+
$userContext,
135+
$timezone,
136+
$eavConfig,
137+
$productFactory,
138+
$dateTimeFilter
139+
);
140+
141+
$method = new \ReflectionMethod($controller, 'validateProductAttributes');
142+
$method->setAccessible(true);
143+
144+
$this->expectException(LocalizedException::class);
145+
$this->expectExceptionMessage('Make sure the To Date is later than or the same as the From Date.');
146+
147+
$method->invoke($controller, [
148+
'special_from_date' => '2025-09-10 00:00:00',
149+
'special_to_date' => '2025-09-01 00:00:00',
150+
]);
151+
}
152+
153+
public function testValidateProductAttributesPassesWhenDatesValid(): void
154+
{
155+
$context = $this->createMock(Context::class);
156+
$attributeHelper = $this->createMock(AttributeHelper::class);
157+
$bulkManagement = $this->createMock(BulkManagementInterface::class);
158+
$operationFactory = $this->createMock(OperationInterfaceFactory::class);
159+
$identityService = $this->createMock(IdentityGeneratorInterface::class);
160+
$serializer = $this->createMock(SerializerInterface::class);
161+
$userContext = $this->createMock(UserContextInterface::class);
162+
$timezone = $this->createMock(TimezoneInterface::class);
163+
$eavConfig = $this->createMock(EavConfig::class);
164+
$productFactory = $this->createMock(ProductFactory::class);
165+
$dateTimeFilter = $this->createMock(DateTimeFilter::class);
166+
167+
$product = $this->getMockBuilder(\Magento\Catalog\Model\Product::class)
168+
->disableOriginalConstructor()
169+
->onlyMethods(['setData', 'getSpecialToDate'])
170+
->getMock();
171+
$product->method('setData')->with([
172+
'special_from_date' => '2025-09-01 00:00:00',
173+
'special_to_date' => '2025-09-10 00:00:00',
174+
]);
175+
$product->method('getSpecialToDate')->willReturn('2025-09-10 00:00:00');
176+
$productFactory->method('create')->willReturn($product);
177+
178+
$okBackend = $this->getMockBuilder(\stdClass::class)
179+
->addMethods(['validate'])
180+
->getMock();
181+
$okBackend->method('validate')->willReturn(true);
182+
183+
$fromAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
184+
->disableOriginalConstructor()
185+
->onlyMethods(['getBackend'])
186+
->addMethods(['setMaxValue'])
187+
->getMockForAbstractClass();
188+
$fromAttribute->expects($this->once())->method('setMaxValue')->with('2025-09-10 00:00:00');
189+
$fromAttribute->method('getBackend')->willReturn($okBackend);
190+
191+
$toAttribute = $this->getMockBuilder(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute::class)
192+
->disableOriginalConstructor()
193+
->onlyMethods(['getBackend'])
194+
->getMockForAbstractClass();
195+
$toAttribute->method('getBackend')->willReturn($okBackend);
196+
197+
$eavConfig->method('getAttribute')
198+
->willReturnCallback(function ($entity, $code) use ($fromAttribute, $toAttribute) {
199+
unset($entity);
200+
return $code === 'special_from_date' ? $fromAttribute : $toAttribute;
201+
});
202+
203+
$controller = $this->buildController(
204+
$context,
205+
$attributeHelper,
206+
$bulkManagement,
207+
$operationFactory,
208+
$identityService,
209+
$serializer,
210+
$userContext,
211+
$timezone,
212+
$eavConfig,
213+
$productFactory,
214+
$dateTimeFilter
215+
);
216+
217+
$method = new \ReflectionMethod($controller, 'validateProductAttributes');
218+
$method->setAccessible(true);
219+
220+
// Should not throw
221+
$method->invoke($controller, [
222+
'special_from_date' => '2025-09-01 00:00:00',
223+
'special_to_date' => '2025-09-10 00:00:00',
224+
]);
225+
226+
$this->addToAssertionCount(1);
227+
}
228+
}

app/code/Magento/Catalog/etc/queue_publisher.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
-->
88
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/publisher.xsd">
9-
<publisher topic="product_action_attribute.update"/>
10-
<publisher topic="product_action_attribute.website.update"/>
11-
<publisher topic="catalog_website_attribute_value_sync"/>
9+
<publisher topic="product_action_attribute.update" queue="product_action_attribute.update"/>
10+
<publisher topic="product_action_attribute.website.update" queue="product_action_attribute.website.update"/>
11+
<publisher topic="catalog_website_attribute_value_sync" queue="catalog_website_attribute_value_sync"/>
1212
</config>

app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class SearchCriteriaBuilder
3838
* @param RequestDataBuilder $localData
3939
* @param SearchCriteriaResolverFactory $criteriaResolverFactory
4040
* @param ArgumentApplierPool $argumentApplierPool
41+
* @param array $partialSearchAnalyzers
4142
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
4243
*/
4344
public function __construct(
@@ -51,6 +52,7 @@ public function __construct(
5152
private readonly RequestDataBuilder $localData,
5253
private readonly SearchCriteriaResolverFactory $criteriaResolverFactory,
5354
private readonly ArgumentApplierPool $argumentApplierPool,
55+
private readonly array $partialSearchAnalyzers = []
5456
) {
5557
}
5658

@@ -117,6 +119,10 @@ private function updateMatchTypeRequestConfig(string $requestName, array $partia
117119
foreach ($query['match'] ?? [] as $index => $matchItem) {
118120
if (in_array($matchItem['field'] ?? null, $partialMatchFilters, true)) {
119121
$data['queries'][$queryName]['match'][$index]['matchCondition'] = 'match_phrase_prefix';
122+
if (array_key_exists($matchItem['field'], $this->partialSearchAnalyzers)) {
123+
$data['queries'][$queryName]['match'][$index]['analyzer']
124+
= $this->partialSearchAnalyzers[$matchItem['field']];
125+
}
120126
}
121127
}
122128
}

app/code/Magento/CatalogGraphQl/Model/Resolver/Product/Price/Discount.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ private function getPriceDifferenceAsValue(float $regularPrice, float $finalPric
7272
*/
7373
private function getPriceDifferenceAsPercent(float $regularPrice, float $finalPrice): float
7474
{
75-
$difference = $this->getPriceDifferenceAsValue($regularPrice, $finalPrice);
75+
$difference = $regularPrice - $finalPrice;
76+
if ($difference <= $this->zeroThreshold) {
77+
return 0;
78+
}
7679

7780
if ($difference <= $this->zeroThreshold || $regularPrice <= $this->zeroThreshold) {
7881
return 0;

0 commit comments

Comments
 (0)