Skip to content

Commit 76f8a43

Browse files
committed
Merge remote-tracking branch 'origin/AC-13414' into spartans_pr_25022025
2 parents 3a6536d + a246b23 commit 76f8a43

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All rights reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Paypal\Plugin;
9+
10+
use Magento\Framework\App\RequestInterface;
11+
use Magento\Paypal\Controller\Payflow\ReturnUrl as Subject;
12+
use Magento\Paypal\Model\PayflowlinkFactory;
13+
use Magento\Sales\Model\Order;
14+
use Magento\Sales\Model\OrderFactory;
15+
use Psr\Log\LoggerInterface;
16+
17+
class PayflowSilentPost
18+
{
19+
/**
20+
* @var array
21+
*/
22+
protected array $allowedOrderStates = [
23+
Order::STATE_PROCESSING,
24+
Order::STATE_COMPLETE,
25+
Order::STATE_PAYMENT_REVIEW
26+
];
27+
28+
/**
29+
* @param RequestInterface $request
30+
* @param OrderFactory $orderFactory
31+
* @param PayflowlinkFactory $payflowlinkFactory
32+
* @param LoggerInterface $logger
33+
*/
34+
public function __construct(
35+
private readonly RequestInterface $request,
36+
private readonly OrderFactory $orderFactory,
37+
private readonly PayflowlinkFactory $payflowlinkFactory,
38+
private readonly LoggerInterface $logger,
39+
) {
40+
}
41+
42+
/**
43+
* Process payment if not already done via silent post
44+
*
45+
* @param Subject $subject
46+
* @return void
47+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
48+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
49+
*/
50+
public function beforeExecute(Subject $subject): void
51+
{
52+
$data = $this->request->getParams();
53+
if (!array_key_exists('INVNUM', $data)
54+
|| !array_key_exists('RESPMSG', $data)
55+
|| !array_key_exists('RESULT', $data)) {
56+
return;
57+
}
58+
59+
$orderId = (string)$data['INVNUM'];
60+
if (!$orderId) {
61+
return;
62+
}
63+
64+
$order = $this->orderFactory->create()->loadByIncrementId($orderId);
65+
$payment = $order->getPayment();
66+
if (in_array($order->getState(), $this->allowedOrderStates) || $payment->getLastTransId()
67+
|| trim((string)$data['RESPMSG']) !== 'Approved' || (int)$data['RESULT'] !== 0) {
68+
return;
69+
}
70+
71+
$paymentModel = $this->payflowlinkFactory->create();
72+
try {
73+
$paymentModel->process($data);
74+
} catch (\Exception $e) {
75+
$this->logger->critical($e);
76+
}
77+
}
78+
}

app/code/Magento/Paypal/etc/di.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,7 @@
291291
</argument>
292292
</arguments>
293293
</type>
294+
<type name="Magento\Paypal\Controller\Payflow\ReturnUrl">
295+
<plugin name="payflow_silentpost" type="Magento\Paypal\Plugin\PayflowSilentPost"/>
296+
</type>
294297
</config>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All rights reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Paypal\Plugin;
9+
10+
use Magento\Framework\Api\FilterBuilder;
11+
use Magento\Framework\Api\SearchCriteriaBuilder;
12+
use Magento\Framework\DataObject;
13+
use Magento\Paypal\Model\Config;
14+
use Magento\Paypal\Model\Payflow\Service\Gateway;
15+
use Magento\Paypal\Model\Payflowlink;
16+
use Magento\Sales\Api\Data\OrderInterface;
17+
use Magento\Sales\Api\OrderRepositoryInterface;
18+
use Magento\Sales\Model\Order;
19+
use Magento\Sales\Model\Order\Email\Sender\OrderSender;
20+
use Magento\Sales\Model\Order\Payment;
21+
use Magento\TestFramework\TestCase\AbstractController;
22+
use PHPUnit\Framework\MockObject\MockObject;
23+
24+
/**
25+
* @magentoAppIsolation enabled
26+
*/
27+
class PayflowSilentPostTest extends AbstractController
28+
{
29+
/**
30+
* @var Gateway|MockObject
31+
*/
32+
private $gateway;
33+
34+
/**
35+
* @var OrderSender|MockObject
36+
*/
37+
private $orderSender;
38+
39+
/**
40+
* @var string
41+
*/
42+
protected $orderIncrementId = '000000045';
43+
44+
/**
45+
* @inheritdoc
46+
*/
47+
protected function setUp(): void
48+
{
49+
parent::setUp();
50+
51+
$this->gateway = $this->getMockBuilder(Gateway::class)
52+
->disableOriginalConstructor()
53+
->getMock();
54+
55+
$this->orderSender = $this->getMockBuilder(OrderSender::class)
56+
->disableOriginalConstructor()
57+
->getMock();
58+
59+
$this->_objectManager->addSharedInstance($this->gateway, Gateway::class);
60+
$this->_objectManager->addSharedInstance($this->orderSender, OrderSender::class);
61+
62+
$order = $this->getOrder();
63+
$payment = $this->_objectManager->create(Payment::class);
64+
$payment->setMethod(Config::METHOD_PAYFLOWLINK)
65+
->setBaseAmountAuthorized(100)
66+
->setAdditionalInformation(
67+
[
68+
'secure_silent_post_hash' => 'cf7i85d01ed7c92223031afb4rdl2f1f'
69+
]
70+
);
71+
$order->setPayment($payment);
72+
$order->setState(Order::STATE_PENDING_PAYMENT)
73+
->setStatus(Order::STATE_PENDING_PAYMENT);
74+
$orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class);
75+
$orderRepository->save($order);
76+
}
77+
78+
/**
79+
* @inheritdoc
80+
*/
81+
protected function tearDown(): void
82+
{
83+
$this->_objectManager->removeSharedInstance(Gateway::class);
84+
$this->_objectManager->removeSharedInstance(OrderSender::class);
85+
parent::tearDown();
86+
}
87+
88+
/**
89+
* Checks a test case when Payflow Link return url before plugin is executed with transaction details.
90+
*
91+
* @param int $resultCode
92+
* @param string $orderState
93+
* @param string $orderStatus
94+
* @magentoDataFixture Magento/Paypal/_files/order_payflow_link.php
95+
* @dataProvider responseCodeDataProvider
96+
*/
97+
public function testOrderStatusWithDifferentPaypalResponse($resultCode, $orderState, $orderStatus)
98+
{
99+
$this->withRequest($resultCode);
100+
$this->withGatewayResponse($resultCode);
101+
102+
$this->dispatch('paypal/payflow/returnUrl');
103+
self::assertEquals(200, $this->getResponse()->getStatusCode());
104+
105+
$order = $this->getOrder();
106+
self::assertEquals($orderState, $order->getState());
107+
self::assertEquals($orderStatus, $order->getStatus());
108+
}
109+
110+
/**
111+
* Get list of different variations for paypal response
112+
*
113+
* @return array
114+
*/
115+
public static function responseCodeDataProvider()
116+
{
117+
return [
118+
[Payflowlink::RESPONSE_CODE_APPROVED, Order::STATE_COMPLETE, Order::STATE_COMPLETE],
119+
[Payflowlink::RESPONSE_CODE_DECLINED, Order::STATE_PENDING_PAYMENT, Order::STATE_PENDING_PAYMENT]
120+
];
121+
}
122+
123+
/**
124+
* Imitates real request with test data.
125+
*
126+
* @param int $resultCode
127+
* @return void
128+
*/
129+
private function withRequest($resultCode)
130+
{
131+
$data = [
132+
'INVNUM' => $this->orderIncrementId,
133+
'AMT' => 100,
134+
'PNREF' => 'A21CP234KLB8',
135+
'USER2' => 'cf7i85d01ed7c92223031afb4rdl2f1f',
136+
'RESULT' => $resultCode,
137+
'TYPE' => 'A',
138+
'RESPMSG' => 'Approved'
139+
];
140+
$this->getRequest()->setParams($data);
141+
}
142+
143+
/**
144+
* Imitates response from PayPal gateway.
145+
*
146+
* @param int $resultCode
147+
* @return void
148+
*/
149+
private function withGatewayResponse($resultCode)
150+
{
151+
$response = new DataObject([
152+
'custref' => $this->orderIncrementId,
153+
'origresult' => $resultCode,
154+
'respmsg' => 'Response message from PayPal gateway'
155+
]);
156+
$this->gateway->method('postRequest')
157+
->willReturn($response);
158+
}
159+
160+
/**
161+
* Gets order stored by fixture.
162+
*
163+
* @return OrderInterface
164+
*/
165+
private function getOrder()
166+
{
167+
/** @var FilterBuilder $filterBuilder */
168+
$filterBuilder = $this->_objectManager->get(FilterBuilder::class);
169+
$filters = [
170+
$filterBuilder->setField(OrderInterface::INCREMENT_ID)
171+
->setValue($this->orderIncrementId)
172+
->create()
173+
];
174+
175+
/** @var SearchCriteriaBuilder $searchCriteriaBuilder */
176+
$searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class);
177+
$searchCriteria = $searchCriteriaBuilder->addFilters($filters)
178+
->create();
179+
180+
$orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class);
181+
$orders = $orderRepository->getList($searchCriteria)
182+
->getItems();
183+
184+
/** @var OrderInterface $order */
185+
return array_pop($orders);
186+
}
187+
}

0 commit comments

Comments
 (0)