Skip to content

Commit ec8810d

Browse files
committed
Merge remote-tracking branch 'origin/MC-32494' into 2.4-develop-pr19
2 parents 9767717 + 24481e9 commit ec8810d

File tree

12 files changed

+458
-18
lines changed

12 files changed

+458
-18
lines changed

app/code/Magento/Paypal/Model/Payflow/Service/Response/Transaction.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public function savePaymentInQuote($response, $cartId)
117117

118118
$payment->setData(OrderPaymentInterface::CC_TYPE, $response->getData(OrderPaymentInterface::CC_TYPE));
119119
$payment->setAdditionalInformation(Payflowpro::PNREF, $response->getData(Payflowpro::PNREF));
120+
$payment->setAdditionalInformation('result_code', $response->getData('result'));
120121

121122
$expDate = $response->getData('expdate');
122123
$expMonth = $this->getCcExpMonth($expDate);

app/code/Magento/Paypal/Model/Payflow/Transparent.php

Lines changed: 157 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,24 @@
77

88
namespace Magento\Paypal\Model\Payflow;
99

10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Framework\Exception\State\InvalidTransitionException;
1012
use Magento\Payment\Helper\Formatter;
1113
use Magento\Payment\Model\InfoInterface;
12-
use Magento\Paypal\Model\Payflowpro;
13-
use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory;
14-
use Magento\Sales\Model\Order\Payment;
15-
use Magento\Paypal\Model\Payflow\Service\Gateway;
16-
use Magento\Framework\Exception\LocalizedException;
17-
use Magento\Payment\Model\Method\TransparentInterface;
1814
use Magento\Payment\Model\Method\ConfigInterfaceFactory;
19-
use Magento\Framework\Exception\State\InvalidTransitionException;
15+
use Magento\Payment\Model\Method\TransparentInterface;
16+
use Magento\Payment\Model\MethodInterface;
17+
use Magento\Paypal\Model\Payflow\Service\Gateway;
2018
use Magento\Paypal\Model\Payflow\Service\Response\Handler\HandlerInterface;
2119
use Magento\Paypal\Model\Payflow\Service\Response\Validator\ResponseValidator;
20+
use Magento\Paypal\Model\Payflowpro;
21+
use Magento\Sales\Api\Data\OrderPaymentExtensionInterfaceFactory;
22+
use Magento\Sales\Model\Order\Payment;
2223
use Magento\Vault\Api\Data\PaymentTokenInterface;
2324
use Magento\Vault\Api\Data\PaymentTokenInterfaceFactory;
2425

2526
/**
26-
* Payflow Pro payment gateway model
27+
* Payflow Pro payment gateway model (transparent redirect).
2728
*
2829
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2930
*/
@@ -35,6 +36,16 @@ class Transparent extends Payflowpro implements TransparentInterface
3536

3637
const CC_VAULT_CODE = 'payflowpro_cc_vault';
3738

39+
/**
40+
* Result code of account verification transaction request.
41+
*/
42+
private const RESULT_CODE = 'result_code';
43+
44+
/**
45+
* Fraud Management Filters config setting.
46+
*/
47+
private const CONFIG_FMF = 'fmf';
48+
3849
/**
3950
* @var string
4051
*/
@@ -45,6 +56,13 @@ class Transparent extends Payflowpro implements TransparentInterface
4556
*/
4657
protected $_infoBlockType = \Magento\Paypal\Block\Payment\Info::class;
4758

59+
/**
60+
* Fetch transaction details availability option.
61+
*
62+
* @var bool
63+
*/
64+
protected $_canFetchTransactionInfo = false;
65+
4866
/**
4967
* @var ResponseValidator
5068
*/
@@ -165,6 +183,14 @@ public function validate()
165183
*/
166184
public function authorize(InfoInterface $payment, $amount)
167185
{
186+
if ($this->isFraudDetected($payment)) {
187+
$this->markPaymentAsFraudulent($payment);
188+
return $this;
189+
}
190+
191+
$zeroAmountAuthorizationId = $this->getZeroAmountAuthorizationId($payment);
192+
/** @var PaymentTokenInterface $vaultPaymentToken */
193+
$vaultPaymentToken = $payment->getExtensionAttributes()->getVaultPaymentToken();
168194
/** @var Payment $payment */
169195
$request = $this->buildBasicRequest();
170196

@@ -177,9 +203,9 @@ public function authorize(InfoInterface $payment, $amount)
177203
$payPalCart = $this->payPalCartFactory->create(['salesModel' => $order]);
178204
$payPalCart->getAmounts();
179205

180-
$token = $payment->getAdditionalInformation(self::PNREF);
206+
$parentTransactionId = $vaultPaymentToken ? $vaultPaymentToken->getGatewayToken() : $zeroAmountAuthorizationId;
181207
$request->setData('trxtype', self::TRXTYPE_AUTH_ONLY);
182-
$request->setData('origid', $token);
208+
$request->setData('origid', $parentTransactionId);
183209
$request->setData('amt', $this->formatPrice($amount));
184210
$request->setData('currency', $order->getBaseCurrencyCode());
185211
$request->setData('itemamt', $this->formatPrice($payPalCart->getSubtotal()));
@@ -200,10 +226,15 @@ public function authorize(InfoInterface $payment, $amount)
200226

201227
$this->setTransStatus($payment, $response);
202228

203-
$this->createPaymentToken($payment, $token);
229+
if ($vaultPaymentToken) {
230+
$payment->setParentTransactionId($vaultPaymentToken->getGatewayToken());
231+
} else {
232+
$this->createPaymentToken($payment, $zeroAmountAuthorizationId);
233+
}
204234

205235
$payment->unsAdditionalInformation(self::CC_DETAILS);
206236
$payment->unsAdditionalInformation(self::PNREF);
237+
$payment->unsAdditionalInformation(self::RESULT_CODE);
207238

208239
return $this;
209240
}
@@ -291,14 +322,126 @@ private function getPaymentExtensionAttributes(Payment $payment)
291322
*/
292323
public function capture(InfoInterface $payment, $amount)
293324
{
325+
if ($this->isFraudDetected($payment)) {
326+
$this->markPaymentAsFraudulent($payment);
327+
return $this;
328+
}
329+
294330
/** @var Payment $payment */
295-
$token = $payment->getAdditionalInformation(self::PNREF);
331+
$zeroAmountAuthorizationId = $this->getZeroAmountAuthorizationId($payment);
332+
/** @var PaymentTokenInterface $vaultPaymentToken */
333+
$vaultPaymentToken = $payment->getExtensionAttributes()->getVaultPaymentToken();
334+
if ($vaultPaymentToken && empty($zeroAmountAuthorizationId)) {
335+
$payment->setAdditionalInformation(self::PNREF, $vaultPaymentToken->getGatewayToken());
336+
if (!$payment->getParentTransactionId()) {
337+
$payment->setParentTransactionId($vaultPaymentToken->getGatewayToken());
338+
}
339+
}
296340
parent::capture($payment, $amount);
297341

298-
if ($token && !$payment->getAuthorizationTransaction()) {
299-
$this->createPaymentToken($payment, $token);
342+
if ($zeroAmountAuthorizationId && $vaultPaymentToken === null) {
343+
$this->createPaymentToken($payment, $zeroAmountAuthorizationId);
300344
}
301345

302346
return $this;
303347
}
348+
349+
/**
350+
* Attempt to accept a pending payment.
351+
*
352+
* Order acquires a payment review state based on results of PayPal account verification transaction (zero-amount
353+
* authorization). For accepting a payment should be created PayPal reference transaction with a real order amount.
354+
* Fraud Protection Service filters do not screen reference transactions.
355+
*
356+
* @param InfoInterface $payment
357+
* @return bool
358+
* @throws InvalidTransitionException
359+
* @throws LocalizedException
360+
*/
361+
public function acceptPayment(InfoInterface $payment)
362+
{
363+
if ($this->getConfigPaymentAction() === MethodInterface::ACTION_AUTHORIZE_CAPTURE) {
364+
$invoices = iterator_to_array($payment->getOrder()->getInvoiceCollection());
365+
$invoice = count($invoices) ? reset($invoices) : null;
366+
$payment->capture($invoice);
367+
} else {
368+
$amount = $payment->getOrder()->getBaseGrandTotal();
369+
$payment->authorize(true, $amount);
370+
}
371+
372+
return true;
373+
}
374+
375+
/**
376+
* Deny a pending payment.
377+
*
378+
* Order acquires a payment review state based on results of PayPal account verification transaction (zero-amount
379+
* authorization). This transaction type cannot be voided, so we do not send any request to payment gateway.
380+
*
381+
* @param InfoInterface $payment
382+
* @return bool
383+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
384+
*/
385+
public function denyPayment(InfoInterface $payment)
386+
{
387+
return true;
388+
}
389+
390+
/**
391+
* Marks payment as fraudulent.
392+
*
393+
* @param InfoInterface $payment
394+
* @throws \Exception
395+
*/
396+
private function markPaymentAsFraudulent(InfoInterface $payment): void
397+
{
398+
$zeroAmountAuthorizationId = $this->getZeroAmountAuthorizationId($payment);
399+
$payment->setTransactionId($zeroAmountAuthorizationId);
400+
$payment->setIsTransactionClosed(0);
401+
$payment->setIsTransactionPending(true);
402+
$payment->setIsFraudDetected(true);
403+
$this->createPaymentToken($payment, $zeroAmountAuthorizationId);
404+
$fraudulentMsg = 'Order is suspended as an account verification transaction is suspected to be fraudulent.';
405+
$extensionAttributes = $this->getPaymentExtensionAttributes($payment);
406+
$extensionAttributes->setNotificationMessage($fraudulentMsg);
407+
$payment->unsAdditionalInformation(self::CC_DETAILS);
408+
$payment->unsAdditionalInformation(self::PNREF);
409+
$payment->unsAdditionalInformation(self::RESULT_CODE);
410+
}
411+
412+
/**
413+
* Checks if fraud filters were triggered for the payment.
414+
*
415+
* For current PayPal PayflowPro transparent redirect integration
416+
* Fraud Protection Service filters screen only account verification
417+
* transaction (also known as zero dollar authorization).
418+
* Following reference transaction with real dollar amount will not be screened
419+
* by Fraud Protection Service.
420+
*
421+
* @param InfoInterface $payment
422+
* @return bool
423+
*/
424+
private function isFraudDetected(InfoInterface $payment): bool
425+
{
426+
$resultCode = $payment->getAdditionalInformation(self::RESULT_CODE);
427+
$isFmfEnabled = (bool)$this->getConfig()->getValue(self::CONFIG_FMF);
428+
return $isFmfEnabled && $this->getZeroAmountAuthorizationId($payment) && in_array(
429+
$resultCode,
430+
[self::RESPONSE_CODE_DECLINED_BY_FILTER, self::RESPONSE_CODE_FRAUDSERVICE_FILTER]
431+
);
432+
}
433+
434+
/**
435+
* Returns zero dollar authorization transaction id.
436+
*
437+
* PNREF (transaction id) is available in payment additional information only right after
438+
* PayPal account verification transaction (also known as zero dollar authorization).
439+
*
440+
* @param InfoInterface $payment
441+
* @return string
442+
*/
443+
private function getZeroAmountAuthorizationId(InfoInterface $payment): string
444+
{
445+
return (string)$payment->getAdditionalInformation(self::PNREF);
446+
}
304447
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Paypal\Plugin;
9+
10+
use Magento\Framework\Exception\LocalizedException;
11+
use Magento\Sales\Api\InvoiceRepositoryInterface;
12+
use Magento\Sales\Model\Order\Payment;
13+
14+
/**
15+
* Updates invoice transaction id for PayPal PayflowPro payment.
16+
*/
17+
class TransparentOrderPayment
18+
{
19+
/**
20+
* @var InvoiceRepositoryInterface
21+
*/
22+
private $invoiceRepository;
23+
24+
/**
25+
* @param InvoiceRepositoryInterface $invoiceRepository
26+
*/
27+
public function __construct(InvoiceRepositoryInterface $invoiceRepository)
28+
{
29+
$this->invoiceRepository = $invoiceRepository;
30+
}
31+
32+
/**
33+
* Updates invoice transaction id.
34+
*
35+
* Accepting PayPal PayflowPro payment actually means executing new reference transaction
36+
* based on account verification. So for existing pending invoice, transaction id should be updated
37+
* with the id of last reference transaction.
38+
*
39+
* @param Payment $subject
40+
* @param Payment $result
41+
* @return Payment
42+
* @throws LocalizedException
43+
*/
44+
public function afterAccept(Payment $subject, Payment $result): Payment
45+
{
46+
$paymentMethod = $subject->getMethodInstance();
47+
if (!$paymentMethod instanceof \Magento\Paypal\Model\Payflow\Transparent) {
48+
return $result;
49+
}
50+
51+
$invoices = iterator_to_array($subject->getOrder()->getInvoiceCollection());
52+
$invoice = reset($invoices);
53+
if ($invoice) {
54+
$invoice->setTransactionId($subject->getLastTransId());
55+
$this->invoiceRepository->save($invoice);
56+
}
57+
58+
return $result;
59+
}
60+
}

app/code/Magento/Paypal/Test/Unit/Model/Payflow/TransparentTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,9 @@ private function getPaymentExtensionInterfaceFactory()
196196
->disableOriginalConstructor()
197197
->getMock();
198198
$orderPaymentExtension = $this->getMockBuilder(OrderPaymentExtensionInterface::class)
199-
->setMethods(['setVaultPaymentToken', 'getVaultPaymentToken'])
199+
->setMethods(
200+
['setVaultPaymentToken', 'getVaultPaymentToken', 'setNotificationMessage', 'getNotificationMessage']
201+
)
200202
->disableOriginalConstructor()
201203
->getMock();
202204

@@ -290,12 +292,17 @@ private function initPayment()
290292
$this->order = $this->getMockBuilder(Order::class)
291293
->disableOriginalConstructor()
292294
->getMock();
293-
295+
$paymentExtensionAttributes = $this->getMockBuilder(OrderPaymentExtensionInterface::class)
296+
->setMethods(
297+
['setVaultPaymentToken', 'getVaultPaymentToken', 'setNotificationMessage', 'getNotificationMessage']
298+
)
299+
->getMockForAbstractClass();
294300
$this->payment->method('getOrder')->willReturn($this->order);
295301
$this->payment->method('setTransactionId')->willReturnSelf();
296302
$this->payment->method('setIsTransactionClosed')->willReturnSelf();
297303
$this->payment->method('getCcExpYear')->willReturn('2019');
298304
$this->payment->method('getCcExpMonth')->willReturn('05');
305+
$this->payment->method('getExtensionAttributes')->willReturn($paymentExtensionAttributes);
299306

300307
return $this->payment;
301308
}

app/code/Magento/Paypal/etc/adminhtml/system/paypal_payflowpro.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@
170170
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
171171
<attribute type="shared">1</attribute>
172172
</field>
173+
<field id="fmf" translate="label comment" type="select" sortOrder="45" showInDefault="1" showInWebsite="1" showInStore="0">
174+
<label>Fraud Management Filters</label>
175+
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
176+
<comment>Be sure to configure Fraud Management Filters in your PayPal account in "Service Settings/Fraud Protection" section. Attention! Please don't use Total Purchase Price Ceiling/Floor Filters. Current integration doesn't support them.</comment>
177+
<config_path>payment/payflowpro/fmf</config_path>
178+
</field>
173179
<group id="paypal_payflow_avs_check" translate="label" showInDefault="1" showInWebsite="1" sortOrder="80">
174180
<label>CVV and AVS Settings</label>
175181
<field id="heading_avs_settings" translate="label" sortOrder="0" showInDefault="1" showInWebsite="1">

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<cgi_url>https://payflowlink.paypal.com</cgi_url>
110110
<transaction_url_test_mode>https://pilot-payflowpro.paypal.com</transaction_url_test_mode>
111111
<transaction_url>https://payflowpro.paypal.com</transaction_url>
112+
<fmf>0</fmf>
112113
<avs_street>0</avs_street>
113114
<avs_zip>0</avs_zip>
114115
<avs_international>0</avs_international>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,7 @@
255255
<type name="Magento\Framework\Session\SessionStartChecker">
256256
<plugin name="transparent_session_checker" type="Magento\Paypal\Plugin\TransparentSessionChecker"/>
257257
</type>
258+
<type name="Magento\Sales\Model\Order\Payment">
259+
<plugin name="paypal_transparent" type="Magento\Paypal\Plugin\TransparentOrderPayment"/>
260+
</type>
258261
</config>

app/code/Magento/Paypal/i18n/en_US.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,3 +738,4 @@ User,User
738738
"PayPal Guest Checkout Credit Card Icons","PayPal Guest Checkout Credit Card Icons"
739739
"Elektronisches Lastschriftverfahren - German ELV","Elektronisches Lastschriftverfahren - German ELV"
740740
"Please enter at least 0 and at most 65535","Please enter at least 0 and at most 65535"
741+
"Order is suspended as an account verification transaction is suspected to be fraudulent.","Order is suspended as an account verification transaction is suspected to be fraudulent."

0 commit comments

Comments
 (0)