Skip to content

Commit 27a7c81

Browse files
committed
Merge remote-tracking branch 'origin/AC-10472-V1' into spartans_pr_27102025
2 parents 2d6cd7d + 0996062 commit 27a7c81

File tree

4 files changed

+256
-0
lines changed

4 files changed

+256
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Plugin;
9+
10+
use Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule;
11+
use Magento\Quote\Model\Quote;
12+
use Magento\Framework\Validation\ValidationResult;
13+
use Magento\Framework\Validation\ValidationResultFactory;
14+
15+
class ShippingMethodValidationRulePlugin
16+
{
17+
/**
18+
* @var ValidationResultFactory
19+
*/
20+
private $validationResultFactory;
21+
22+
/**
23+
* @param ValidationResultFactory $validationResultFactory
24+
*/
25+
public function __construct(ValidationResultFactory $validationResultFactory)
26+
{
27+
$this->validationResultFactory = $validationResultFactory;
28+
}
29+
30+
/**
31+
* After plugin for validate method to ensure shipping method validity.
32+
*
33+
* @param ShippingMethodValidationRule $subject
34+
* @param ValidationResult[] $result
35+
* @param Quote $quote
36+
* @return ValidationResult[]
37+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
38+
*/
39+
public function afterValidate(
40+
ShippingMethodValidationRule $subject,
41+
array $result,
42+
Quote $quote
43+
): array {
44+
$shippingAddress = $quote->getShippingAddress();
45+
if (!$shippingAddress || $quote->isVirtual()) {
46+
return $result;
47+
}
48+
49+
$shippingMethod = $shippingAddress->getShippingMethod();
50+
$shippingRate = $shippingMethod ? $shippingAddress->getShippingRateByCode($shippingMethod) : null;
51+
$validationResult = $shippingMethod && $shippingRate && $shippingAddress->requestShippingRates();
52+
53+
if ($validationResult) {
54+
return $result;
55+
}
56+
57+
$existing = $result[0] ?? null;
58+
if ($existing instanceof ValidationResult && $existing->isValid()) {
59+
$result[0] = $this->validationResultFactory->create([
60+
'errors' => [__('The shipping method is missing. Select the shipping method and try again')]
61+
]);
62+
}
63+
64+
return $result;
65+
}
66+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Test\Unit\Plugin;
9+
10+
use Magento\Framework\Validation\ValidationResult;
11+
use Magento\Framework\Validation\ValidationResultFactory;
12+
use Magento\Quote\Model\Quote;
13+
use Magento\Quote\Model\Quote\Address;
14+
use Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule;
15+
use Magento\QuoteGraphQl\Plugin\ShippingMethodValidationRulePlugin;
16+
use PHPUnit\Framework\MockObject\MockObject;
17+
use PHPUnit\Framework\TestCase;
18+
19+
class ShippingMethodValidationRulePluginTest extends TestCase
20+
{
21+
/** @var ValidationResultFactory|MockObject */
22+
private $validationResultFactory;
23+
24+
/** @var ShippingMethodValidationRulePlugin */
25+
private $plugin;
26+
27+
protected function setUp(): void
28+
{
29+
$this->validationResultFactory = $this->createMock(ValidationResultFactory::class);
30+
$this->plugin = new ShippingMethodValidationRulePlugin($this->validationResultFactory);
31+
}
32+
33+
public function testReturnsOriginalResultWhenNoShippingAddress(): void
34+
{
35+
$subject = $this->createMock(ShippingMethodValidationRule::class);
36+
$quote = $this->createMock(Quote::class);
37+
$quote->method('getShippingAddress')->willReturn(null);
38+
$quote->method('isVirtual')->willReturn(false);
39+
40+
$existingValidation = $this->createMock(ValidationResult::class);
41+
$existingValidation->method('isValid')->willReturn(true);
42+
$result = [$existingValidation];
43+
44+
$this->validationResultFactory->expects($this->never())->method('create');
45+
46+
$actual = $this->plugin->afterValidate($subject, $result, $quote);
47+
48+
$this->assertSame($result[0], $actual[0]);
49+
}
50+
51+
public function testReturnsOriginalResultWhenQuoteIsVirtual(): void
52+
{
53+
$subject = $this->createMock(ShippingMethodValidationRule::class);
54+
$quote = $this->createMock(Quote::class);
55+
$address = $this->createMock(Address::class);
56+
57+
$quote->method('getShippingAddress')->willReturn($address);
58+
$quote->method('isVirtual')->willReturn(true);
59+
60+
$existingValidation = $this->createMock(ValidationResult::class);
61+
$existingValidation->method('isValid')->willReturn(true);
62+
$result = [$existingValidation];
63+
64+
$this->validationResultFactory->expects($this->never())->method('create');
65+
66+
$actual = $this->plugin->afterValidate($subject, $result, $quote);
67+
68+
$this->assertSame($result[0], $actual[0]);
69+
}
70+
71+
public function testReturnsOriginalResultWhenShippingIsValid(): void
72+
{
73+
$subject = $this->createMock(ShippingMethodValidationRule::class);
74+
$quote = $this->createMock(Quote::class);
75+
$address = $this->createMock(Address::class);
76+
77+
$quote->method('getShippingAddress')->willReturn($address);
78+
$quote->method('isVirtual')->willReturn(false);
79+
80+
$methodCode = 'flatrate_flatrate';
81+
$address->method('getShippingMethod')->willReturn($methodCode);
82+
$address->method('getShippingRateByCode')->with($methodCode)->willReturn(new \stdClass());
83+
$address->method('requestShippingRates')->willReturn(true);
84+
85+
$existingValidation = $this->createMock(ValidationResult::class);
86+
$existingValidation->method('isValid')->willReturn(true);
87+
$result = [$existingValidation];
88+
89+
$this->validationResultFactory->expects($this->never())->method('create');
90+
91+
$actual = $this->plugin->afterValidate($subject, $result, $quote);
92+
93+
$this->assertSame($result[0], $actual[0]);
94+
}
95+
96+
public function testReplacesResultWhenInvalidShippingAndExistingIsValid(): void
97+
{
98+
$subject = $this->createMock(ShippingMethodValidationRule::class);
99+
$quote = $this->createMock(Quote::class);
100+
$address = $this->createMock(Address::class);
101+
102+
$quote->method('getShippingAddress')->willReturn($address);
103+
$quote->method('isVirtual')->willReturn(false);
104+
105+
// Invalid shipping: no method set (could also be rate null or requestShippingRates false)
106+
$address->method('getShippingMethod')->willReturn(null);
107+
108+
$existingValid = $this->createMock(ValidationResult::class);
109+
$existingValid->method('isValid')->willReturn(true);
110+
$result = [$existingValid];
111+
112+
$replacement = $this->createMock(ValidationResult::class);
113+
114+
$this->validationResultFactory
115+
->expects($this->once())
116+
->method('create')
117+
->with($this->callback(function ($params) {
118+
return is_array($params)
119+
&& array_key_exists('errors', $params)
120+
&& is_array($params['errors'])
121+
&& count($params['errors']) === 1;
122+
}))
123+
->willReturn($replacement);
124+
125+
$actual = $this->plugin->afterValidate($subject, $result, $quote);
126+
127+
$this->assertSame($replacement, $actual[0]);
128+
}
129+
130+
public function testDoesNotReplaceWhenInvalidShippingAndExistingIsInvalid(): void
131+
{
132+
$subject = $this->createMock(ShippingMethodValidationRule::class);
133+
$quote = $this->createMock(Quote::class);
134+
$address = $this->createMock(Address::class);
135+
136+
$quote->method('getShippingAddress')->willReturn($address);
137+
$quote->method('isVirtual')->willReturn(false);
138+
139+
$address->method('getShippingMethod')->willReturn(null);
140+
141+
$existingInvalid = $this->createMock(ValidationResult::class);
142+
$existingInvalid->method('isValid')->willReturn(false);
143+
$result = [$existingInvalid];
144+
145+
$this->validationResultFactory->expects($this->never())->method('create');
146+
147+
$actual = $this->plugin->afterValidate($subject, $result, $quote);
148+
149+
$this->assertSame($existingInvalid, $actual[0]);
150+
}
151+
}

app/code/Magento/QuoteGraphQl/etc/graphql/di.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,10 @@
117117
</argument>
118118
</arguments>
119119
</type>
120+
<type name="Magento\Quote\Model\ValidationRules\ShippingMethodValidationRule">
121+
<plugin name="shipping_method_validation_plugin"
122+
type="Magento\QuoteGraphQl\Plugin\ShippingMethodValidationRulePlugin"
123+
sortOrder="10"
124+
disabled="false"/>
125+
</type>
120126
</config>

dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/PlaceOrderTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,39 @@ private function getHeaderMap(string $username = 'customer@example.com', string
522522
return $headerMap;
523523
}
524524

525+
#[
526+
Config('carriers/flatrate/active', '1', 'store', 'default'),
527+
Config('payment/checkmo/active', '1', 'store', 'default'),
528+
DataFixture(ProductFixture::class, as: 'product'),
529+
DataFixture(Indexer::class, as: 'indexer'),
530+
DataFixture(Customer::class, ['email' => 'customer@example.com'], as: 'customer'),
531+
DataFixture(
532+
CustomerCart::class,
533+
[
534+
'customer_id' => '$customer.id$',
535+
'reserved_order_id' => 'test_quote'
536+
],
537+
'cart'
538+
),
539+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
540+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
541+
DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']),
542+
DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']),
543+
DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']),
544+
DataFixture(QuoteIdMask::class, ['cart_id' => '$cart.id$'], 'quoteIdMask'),
545+
Config('carriers/flatrate/active', '0', 'store', 'default'),
546+
]
547+
public function testPlaceOrderWithDisabledShippingMethod()
548+
{
549+
$maskedQuoteId = DataFixtureStorageManager::getStorage()->get('quoteIdMask')->getMaskedId();
550+
$query = $this->getQuery($maskedQuoteId);
551+
552+
self::expectExceptionMessage(
553+
'Unable to place order: The shipping method is missing. Select the shipping method and try again'
554+
);
555+
$this->graphQlMutation($query, [], '', $this->getHeaderMap());
556+
}
557+
525558
/**
526559
* @inheritdoc
527560
*/

0 commit comments

Comments
 (0)