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