Skip to content

Commit 039a867

Browse files
committed
Merge remote-tracking branch 'origin/AC-9605' into spartans_pr_27102025
2 parents 17a9dd3 + 26b8113 commit 039a867

File tree

4 files changed

+363
-0
lines changed

4 files changed

+363
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\QuoteGraphQl\Plugin\Quote;
9+
10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Payment\Helper\Data as PaymentHelper;
12+
use Magento\Quote\Api\CartRepositoryInterface;
13+
use Magento\Quote\Api\Data\PaymentInterface;
14+
use Magento\Quote\Model\QuoteManagement;
15+
16+
class ValidatePaymentOnPlaceOrder
17+
{
18+
/**
19+
* @param CartRepositoryInterface $cartRepository
20+
* @param PaymentHelper $paymentHelper
21+
*/
22+
public function __construct(
23+
private CartRepositoryInterface $cartRepository,
24+
private PaymentHelper $paymentHelper
25+
) {
26+
}
27+
28+
/**
29+
* Validate payment method
30+
*
31+
* @param QuoteManagement $subject
32+
* @param int $cartId
33+
* @param PaymentInterface|null $paymentMethod
34+
* @return array
35+
* @throws LocalizedException
36+
* @throws \Magento\Framework\Exception\NoSuchEntityException
37+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
38+
*/
39+
public function beforePlaceOrder(
40+
QuoteManagement $subject,
41+
$cartId,
42+
?PaymentInterface $paymentMethod = null
43+
): array {
44+
$quote = $this->cartRepository->getActive((int)$cartId);
45+
46+
$payment = $quote->getPayment();
47+
$code = $paymentMethod?->getMethod() ?: $payment->getMethod();
48+
49+
if (!$code) {
50+
return [$cartId, $paymentMethod];
51+
}
52+
53+
$methodInstance = $this->paymentHelper->getMethodInstance($code);
54+
$methodInstance->setInfoInstance($payment);
55+
56+
if (!$methodInstance->isAvailable($quote)) {
57+
throw new LocalizedException(__('The requested Payment Method is not available.'));
58+
}
59+
60+
return [$cartId, $paymentMethod];
61+
}
62+
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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\Quote;
9+
10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Payment\Helper\Data as PaymentHelper;
12+
use Magento\Payment\Model\MethodInterface;
13+
use Magento\Quote\Api\CartRepositoryInterface;
14+
use Magento\Quote\Api\Data\PaymentInterface;
15+
use Magento\Quote\Model\QuoteManagement;
16+
use Magento\QuoteGraphQl\Plugin\Quote\ValidatePaymentOnPlaceOrder;
17+
use PHPUnit\Framework\MockObject\MockObject;
18+
use PHPUnit\Framework\TestCase;
19+
20+
class ValidatePaymentOnPlaceOrderTest extends TestCase
21+
{
22+
/** @var CartRepositoryInterface&MockObject */
23+
private CartRepositoryInterface $cartRepository;
24+
25+
/** @var PaymentHelper&MockObject */
26+
private PaymentHelper $paymentHelper;
27+
28+
/**
29+
* @var ValidatePaymentOnPlaceOrder
30+
*/
31+
private ValidatePaymentOnPlaceOrder $plugin;
32+
33+
protected function setUp(): void
34+
{
35+
$this->cartRepository = $this->createMock(CartRepositoryInterface::class);
36+
$this->paymentHelper = $this->createMock(PaymentHelper::class);
37+
38+
$this->plugin = new ValidatePaymentOnPlaceOrder(
39+
$this->cartRepository,
40+
$this->paymentHelper
41+
);
42+
}
43+
44+
public function testReturnsArgsWhenNoPaymentCode(): void
45+
{
46+
$subject = $this->createMock(QuoteManagement::class);
47+
48+
$quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
49+
->disableOriginalConstructor()
50+
->onlyMethods(['getPayment'])
51+
->getMock();
52+
53+
$payment = $this->getMockBuilder(\Magento\Quote\Model\Quote\Payment::class)
54+
->disableOriginalConstructor()
55+
->onlyMethods(['getMethod'])
56+
->getMock();
57+
58+
$this->cartRepository
59+
->expects($this->once())
60+
->method('getActive')
61+
->with($this->identicalTo(123))
62+
->willReturn($quote);
63+
64+
$quote
65+
->expects($this->once())
66+
->method('getPayment')
67+
->willReturn($payment);
68+
69+
$payment
70+
->expects($this->once())
71+
->method('getMethod')
72+
->willReturn(null);
73+
74+
$this->paymentHelper
75+
->expects($this->never())
76+
->method('getMethodInstance');
77+
78+
$result = $this->plugin->beforePlaceOrder($subject, '123', null);
79+
80+
$this->assertSame(['123', null], $result);
81+
}
82+
83+
public function testUsesProvidedPaymentMethodCodeAndIsAvailable(): void
84+
{
85+
$subject = $this->createMock(QuoteManagement::class);
86+
87+
$quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
88+
->disableOriginalConstructor()
89+
->onlyMethods(['getPayment'])
90+
->getMock();
91+
92+
$payment = $this->getMockBuilder(\Magento\Quote\Model\Quote\Payment::class)
93+
->disableOriginalConstructor()
94+
->onlyMethods(['getMethod'])
95+
->getMock();
96+
97+
$paymentMethodParam = $this->createMock(PaymentInterface::class);
98+
$methodInstance = $this->createMock(MethodInterface::class);
99+
100+
$this->cartRepository
101+
->expects($this->once())
102+
->method('getActive')
103+
->with($this->identicalTo(10))
104+
->willReturn($quote);
105+
106+
$quote
107+
->expects($this->once())
108+
->method('getPayment')
109+
->willReturn($payment);
110+
111+
$paymentMethodParam
112+
->expects($this->once())
113+
->method('getMethod')
114+
->willReturn('checkmo');
115+
116+
$this->paymentHelper
117+
->expects($this->once())
118+
->method('getMethodInstance')
119+
->with('checkmo')
120+
->willReturn($methodInstance);
121+
122+
$methodInstance
123+
->expects($this->once())
124+
->method('setInfoInstance')
125+
->with($payment);
126+
127+
$methodInstance
128+
->expects($this->once())
129+
->method('isAvailable')
130+
->with($quote)
131+
->willReturn(true);
132+
133+
$result = $this->plugin->beforePlaceOrder($subject, '10', $paymentMethodParam);
134+
135+
$this->assertSame(['10', $paymentMethodParam], $result);
136+
}
137+
138+
public function testFallsBackToQuotePaymentCodeWhenNoParamProvided(): void
139+
{
140+
$subject = $this->createMock(QuoteManagement::class);
141+
142+
$quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
143+
->disableOriginalConstructor()
144+
->onlyMethods(['getPayment'])
145+
->getMock();
146+
147+
$payment = $this->getMockBuilder(\Magento\Quote\Model\Quote\Payment::class)
148+
->disableOriginalConstructor()
149+
->onlyMethods(['getMethod'])
150+
->getMock();
151+
152+
$methodInstance = $this->createMock(MethodInterface::class);
153+
154+
$this->cartRepository
155+
->expects($this->once())
156+
->method('getActive')
157+
->with($this->identicalTo(77))
158+
->willReturn($quote);
159+
160+
$quote
161+
->expects($this->once())
162+
->method('getPayment')
163+
->willReturn($payment);
164+
165+
$payment
166+
->expects($this->once())
167+
->method('getMethod')
168+
->willReturn('banktransfer');
169+
170+
$this->paymentHelper
171+
->expects($this->once())
172+
->method('getMethodInstance')
173+
->with('banktransfer')
174+
->willReturn($methodInstance);
175+
176+
$methodInstance
177+
->expects($this->once())
178+
->method('setInfoInstance')
179+
->with($payment);
180+
181+
$methodInstance
182+
->expects($this->once())
183+
->method('isAvailable')
184+
->with($quote)
185+
->willReturn(true);
186+
187+
$result = $this->plugin->beforePlaceOrder($subject, '77', null);
188+
189+
$this->assertSame(['77', null], $result);
190+
}
191+
192+
public function testThrowsWhenMethodNotAvailable(): void
193+
{
194+
$subject = $this->createMock(QuoteManagement::class);
195+
196+
$quote = $this->getMockBuilder(\Magento\Quote\Model\Quote::class)
197+
->disableOriginalConstructor()
198+
->onlyMethods(['getPayment'])
199+
->getMock();
200+
201+
$payment = $this->getMockBuilder(\Magento\Quote\Model\Quote\Payment::class)
202+
->disableOriginalConstructor()
203+
->onlyMethods(['getMethod'])
204+
->getMock();
205+
206+
$paymentMethodParam = $this->createMock(PaymentInterface::class);
207+
$methodInstance = $this->createMock(MethodInterface::class);
208+
209+
$this->cartRepository
210+
->expects($this->once())
211+
->method('getActive')
212+
->with($this->identicalTo(5))
213+
->willReturn($quote);
214+
215+
$quote
216+
->expects($this->once())
217+
->method('getPayment')
218+
->willReturn($payment);
219+
220+
$paymentMethodParam
221+
->expects($this->once())
222+
->method('getMethod')
223+
->willReturn('cc');
224+
225+
$this->paymentHelper
226+
->expects($this->once())
227+
->method('getMethodInstance')
228+
->with('cc')
229+
->willReturn($methodInstance);
230+
231+
$methodInstance
232+
->expects($this->once())
233+
->method('setInfoInstance')
234+
->with($payment);
235+
236+
$methodInstance
237+
->expects($this->once())
238+
->method('isAvailable')
239+
->with($quote)
240+
->willReturn(false);
241+
242+
$this->expectException(LocalizedException::class);
243+
$this->expectExceptionMessage('The requested Payment Method is not available.');
244+
245+
$this->plugin->beforePlaceOrder($subject, '5', $paymentMethodParam);
246+
}
247+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
<type name="Magento\Quote\Model\QuoteManagement">
8181
<plugin name="merge_guest_orders_with_customer_after_place"
8282
type="Magento\QuoteGraphQl\Plugin\Model\MergeGuestOrder" />
83+
<plugin name="validate_payment_method_on_place_order"
84+
type="Magento\QuoteGraphQl\Plugin\Quote\ValidatePaymentOnPlaceOrder"
85+
sortOrder="10"/>
8386
</type>
8487
<type name="Magento\QuoteGraphQl\Model\Resolver\UpdateCartItems">
8588
<arguments>

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use Magento\TestFramework\Helper\Bootstrap;
3535
use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException;
3636
use Magento\TestFramework\TestCase\GraphQlAbstract;
37+
use Magento\OfflinePayments\Model\Checkmo;
3738

3839
/**
3940
* Test for placing an order for guest
@@ -533,6 +534,56 @@ public function testPlaceOrderWithGiftMessage()
533534
$this->assertNotEmpty($order->getGiftMessageId());
534535
}
535536

537+
#[
538+
Config('carriers/flatrate/active', '1', 'store', 'default'),
539+
Config('carriers/tablerate/active', '1', 'store', 'default'),
540+
Config('carriers/freeshipping/active', '1', 'store', 'default'),
541+
Config('payment/checkmo/active', '1', 'store', 'default'),
542+
DataFixture(ProductFixture::class, as: 'product'),
543+
DataFixture(Indexer::class, as: 'indexer'),
544+
DataFixture(GuestCartFixture::class, ['reserved_order_id' => 'test_quote'], as: 'cart'),
545+
DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']),
546+
DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']),
547+
DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']),
548+
DataFixture(
549+
SetPaymentMethodFixture::class,
550+
['cart_id' => '$cart.id$', 'method' => Checkmo::PAYMENT_METHOD_CHECKMO_CODE]
551+
),
552+
Config('payment/checkmo/active', '0', 'store', 'default'),
553+
]
554+
public function testSetPreviouslyAddedPaymentMethodAfterItWasDisabled()
555+
{
556+
$cart = DataFixtureStorageManager::getStorage()->get('cart');
557+
$maskedQuoteId = $this->quoteIdToMaskedQuoteIdInterface->execute((int)$cart->getId());
558+
559+
$query = $this->setPaymentMethodQuery($maskedQuoteId, Checkmo::PAYMENT_METHOD_CHECKMO_CODE);
560+
561+
$this->expectException(\Exception::class);
562+
$this->expectExceptionMessage('The requested Payment Method is not available.');
563+
$this->graphQlMutation($query);
564+
}
565+
566+
/**
567+
* @param string $maskedQuoteId
568+
* @param string $methodCode
569+
* @return string
570+
*/
571+
private function setPaymentMethodQuery(string $maskedQuoteId, string $methodCode): string
572+
{
573+
return <<<QUERY
574+
mutation {
575+
setPaymentMethodOnCart(input: {
576+
cart_id: "{$maskedQuoteId}",
577+
payment_method: { code: "{$methodCode}" }
578+
}) {
579+
cart {
580+
selected_payment_method { code }
581+
}
582+
}
583+
}
584+
QUERY;
585+
}
586+
536587
/**
537588
* @param string $maskedQuoteId
538589
* @return string

0 commit comments

Comments
 (0)