diff --git a/Block/Adminhtml/System/Config/Field/RefusalReasonMapping.php b/Block/Adminhtml/System/Config/Field/RefusalReasonMapping.php new file mode 100644 index 000000000..d6a3a4613 --- /dev/null +++ b/Block/Adminhtml/System/Config/Field/RefusalReasonMapping.php @@ -0,0 +1,82 @@ + + */ + +namespace Adyen\Payment\Block\Adminhtml\System\Config\Field; + +use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray; +use Magento\Framework\DataObject; + +/** + * Class RefusalReasonMapping + * + * @package Adyen\Payment\Block\Adminhtml\System\Config\Field + */ +class RefusalReasonMapping extends AbstractFieldArray +{ + protected ?RefusalReasonSelect $refusalReasonSelect = null; + + /** + * @inheritDoc + * @throws \Exception + */ + protected function _prepareToRender(): void + { + $this->addColumn( + 'refusal_reason', + ['label' => __('Refusal Reason'), 'renderer' => $this->getRefusalReasonSelect()] + ); + + $this->addColumn( + 'value', + ['label' => __('Value')] + ); + + $this->_addAfter = false; + $this->_addButtonLabel = __('Add Refusal Reason'); + parent::_prepareToRender(); + } + + /** + * @throws \Exception + * @return RefusalReasonSelect + */ + private function getRefusalReasonSelect(): RefusalReasonSelect + { + if (!$this->refusalReasonSelect instanceof RefusalReasonSelect) { + /** @var RefusalReasonSelect $countriesRenderer */ + $select = $this->getLayout()->createBlock( + RefusalReasonSelect::class, + '', + ['data' => ['is_render_to_js_template' => true]] + ); + + $this->refusalReasonSelect = $select; + } + + return $this->refusalReasonSelect; + } + + /** + * @inheritDoc + * @throws \Exception + */ + protected function _prepareArrayRow(DataObject $row): void + { + $options = []; + $refusalReason = $row->getData('refusal_reason'); + if (is_string($refusalReason) && $refusalReason !== '') { + $options['option_' . $this->getRefusalReasonSelect()->calcOptionHash($refusalReason)] + = 'selected="selected"'; + } + + $row->setData('option_extra_attrs', $options); + } +} diff --git a/Block/Adminhtml/System/Config/Field/RefusalReasonSelect.php b/Block/Adminhtml/System/Config/Field/RefusalReasonSelect.php new file mode 100644 index 000000000..36b1508be --- /dev/null +++ b/Block/Adminhtml/System/Config/Field/RefusalReasonSelect.php @@ -0,0 +1,58 @@ + + */ + +namespace Adyen\Payment\Block\Adminhtml\System\Config\Field; + +use Magento\Framework\View\Element\Html\Select; +use Adyen\Payment\Enum\AdyenRefusalReason; + +/** + * Class RefusalReasonSelect + * + * @package Adyen\Payment\Block\Adminhtml\System\Config\Field + */ +class RefusalReasonSelect extends Select +{ + /** + * @param string $value + * @return self + */ + public function setInputName(string $value): self + { + return $this->setData('name', $value); + } + + /** + * @param $value + * @return self + */ + public function setInputId($value): self + { + return $this->setId($value); + } + + /** + * @inheritDoc + */ + public function _toHtml(): string + { + if (!$this->getOptions()) { + $options = array_map(fn (AdyenRefusalReason $reason) => [ + 'label' => $reason->getLabel(), + 'value' => $reason->value, + ], AdyenRefusalReason::cases()); + + $this->setOptions($options); + } + + return parent::_toHtml(); + } +} diff --git a/Enum/AdyenRefusalReason.php b/Enum/AdyenRefusalReason.php new file mode 100644 index 000000000..175408b99 --- /dev/null +++ b/Enum/AdyenRefusalReason.php @@ -0,0 +1,121 @@ + + */ + +namespace Adyen\Payment\Enum; + +/** + * Class AdyenRefusalReason + * + * @package Adyen\Payment\Enum + * @see https://docs.adyen.com/development-resources/refusal-reasons/ + */ +enum AdyenRefusalReason: int +{ + case None = 0; + case Refused = 2; + case Referral = 3; + case AcquirerError = 4; + case BlockedCard = 5; + case ExpiredCard = 6; + case InvalidAmount = 7; + case InvalidCardNumber = 8; + case IssuerUnavailable = 9; + case NotSupported = 10; + case NotAuthenticated3D = 11; + case NotEnoughBalance = 12; + case AcquirerFraud = 14; + case Cancelled = 15; + case ShopperCancelled = 16; + case InvalidPin = 17; + case PinTriesExceeded = 18; + case PinValidationNotPossible = 19; + case Fraud = 20; + case NotSubmitted = 21; + case FraudCancelled = 22; + case TransactionNotPermitted = 23; + case CVCDeclined = 24; + case RestrictedCard = 25; + case RevocationOfAuth = 26; + case DeclinedNonGeneric = 27; + case WithdrawalAmountExceeded = 28; + case WithdrawalCountExceeded = 29; + case IssuerSuspectedFraud = 31; + case AVSDeclined = 32; + case CardRequiresOnlinePin = 33; + case NoCheckingAccountAvailableOnCard = 34; + case NoSavingsAccountAvailableOnCard = 35; + case MobilePINRequired = 36; + case ContactlessFallback = 37; + case AuthenticationRequired = 38; + case RReqNotReceivedFromDS = 39; + case CurrentAIDIsInPenaltyBox = 40; + case CVMRequiredRestartPayment = 41; + case AuthenticationError3DS = 42; + case OnlinePINRequired = 43; + case TryAnotherInterface = 44; + case ChipDowngradeMode = 45; + case TransactionBlockedByAdyen = 46; + + /** + * @return string + */ + public function getLabel(): string + { + $label = match ($this) { + self::None => __('Unknown'), + self::Refused => __('Refused'), + self::Referral => __('Referral'), + self::AcquirerError => __('Acquirer error'), + self::BlockedCard => __('Blocked card'), + self::ExpiredCard => __('Expired card'), + self::InvalidAmount => __('Invalid amount'), + self::InvalidCardNumber => __('Invalid card number'), + self::IssuerUnavailable => __('Issuer unavailable'), + self::NotSupported => __('Not supported'), + self::NotAuthenticated3D => __('3D Not Authenticated'), + self::NotEnoughBalance => __('Not enough balance'), + self::AcquirerFraud => __('Acquirer Fraud'), + self::Cancelled => __('Cancelled'), + self::ShopperCancelled => __('Shopper cancelled'), + self::InvalidPin => __('Invalid PIN'), + self::PinTriesExceeded => __('PIN tries exceeded'), + self::PinValidationNotPossible => __('PIN validation not possible'), + self::Fraud => __('Fraud'), + self::NotSubmitted => __('Not submitted'), + self::FraudCancelled => __('FRAUD-CANCELLED'), + self::TransactionNotPermitted => __('Transaction not permitted'), + self::CVCDeclined => __('CVC Declined'), + self::RestrictedCard => __('Restricted card'), + self::RevocationOfAuth => __('Revocation of auth'), + self::DeclinedNonGeneric => __('Declined non-generic'), + self::WithdrawalAmountExceeded => __('Withdrawal amount exceeded'), + self::WithdrawalCountExceeded => __('Withdrawal count exceeded'), + self::IssuerSuspectedFraud => __('Issuer Suspected Fraud'), + self::AVSDeclined => __('AVS Declined'), + self::CardRequiresOnlinePin => __('Card requires online PIN'), + self::NoCheckingAccountAvailableOnCard => __('No checking account available on card'), + self::NoSavingsAccountAvailableOnCard => __('No savings account available on card'), + self::MobilePINRequired => __('Mobile PIN required'), + self::ContactlessFallback => __('Contactless fallback'), + self::AuthenticationRequired => __('Authentication required'), + self::RReqNotReceivedFromDS => __('RReq not received from DS'), + self::CurrentAIDIsInPenaltyBox => __('Current AID is in penalty box'), + self::CVMRequiredRestartPayment => __('CVM required restart payment'), + self::AuthenticationError3DS => __('3DS Authentication Error'), + self::OnlinePINRequired => __('Online PIN required'), + self::TryAnotherInterface => __('Try another interface'), + self::ChipDowngradeMode => __('Chip downgrade mode'), + self::TransactionBlockedByAdyen => __('Transaction blocked by Adyen to prevent excessive retry fees'), + }; + + return sprintf('%s - %s', $this->value, $label); + } +} diff --git a/Enum/CallbackOrderProperty.php b/Enum/CallbackOrderProperty.php new file mode 100644 index 000000000..174ba6d32 --- /dev/null +++ b/Enum/CallbackOrderProperty.php @@ -0,0 +1,50 @@ + + */ + +namespace Adyen\Payment\Enum; + +use Magento\Framework\Phrase; + +/** + * Class CallbackOrderProperty + * + * @package Adyen\Payment\Enum + */ +enum CallbackOrderProperty: string +{ + case ShippingFirstName = 'shipping.firstname'; + case ShippingLastName = 'shipping.lastname'; + case ShippingPostCode = 'shipping.postcode'; + case ShippingTelephone = 'shipping.telephone'; + case BillingFirstName = 'billing.firstname'; + case BillingLastName = 'billing.lastname'; + case BillingPostCode = 'billing.postcode'; + case BillingTelephone = 'billing.telephone'; + case CustomerEmail = 'customer_email'; + + /** + * @return Phrase + */ + public function getLabel(): Phrase + { + return match ($this) { + self::ShippingFirstName => __('Shipping first name'), + self::ShippingLastName => __('Shipping last name'), + self::ShippingPostCode => __('Shipping postcode'), + self::ShippingTelephone => __('Shipping telephone'), + self::BillingFirstName => __('Billing first name'), + self::BillingLastName => __('Billing last name'), + self::BillingPostCode => __('Billing postcode'), + self::BillingTelephone => __('Billing telephone'), + self::CustomerEmail => __('Customer email'), + }; + } +} diff --git a/Gateway/Request/TestingRefusalReasonBuilder.php b/Gateway/Request/TestingRefusalReasonBuilder.php new file mode 100644 index 000000000..502cb9f1a --- /dev/null +++ b/Gateway/Request/TestingRefusalReasonBuilder.php @@ -0,0 +1,69 @@ + + */ + +namespace Adyen\Payment\Gateway\Request; + +use Adyen\Payment\Helper\Config; +use Magento\Payment\Gateway\Helper\SubjectReader; +use Magento\Payment\Gateway\Request\BuilderInterface; +use Magento\Sales\Model\Order; +use Adyen\Payment\Enum\AdyenRefusalReason; +use Adyen\Payment\Model\TestingRefusalReason; + +/** + * Class TestingRefusalReasonBuilder + * + * @package Adyen\Payment\Gateway\Request + * https://docs.adyen.com/development-resources/testing/result-codes/?tab=request_using_holder_name_0_1 + */ +class TestingRefusalReasonBuilder implements BuilderInterface +{ + /** + * TestingRefusalReasonBuilder Constructor + * + * @param Config $config + * @param TestingRefusalReason $testingRefusalReason + */ + public function __construct( + protected Config $config, + protected TestingRefusalReason $testingRefusalReason + ) { + } + + /** + * @inheritDoc + */ + public function build(array $buildSubject): array + { + $paymentDataObject = SubjectReader::readPayment($buildSubject); + $payment = $paymentDataObject->getPayment(); + /** @var Order $order */ + $order = $payment->getOrder(); + $storeId = $order->getStoreId(); + + if (!$this->config->isDemoMode($storeId)) { + return []; + } + + $reason = $this->testingRefusalReason->findRefusalReason($order); + if (!$reason instanceof AdyenRefusalReason) { + return []; + } + + return [ + 'body' => [ + 'additionalData' => [ + 'RequestedTestAcquirerResponseCode' => $reason->value, + ], + ], + ]; + } +} diff --git a/Helper/Config/Testing.php b/Helper/Config/Testing.php new file mode 100644 index 000000000..318429172 --- /dev/null +++ b/Helper/Config/Testing.php @@ -0,0 +1,102 @@ + + */ + +namespace Adyen\Payment\Helper\Config; + +use Adyen\Payment\Helper\Config; +use Magento\Framework\Serialize\Serializer\Json; +use Adyen\Payment\Enum\AdyenRefusalReason; +use Adyen\Payment\Enum\CallbackOrderProperty; + +/** + * Class Testing + * + * @package Adyen\Payment\Helper\Config + */ +class Testing +{ + const XML_REFUSAL_REASON_VALUE_SOURCE = 'testing_refusal_reason_value_source'; + const XML_REFUSAL_REASON_MAPPING = 'testing_refusal_reason_mapping'; + + /** + * Testing Constructor + * + * @param Config $config + * @param Json $serializer + */ + public function __construct( + protected Config $config, + protected Json $serializer, + ) { + } + + /** + * @param int|null $storeId + * @return CallbackOrderProperty + */ + public function getRefusalReasonValueSource(?int $storeId = null): CallbackOrderProperty + { + $value = $this->config->getConfigData( + self::XML_REFUSAL_REASON_VALUE_SOURCE, + $this->config::XML_ADYEN_ABSTRACT_PREFIX, + $storeId + ); + + $callback = null; + if (is_string($value) && !empty($value)) { + $callback = CallbackOrderProperty::tryFrom($value); + } + + if (!$callback instanceof CallbackOrderProperty) { + $callback = CallbackOrderProperty::ShippingLastName; + } + + return $callback; + } + + /** + * @param int|null $storeId + * @return array + */ + public function getRefusalReasonMapping(?int $storeId = null): array + { + $value = $this->config->getConfigData( + self::XML_REFUSAL_REASON_MAPPING, + $this->config::XML_ADYEN_ABSTRACT_PREFIX, + $storeId + ); + + if (!is_string($value) || $value === '') { + return []; + } + + $rows = $this->serializer->unserialize($value); + $mapping = []; + + foreach ($rows as $row) { + $value = $row['value'] ?? null; + $reason = $row['refusal_reason'] ?? null; + + if (!$value || !is_numeric($reason)) { + continue; + } + + $reason = AdyenRefusalReason::tryFrom((int)$reason); + if (!$reason instanceof AdyenRefusalReason) { + continue; + } + + $mapping[$value] = $reason; + } + + return $mapping; + } +} diff --git a/Model/Config/Source/OrderCallbackProperty.php b/Model/Config/Source/OrderCallbackProperty.php new file mode 100644 index 000000000..8cf1f8116 --- /dev/null +++ b/Model/Config/Source/OrderCallbackProperty.php @@ -0,0 +1,34 @@ + + */ + +namespace Adyen\Payment\Model\Config\Source; + +use Magento\Framework\Data\OptionSourceInterface; +use Adyen\Payment\Enum\CallbackOrderProperty; + +/** + * Class OrderCallbackProperty + * + * @package Adyen\Payment\Model\Config\Source + */ +class OrderCallbackProperty implements OptionSourceInterface +{ + /** + * @inheritDoc + */ + public function toOptionArray(): array + { + return array_map(fn (CallbackOrderProperty $property) => [ + 'value' => $property->value, + 'label' => $property->getLabel(), + ], CallbackOrderProperty::cases()); + } +} diff --git a/Model/TestingRefusalReason.php b/Model/TestingRefusalReason.php new file mode 100644 index 000000000..e1e78b5bd --- /dev/null +++ b/Model/TestingRefusalReason.php @@ -0,0 +1,84 @@ + + */ + +namespace Adyen\Payment\Model; + +use Magento\Framework\DataObject; +use Magento\Sales\Model\Order; +use Adyen\Payment\Enum\AdyenRefusalReason; +use Adyen\Payment\Enum\CallbackOrderProperty; +use Adyen\Payment\Helper\Config\Testing; + +/** + * Class TestingRefusalReason + * + * @package Adyen\Payment\Model + */ +class TestingRefusalReason +{ + /** + * TestingRefusalReason Constructor + * + * @param Testing $testingConfig + */ + public function __construct( + protected Testing $testingConfig, + ) { + } + + /** + * @param Order $order + * @return AdyenRefusalReason|null + */ + public function findRefusalReason(Order $order): ?AdyenRefusalReason + { + $storeId = (int)$order->getStoreId(); + $source = $this->testingConfig->getRefusalReasonValueSource($storeId); + $value = $this->getSourceValue($order, $source); + if ($value === null || $value === '') { + return null; + } + + $mapping = $this->testingConfig->getRefusalReasonMapping($storeId); + return $mapping[$value] ?? null; + } + + /** + * @param Order $order + * @param CallbackOrderProperty $source + * @return mixed + */ + protected function getSourceValue(Order $order, CallbackOrderProperty $source): mixed + { + $parts = explode('.', $source->value); + if (count($parts) === 1) { + return $order->getData($source->value); + } + + if (count($parts) !== 2) { + throw new \InvalidArgumentException('Invalid source value'); + } + + $root = $parts[0]; + $key = $parts[1]; + + $value = match ($root) { + 'shipping' => $order->getShippingAddress(), + 'billing' => $order->getBillingAddress(), + }; + + if (!$value instanceof DataObject) { + return null; + } + + return $value->getData($key); + } +} diff --git a/Test/Unit/Gateway/Request/TestingRefusalReasonBuilderTest.php b/Test/Unit/Gateway/Request/TestingRefusalReasonBuilderTest.php new file mode 100644 index 000000000..c52377c90 --- /dev/null +++ b/Test/Unit/Gateway/Request/TestingRefusalReasonBuilderTest.php @@ -0,0 +1,156 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Gateway\Request; + +use Adyen\Payment\Helper\Config; +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Payment\Gateway\Data\PaymentDataObjectInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Payment; +use Adyen\Payment\Enum\AdyenRefusalReason; +use Adyen\Payment\Gateway\Request\TestingRefusalReasonBuilder; +use Adyen\Payment\Model\TestingRefusalReason; + +/** + * Class TestingRefusalReasonBuilderTest + * + * @package Adyen\Payment\Test\Unit\Gateway\Request + * @coversDefaultClass \Adyen\Payment\Gateway\Request\TestingRefusalReasonBuilder + */ +final class TestingRefusalReasonBuilderTest extends AbstractAdyenTestCase +{ + private Config $configMock; + private TestingRefusalReason $testingRefusalReasonMock; + private TestingRefusalReasonBuilder $builder; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->configMock = $this->createMock(Config::class); + $this->testingRefusalReasonMock = $this->createMock(TestingRefusalReason::class); + + $this->builder = new TestingRefusalReasonBuilder( + $this->configMock, + $this->testingRefusalReasonMock + ); + } + + /** + * @return void + * @covers ::build() + */ + public function testBuildReturnsEmptyArrayWhenNotInDemoMode(): void + { + $storeId = 1; + [$paymentDO] = $this->createPaymentSubject($storeId); + + $this->configMock + ->expects($this->once()) + ->method('isDemoMode') + ->with($storeId) + ->willReturn(false); + + $this->testingRefusalReasonMock + ->expects($this->never()) + ->method('findRefusalReason'); + + $result = $this->builder->build(['payment' => $paymentDO]); + $this->assertSame([], $result); + } + + /** + * @return void + * @covers ::build() + */ + public function testBuildReturnsEmptyArrayWhenNoReasonFound(): void + { + $storeId = 1; + [$paymentDO, $order] = $this->createPaymentSubject($storeId); + + $this->configMock + ->expects($this->once()) + ->method('isDemoMode') + ->with($storeId) + ->willReturn(true); + + $this->testingRefusalReasonMock + ->expects($this->once()) + ->method('findRefusalReason') + ->with($order) + ->willReturn(null); + + $result = $this->builder->build(['payment' => $paymentDO]); + $this->assertSame([], $result); + } + + /** + * @return void + * @covers ::build() + */ + public function testBuildReturnsAdditionalDataWhenReasonFound(): void + { + $storeId = 1; + [$paymentDO, $order] = $this->createPaymentSubject($storeId); + + $this->configMock + ->expects($this->once()) + ->method('isDemoMode') + ->with($storeId) + ->willReturn(true); + + // Use the first available enum case to avoid hardcoding a specific case name. + $reason = AdyenRefusalReason::cases()[0]; + + $this->testingRefusalReasonMock + ->expects($this->once()) + ->method('findRefusalReason') + ->with($order) + ->willReturn($reason); + + $result = $this->builder->build(['payment' => $paymentDO]); + + $expected = [ + 'body' => [ + 'additionalData' => [ + 'RequestedTestAcquirerResponseCode' => $reason->value, + ], + ], + ]; + + $this->assertSame($expected, $result); + } + + /** + * @return array{0: PaymentDataObjectInterface, 1: Order} + */ + private function createPaymentSubject(int $storeId): array + { + $order = $this->getMockBuilder(Order::class) + ->disableOriginalConstructor() + ->onlyMethods(['getStoreId']) + ->getMock(); + $order->method('getStoreId')->willReturn($storeId); + + $payment = $this->getMockBuilder(Payment::class) + ->disableOriginalConstructor() + ->onlyMethods(['getOrder']) + ->getMock(); + $payment->method('getOrder')->willReturn($order); + + $paymentDO = $this->createMock(PaymentDataObjectInterface::class); + $paymentDO->method('getPayment')->willReturn($payment); + + return [$paymentDO, $order]; + } +} diff --git a/Test/Unit/Model/TestingRefusalReasonTest.php b/Test/Unit/Model/TestingRefusalReasonTest.php new file mode 100644 index 000000000..7c118d401 --- /dev/null +++ b/Test/Unit/Model/TestingRefusalReasonTest.php @@ -0,0 +1,165 @@ + + */ + +namespace Adyen\Payment\Test\Unit\Model; + +use Adyen\Payment\Test\Unit\AbstractAdyenTestCase; +use Magento\Framework\DataObject; +use Magento\Sales\Model\Order; +use Adyen\Payment\Enum\AdyenRefusalReason; +use Adyen\Payment\Enum\CallbackOrderProperty; +use Adyen\Payment\Helper\Config\Testing; +use Adyen\Payment\Model\TestingRefusalReason; + +/** + * Class TestingRefusalReasonTest + * + * @package Adyen\Payment\Test\Unit\Model + * @coversDefaultClass \Adyen\Payment\Model\TestingRefusalReason + */ +class TestingRefusalReasonTest extends AbstractAdyeNTestCase +{ + private Testing $testingConfigMock; + private TestingRefusalReason $subject; + private int $storeId = 1; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->testingConfigMock = $this->createMock(Testing::class); + $this->subject = new TestingRefusalReason($this->testingConfigMock); + } + + /** + * @param DataObject|null $shippingAddress + * @return Order + */ + private function createOrderWithShippingAddress(?DataObject $shippingAddress): Order + { + $order = $this->createMock(Order::class); + $order->method('getStoreId')->willReturn($this->storeId); + $order->method('getShippingAddress')->willReturn($shippingAddress); + return $order; + } + + /** + * @return void + * @covers ::findRefusalReason() + */ + public function testReturnsMappedEnumWhenShippingValueMatches(): void + { + $source = CallbackOrderProperty::ShippingLastName; + + $parts = explode('.', $source->value); + $this->assertCount(2, $parts, 'Expected a two-part source like "shipping.key"'); + $key = $parts[1]; + + $inputValue = 'Smith'; + $expectedReason = AdyenRefusalReason::cases()[0]; + + $this->testingConfigMock + ->method('getRefusalReasonValueSource') + ->with($this->storeId) + ->willReturn($source); + + $this->testingConfigMock + ->method('getRefusalReasonMapping') + ->with($this->storeId) + ->willReturn([$inputValue => $expectedReason]); + + $shipping = new DataObject([$key => $inputValue]); + $order = $this->createOrderWithShippingAddress($shipping); + + $actual = $this->subject->findRefusalReason($order); + + $this->assertSame($expectedReason, $actual); + } + + /** + * @return void + * @covers ::findRefusalReason() + */ + public function testReturnsNullWhenValueIsMissing(): void + { + $source = CallbackOrderProperty::ShippingLastName; + + $parts = explode('.', $source->value); + $this->assertCount(2, $parts, 'Expected a two-part source like "shipping.key"'); + + $this->testingConfigMock + ->method('getRefusalReasonValueSource') + ->with($this->storeId) + ->willReturn($source); + + $this->testingConfigMock + ->expects($this->never()) + ->method('getRefusalReasonMapping') + ->with($this->storeId); + + $shipping = new DataObject(); // No key set => null/empty + $order = $this->createOrderWithShippingAddress($shipping); + + $this->assertNull($this->subject->findRefusalReason($order)); + } + + /** + * @return void + * @covers ::findRefusalReason() + */ + public function testReturnsNullWhenMappingHasNoMatch(): void + { + $source = CallbackOrderProperty::ShippingLastName; + + $parts = explode('.', $source->value); + $this->assertCount(2, $parts, 'Expected a two-part source like "shipping.key"'); + $key = $parts[1]; + + $this->testingConfigMock + ->method('getRefusalReasonValueSource') + ->with($this->storeId) + ->willReturn($source); + + $this->testingConfigMock + ->method('getRefusalReasonMapping') + ->with($this->storeId) + ->willReturn(['MappedValue' => AdyenRefusalReason::cases()[0]]); + + $shipping = new DataObject([$key => 'UnmappedValue']); + $order = $this->createOrderWithShippingAddress($shipping); + + $this->assertNull($this->subject->findRefusalReason($order)); + } + + /** + * @return void + * @covers ::findRefusalReason() + */ + public function testReturnsNullWhenShippingAddressIsNull(): void + { + $source = CallbackOrderProperty::ShippingLastName; + + $this->testingConfigMock + ->method('getRefusalReasonValueSource') + ->with($this->storeId) + ->willReturn($source); + + $this->testingConfigMock + ->expects($this->never()) + ->method('getRefusalReasonMapping') + ->with($this->storeId); + + $order = $this->createOrderWithShippingAddress(null); + + $this->assertNull($this->subject->findRefusalReason($order)); + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 07a43cf8e..bcd705a3d 100755 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -27,6 +27,7 @@ + diff --git a/etc/adminhtml/system/adyen_testing_refusal_reason.xml b/etc/adminhtml/system/adyen_testing_refusal_reason.xml new file mode 100644 index 000000000..5504a409f --- /dev/null +++ b/etc/adminhtml/system/adyen_testing_refusal_reason.xml @@ -0,0 +1,50 @@ + + + + + + + Magento\Config\Block\System\Config\Form\Fieldset + + Adyen documentation. This feature is available only on testing environment. + ]]> + + + + + payment/adyen_abstract/testing_refusal_reason_value_source + Adyen\Payment\Model\Config\Source\OrderCallbackProperty + + + + Magento\Config\Model\Config\Backend\Serialized\ArraySerialized + Adyen\Payment\Block\Adminhtml\System\Config\Field\RefusalReasonMapping + payment/adyen_abstract/testing_refusal_reason_mapping + + + + diff --git a/etc/di.xml b/etc/di.xml index f7cf4e990..b38f71f81 100755 --- a/etc/di.xml +++ b/etc/di.xml @@ -1115,6 +1115,7 @@ Adyen\Payment\Gateway\Request\GiftcardDataBuilder Adyen\Payment\Gateway\Request\CompanyDataBuilder Adyen\Payment\Gateway\Request\MerchantRiskIndicatorDataBuilder + Adyen\Payment\Gateway\Request\TestingRefusalReasonBuilder @@ -1244,6 +1245,7 @@ Adyen\Payment\Gateway\Request\CompanyDataBuilder Adyen\Payment\Gateway\Request\LineItemsDataBuilder Adyen\Payment\Gateway\Request\MerchantRiskIndicatorDataBuilder + Adyen\Payment\Gateway\Request\TestingRefusalReasonBuilder