Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECP-9450] Implement configurable 3DS2 authentication flow #2800

Merged
merged 3 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Gateway/Request/CheckoutDataBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Adyen\Payment\Helper\Data;
use Adyen\Payment\Helper\StateData;
use Adyen\Payment\Helper\OpenInvoice;
use Adyen\Payment\Model\Config\Source\ThreeDSFlow;
use Adyen\Payment\Model\Ui\AdyenPayByLinkConfigProvider;
use Adyen\Payment\Observer\AdyenCcDataAssignObserver;
use Adyen\Payment\Observer\AdyenPaymentMethodDataAssignObserver;
Expand Down Expand Up @@ -209,7 +210,8 @@ public function build(array $buildSubject): array
unset($requestBody['installments']);
}

$requestBody['additionalData']['allow3DS2'] = true;
$requestBody['additionalData']['allow3DS2'] =
$this->configHelper->getThreeDSFlow($storeId) === ThreeDSFlow::THREEDS_NATIVE;

return [
'body' => $requestBody
Expand Down
10 changes: 8 additions & 2 deletions Gateway/Request/RecurringVaultDataBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

namespace Adyen\Payment\Gateway\Request;

use Adyen\Payment\Helper\Config;
use Adyen\Payment\Helper\StateData;
use Adyen\Payment\Helper\Vault;
use Adyen\Payment\Model\Config\Source\ThreeDSFlow;
use Adyen\Payment\Model\Ui\AdyenCcConfigProvider;
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Payment\Gateway\Helper\SubjectReader;
Expand All @@ -22,13 +24,16 @@ class RecurringVaultDataBuilder implements BuilderInterface
{
private StateData $stateData;
private Vault $vaultHelper;
private Config $configHelper;

public function __construct(
StateData $stateData,
Vault $vaultHelper
Vault $vaultHelper,
Config $configHelper
) {
$this->stateData = $stateData;
$this->vaultHelper = $vaultHelper;
$this->configHelper = $configHelper;
}

public function build(array $buildSubject): array
Expand Down Expand Up @@ -62,7 +67,8 @@ public function build(array $buildSubject): array
* Due to new VISA compliance requirements, holderName is added to the payments call
*/
if ($paymentMethod->getCode() === AdyenCcConfigProvider::CC_VAULT_CODE) {
$requestBody['additionalData']['allow3DS2'] = true;
$requestBody['additionalData']['allow3DS2'] =
$this->configHelper->getThreeDSFlow($order->getStoreId()) === ThreeDSFlow::THREEDS_NATIVE;
$requestBody['paymentMethod']['holderName'] = $details['cardHolderName'] ?? null;
}

Expand Down
16 changes: 16 additions & 0 deletions Helper/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class Config
const AUTO_CAPTURE_OPENINVOICE = 'auto';
const XML_RECURRING_CONFIGURATION = 'recurring_configuration';
const XML_ALLOW_MULTISTORE_TOKENS = 'allow_multistore_tokens';
const XML_THREEDS_FLOW = 'threeds_flow';

protected ScopeConfigInterface $scopeConfig;
private EncryptorInterface $encryptor;
Expand Down Expand Up @@ -576,6 +577,21 @@ public function getAllowMultistoreTokens(int $storeId = null): ?bool
);
}

/**
* Returns the preferred ThreeDS authentication type for card and card vault payments.
*
* @param int|null $storeId
* @return string
*/
public function getThreeDSFlow(int $storeId = null): string
{
return $this->getConfigData(
self::XML_THREEDS_FLOW,
self::XML_ADYEN_CC,
$storeId
);
}

public function getConfigData(string $field, string $xmlPrefix, ?int $storeId, bool $flag = false): mixed
{
$path = implode("/", [self::XML_PAYMENT_PREFIX, $xmlPrefix, $field]);
Expand Down
31 changes: 31 additions & 0 deletions Model/Config/Source/ThreeDSFlow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2024 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Model\Config\Source;

use Magento\Framework\Data\OptionSourceInterface;

class ThreeDSFlow implements OptionSourceInterface
{
const THREEDS_NATIVE = 'native';
const THREEDS_REDIRECT = 'redirect';

/**
* @return array
*/
public function toOptionArray(): array
{
return [
['value' => self::THREEDS_NATIVE, 'label' => __('Native 3D Secure 2')],
['value' => self::THREEDS_REDIRECT, 'label' => __('Redirect 3D Secure 2')],
];
}
}
83 changes: 83 additions & 0 deletions Test/Unit/Gateway/Request/CheckoutDataBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Adyen\Payment\Test\Gateway\Request;

use Adyen\Payment\Gateway\Request\CheckoutDataBuilder;
use Adyen\Payment\Helper\ChargedCurrency;
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Helper\Data;
use Adyen\Payment\Helper\OpenInvoice;
use Adyen\Payment\Helper\StateData;
use Adyen\Payment\Model\Config\Source\ThreeDSFlow;
use Adyen\Payment\Test\Unit\AbstractAdyenTestCase;
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Payment;
use PHPUnit\Framework\MockObject\MockObject;

class CheckoutDataBuilderTest extends AbstractAdyenTestCase
{
protected ?CheckoutDataBuilder $checkoutDataBuilder;

protected Data|MockObject $adyenHelperMock;
protected StateData|MockObject $stateDataMock;
protected CartRepositoryInterface|MockObject $cartRepositoryMock;
protected ChargedCurrency|MockObject $chargedCurrencyMock;
protected Config|MockObject $configMock;
protected OpenInvoice|MockObject $openInvoiceMock;

public function setUp(): void
{
$this->adyenHelperMock = $this->createMock(Data::class);
$this->stateDataMock = $this->createMock(StateData::class);
$this->cartRepositoryMock = $this->createMock(CartRepositoryInterface::class);
$this->chargedCurrencyMock = $this->createMock(ChargedCurrency::class);
$this->configMock = $this->createMock(Config::class);
$this->openInvoiceMock = $this->createMock(OpenInvoice::class);

$this->checkoutDataBuilder = new CheckoutDataBuilder(
$this->adyenHelperMock,
$this->stateDataMock,
$this->cartRepositoryMock,
$this->chargedCurrencyMock,
$this->configMock,
$this->openInvoiceMock
);

parent::setUp();
}

public function tearDown(): void
{
$this->checkoutDataBuilder = null;
}


public function testAllowThreeDSFlag()
{
$storeId = 1;

$orderMock = $this->createMock(Order::class);
$orderMock->method('getQuoteId')->willReturn(1);
$orderMock->method('getStoreId')->willReturn($storeId);

$paymentMock = $this->createMock(Payment::class);
$paymentMock->method('getOrder')->willReturn($orderMock);

$buildSubject = [
'payment' => $this->createConfiguredMock(PaymentDataObject::class, [
'getPayment' => $paymentMock
])
];

$this->configMock->expects($this->once())
->method('getThreeDSFlow')
->with($storeId)
->willReturn(ThreeDSFlow::THREEDS_NATIVE);

$request = $this->checkoutDataBuilder->build($buildSubject);

$this->assertTrue($request['body']['additionalData']['allow3DS2']);
}
}
20 changes: 20 additions & 0 deletions Test/Unit/Helper/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,24 @@ public function testRemoveConfigData()

$this->configHelper->removeConfigData($field, $xml_prefix);
}

public function testGetThreeDSModes()
{
$storeId = PHP_INT_MAX;
$expectedResult = MethodInterface::ACTION_ORDER;
$path = sprintf(
"%s/%s/%s",
Config::XML_PAYMENT_PREFIX,
Config::XML_ADYEN_CC,
Config::XML_THREEDS_FLOW
);

$this->scopeConfigMock->expects($this->once())
->method('getValue')
->with($this->equalTo($path), $this->equalTo(ScopeInterface::SCOPE_STORE), $this->equalTo($storeId))
->willReturn($expectedResult);

$result = $this->configHelper->getThreeDSFlow($storeId);
$this->assertEquals($expectedResult, $result);
}
}
21 changes: 21 additions & 0 deletions Test/Unit/Model/Config/Source/ThreeDSModeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Adyen\Payment\Test\Unit\Model\Config\Source;

use Adyen\Payment\Model\Config\Source\ThreeDSFlow;
use Adyen\Payment\Test\Unit\AbstractAdyenTestCase;

class ThreeDSModeTest extends AbstractAdyenTestCase
{
public function testToOptionArray()
{
$threeDSModeSource = new ThreeDSFlow();

$expected = [
['value' => ThreeDSFlow::THREEDS_NATIVE, 'label' => __('Native 3D Secure 2')],
['value' => ThreeDSFlow::THREEDS_REDIRECT, 'label' => __('Redirect 3D Secure 2')],
];

$this->assertEquals($expected, $threeDSModeSource->toOptionArray());
}
}
5 changes: 5 additions & 0 deletions etc/adminhtml/system/adyen_card_payments.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,10 @@
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<config_path>payment/adyen_cc/enable_click_to_pay</config_path>
</field>
<field id="threeds_flow" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1">
<label>3D Secure 2 authentication flow</label>
<source_model>Adyen\Payment\Model\Config\Source\ThreeDSFlow</source_model>
<config_path>payment/adyen_cc/threeds_flow</config_path>
</field>
</group>
</include>
1 change: 1 addition & 0 deletions etc/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<can_authorize_vault>1</can_authorize_vault>
<can_capture_vault>1</can_capture_vault>
<supports_recurring>1</supports_recurring>
<threeds_flow>native</threeds_flow>
<group>adyen</group>
</adyen_cc>
<adyen_cc_vault>
Expand Down
Loading