Skip to content

Commit

Permalink
[ECP-9357] Implement Instant Purchase (#2828)
Browse files Browse the repository at this point in the history
* [ECP-9357] Implement Instant Purchase

* [ECP-9357] Add InstantPurchase dependency

* [ECP-9357] Use local exception type

* [ECP-9357] Refactor RecurringVaultDataBuilder unit tests

* [ECP-9357] Write unit tests for AdyenCcConfigProvider class

---------

Co-authored-by: Can Demiralp <[email protected]>
  • Loading branch information
candemiralp and Can Demiralp authored Dec 31, 2024
1 parent 042dcd6 commit 0ad2747
Show file tree
Hide file tree
Showing 20 changed files with 1,243 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Api\InstantPurchase\PaymentMethodIntegration;

use Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface;

interface AdyenAvailabilityCheckerInterface extends AvailabilityCheckerInterface
{
/**
* Checks if Adyen alternative payment method may be used for instant purchase.
*
* This interface extends the default `AvailabilityCheckerInterface` and implements
* a new method with payment method argument. This interface is used in `InstantPurchaseIntegrations`
* plugin to override the `AvailabilityCheckerInterface` which doesn't have payment method argument.
*
* @param string $paymentMethodCode
*
* @return bool
*/
public function isAvailableAdyenMethod(string $paymentMethodCode): bool;
}
90 changes: 51 additions & 39 deletions Gateway/Request/RecurringVaultDataBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Adyen Payment module (https://www.adyen.com/)
*
* Copyright (c) 2023 Adyen N.V. (https://www.adyen.com/)
* Copyright (c) 2024 Adyen N.V. (https://www.adyen.com/)
* See LICENSE.txt for license details.
*
* Author: Adyen <[email protected]>
Expand All @@ -15,75 +15,87 @@
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\Framework\Exception\LocalizedException;
use Magento\Payment\Gateway\Data\PaymentDataObject;
use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Vault\Api\Data\PaymentTokenFactoryInterface;

class RecurringVaultDataBuilder implements BuilderInterface
{
private StateData $stateData;
private Vault $vaultHelper;
private Config $configHelper;

/**
* @param StateData $stateData
* @param Vault $vaultHelper
* @param Config $configHelper
*/
public function __construct(
StateData $stateData,
Vault $vaultHelper,
Config $configHelper
) {
$this->stateData = $stateData;
$this->vaultHelper = $vaultHelper;
$this->configHelper = $configHelper;
}
private readonly StateData $stateData,
private readonly Vault $vaultHelper,
private readonly Config $configHelper
) { }

/**
* @throws LocalizedException
*/
public function build(array $buildSubject): array
{
/** @var PaymentDataObject $paymentDataObject */
$paymentDataObject = SubjectReader::readPayment($buildSubject);

$payment = $paymentDataObject->getPayment();
$paymentMethod = $payment->getMethodInstance();

$order = $payment->getOrder();
$extensionAttributes = $payment->getExtensionAttributes();

$paymentToken = $extensionAttributes->getVaultPaymentToken();
$details = json_decode((string) ($paymentToken->getTokenDetails() ?: '{}'), true);

// Initialize the request body with the current state data
$requestBody = $this->stateData->getStateData($order->getQuoteId());
if ($paymentToken->getType() === PaymentTokenFactoryInterface::TOKEN_TYPE_CREDIT_CARD) {
// Build base request for card token payments (including card wallets)

// For now this will only be used by tokens created trough adyen_hpp payment methods
if (array_key_exists(Vault::TOKEN_TYPE, $details)) {
$requestBody['recurringProcessingModel'] = $details[Vault::TOKEN_TYPE];
} else {
// If recurringProcessingModel doesn't exist in the token details, use the default value from config.
$requestBody['recurringProcessingModel'] = $this->vaultHelper->getPaymentMethodRecurringProcessingModel(
$paymentMethod->getProviderCode(),
$order->getStoreId()
);
}
$isInstantPurchase = (bool) $payment->getAdditionalInformation('instant-purchase');

if ($isInstantPurchase) {
// `Instant Purchase` doesn't have the component and state data. Build the `paymentMethod` object.
$requestBody['paymentMethod']['type'] = 'scheme';
$requestBody['paymentMethod']['storedPaymentMethodId'] = $paymentToken->getGatewayToken();
} else {
// Initialize the request body with the current state data if it's not `Instant Purchase`.
$requestBody = $this->stateData->getStateData($order->getQuoteId());
}

/*
* allow3DS flag is required to trigger the native 3DS challenge.
* Otherwise, shopper will be redirected to the issuer for challenge.
* Due to new VISA compliance requirements, holderName is added to the payments call
*/
if ($paymentMethod->getCode() === AdyenCcConfigProvider::CC_VAULT_CODE) {
/*
* `allow3DS: true` flag is required to trigger the native 3DS challenge.
* Otherwise, shopper will be redirected to the issuer for challenge.
*/
$requestBody['additionalData']['allow3DS2'] =
$this->configHelper->getThreeDSFlow($order->getStoreId()) === ThreeDSFlow::THREEDS_NATIVE;

// Due to new VISA compliance requirements, holderName is added to the payments call
$requestBody['paymentMethod']['holderName'] = $details['cardHolderName'] ?? null;
}
} else {
// Build base request for alternative payment methods for regular checkout and Instant Purchase

/**
* Build paymentMethod object for alternative payment methods
*/
if ($paymentMethod->getCode() !== AdyenCcConfigProvider::CC_VAULT_CODE) {
$requestBody['paymentMethod'] = [
'type' => $details['type'],
'storedPaymentMethodId' => $paymentToken->getGatewayToken()
];
}

$request['body'] = $requestBody;
// Check the `stateData` if `recurringProcessingModel` is added through a headless request.
if (array_key_exists(Vault::TOKEN_TYPE, $details)) {
$requestBody['recurringProcessingModel'] = $details[Vault::TOKEN_TYPE];
} else {
// If recurringProcessingModel doesn't exist in the token details, use the default value from config.
$requestBody['recurringProcessingModel'] = $this->vaultHelper->getPaymentMethodRecurringProcessingModel(
$paymentMethod->getProviderCode(),
$order->getStoreId()
);
}

return $request;
return [
'body' => $requestBody
];
}
}
10 changes: 10 additions & 0 deletions Helper/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,16 @@ public function getThreeDSFlow(int $storeId = null): string
);
}

public function getIsCvcRequiredForRecurringCardPayments(int $storeId = null): bool
{
return (bool) $this->getConfigData(
'require_cvc',
Config::XML_ADYEN_CC_VAULT,
$storeId,
true
);
}

public function getConfigData(string $field, string $xmlPrefix, ?int $storeId, bool $flag = false): mixed
{
$path = implode("/", [self::XML_PAYMENT_PREFIX, $xmlPrefix, $field]);
Expand Down
59 changes: 59 additions & 0 deletions Model/InstantPurchase/Card/AvailabilityChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Model\InstantPurchase\Card;

use Adyen\Payment\Helper\Config;
use Adyen\Payment\Helper\Vault;
use Adyen\Payment\Model\Ui\AdyenCcConfigProvider;
use Magento\InstantPurchase\PaymentMethodIntegration\AvailabilityCheckerInterface;
use Magento\Store\Model\StoreManagerInterface;

class AvailabilityChecker implements AvailabilityCheckerInterface
{
/**
* @param Config $configHelper
* @param Vault $vaultHelper
* @param StoreManagerInterface $storeManager
*/
public function __construct(
private readonly Config $configHelper,
private readonly Vault $vaultHelper,
private readonly StoreManagerInterface $storeManager
) { }

/**
* Instant Purchase is available if card recurring is enabled, recurring processing model is set to `CardOnFile`
* and CVC is not required to complete the payment.
*/
public function isAvailable(): bool
{
$storeId = $this->storeManager->getStore()->getId();

$isCardRecurringEnabled = $this->vaultHelper->getPaymentMethodRecurringActive(
AdyenCcConfigProvider::CODE,
$storeId
);

$recurringProcessingModel = $this->vaultHelper->getPaymentMethodRecurringProcessingModel(
AdyenCcConfigProvider::CODE,
$storeId
);

$isCvcRequiredForCardRecurringPayments =
$this->configHelper->getIsCvcRequiredForRecurringCardPayments($storeId);

return $isCardRecurringEnabled &&
!$isCvcRequiredForCardRecurringPayments &&
$recurringProcessingModel === Vault::CARD_ON_FILE;
}
}
56 changes: 56 additions & 0 deletions Model/InstantPurchase/Card/TokenFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Model\InstantPurchase\Card;

use Adyen\Payment\Helper\Data;
use InvalidArgumentException;
use Magento\InstantPurchase\PaymentMethodIntegration\PaymentTokenFormatterInterface;
use Magento\Vault\Api\Data\PaymentTokenInterface;

/**
* Adyen stored card formatter.
*/
class TokenFormatter implements PaymentTokenFormatterInterface
{
public function __construct(
protected readonly Data $adyenHelper
) { }

public function formatPaymentToken(PaymentTokenInterface $paymentToken): string
{
$details = json_decode($paymentToken->getTokenDetails() ?: '{}', true);

if (!isset($details['type'], $details['maskedCC'], $details['expirationDate'])) {
throw new InvalidArgumentException('Invalid Adyen card token details.');
}

$ccTypes = $this->adyenHelper->getAdyenCcTypes();
$typeArrayIndex = array_search($details['type'], array_column($ccTypes, 'code_alt'));

if (is_int($typeArrayIndex)) {
$ccType = $ccTypes[array_keys($ccTypes)[$typeArrayIndex]]['name'];
} else {
$ccType = $details['type'];
}

return sprintf(
'%s: %s, %s: %s (%s: %s)',
__('Card'),
$ccType,
__('ending'),
$details['maskedCC'],
__('expires'),
$details['expirationDate']
);
}
}
62 changes: 62 additions & 0 deletions Model/InstantPurchase/PaymentMethods/AvailabilityChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Model\InstantPurchase\PaymentMethods;

use Adyen\Payment\Api\InstantPurchase\PaymentMethodIntegration\AdyenAvailabilityCheckerInterface;
use Adyen\Payment\Helper\Vault;
use Magento\Framework\Exception\NotFoundException;
use Magento\Store\Model\StoreManagerInterface;

class AvailabilityChecker implements AdyenAvailabilityCheckerInterface
{
/**
* @param Vault $vaultHelper
* @param StoreManagerInterface $storeManager
*/
public function __construct(
private readonly Vault $vaultHelper,
private readonly StoreManagerInterface $storeManager
) { }

/**
* Instant Purchase is available if payment method recurring is enabled and
* recurring processing model is set to `CardOnFile`.
*/
public function isAvailableAdyenMethod(string $paymentMethodCode): bool
{
$storeId = $this->storeManager->getStore()->getId();

$isMethodRecurringEnabled = $this->vaultHelper->getPaymentMethodRecurringActive(
$paymentMethodCode,
$storeId
);
$recurringProcessingModel = $this->vaultHelper->getPaymentMethodRecurringProcessingModel(
$paymentMethodCode,
$storeId
);

return $isMethodRecurringEnabled && $recurringProcessingModel === Vault::CARD_ON_FILE;
}

/**
* @throws NotFoundException
*/
public function isAvailable(): bool
{
/*
* This is the pseudo implementation of the interface. Actual logic has been written
* in `isAvailableAdyenMethod() and implemented via plugin `InstantPurchaseIntegrationTest`.
*/
throw new NotFoundException(__('This method has not been implemented!'));
}
}
28 changes: 28 additions & 0 deletions Model/InstantPurchase/PaymentMethods/TokenFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
*
* Adyen Payment Module
*
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
* Author: Adyen <[email protected]>
*/

namespace Adyen\Payment\Model\InstantPurchase\PaymentMethods;

use Magento\InstantPurchase\PaymentMethodIntegration\PaymentTokenFormatterInterface;
use Magento\Vault\Api\Data\PaymentTokenInterface;

/**
* Adyen stored payment method formatter.
*/
class TokenFormatter implements PaymentTokenFormatterInterface
{
public function formatPaymentToken(PaymentTokenInterface $paymentToken): string
{
$details = json_decode($paymentToken->getTokenDetails(), true);
return $details['tokenLabel'];
}
}
Loading

0 comments on commit 0ad2747

Please sign in to comment.