diff --git a/Gateway/Request/InstallmentOptionsDataBuilder.php b/Gateway/Request/InstallmentOptionsDataBuilder.php
new file mode 100644
index 000000000..82891589b
--- /dev/null
+++ b/Gateway/Request/InstallmentOptionsDataBuilder.php
@@ -0,0 +1,160 @@
+getOrder()->getGrandTotalAmount();
+
+ $storeId = $this->storeManager->getStore()->getId();
+
+ if (!$this->configHelper->getAdyenCcConfigData('enable_installments', $storeId)) {
+ return [];
+ }
+
+ $raw = $this->configHelper->getAdyenCcConfigData('installments', $storeId);
+ if (empty($raw)) {
+ return [];
+ }
+
+ $installmentsConfig = $this->serializer->unserialize($raw);
+
+ $installmentOptions = $this->formatInstallmentOptions($installmentsConfig, $orderAmount);
+
+ return empty($installmentOptions)
+ ? []
+ : ['body' => ['installmentOptions' => $installmentOptions]];
+ }
+
+ private function formatInstallmentOptions(array $config, float $orderAmount): array
+ {
+ $brandMap = $this->getBrandMapFromXml();
+ $result = [];
+
+ foreach ($config as $brandCode => $rules) {
+ $pm = $brandMap[$brandCode] ?? null;
+ if (!$pm || !is_array($rules)) {
+ continue;
+ }
+
+ $thresholds = $this->parseThresholds($rules);
+ if (!$thresholds) {
+ continue;
+ }
+
+ $values = $this->collectEligibleValues($thresholds, $orderAmount);
+
+ $values[] = 1;
+
+ $values = array_values(array_unique(array_filter($values, static fn($v) => $v > 0)));
+ sort($values, SORT_NUMERIC);
+
+ if ($values) {
+ $result[$pm] = ['values' => $values];
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Converts config rules into a sorted map: [minAmount(float) => values(int[])]
+ * Accepts common shapes:
+ * - [minAmount => [2,3]]
+ * - [minAmount => ['values' => [2,3]]]
+ * - [minAmount => 3]
+ */
+ private function parseThresholds(array $rules): array
+ {
+ $thresholds = [];
+
+ foreach ($rules as $minAmount => $installments) {
+ $values = $this->normalizeInstallmentValues($installments);
+ if (!$values) {
+ continue;
+ }
+
+ $thresholds[(float)$minAmount] = $values;
+ }
+
+ if (!$thresholds) {
+ return [];
+ }
+
+ ksort($thresholds, SORT_NUMERIC);
+ return $thresholds;
+ }
+
+ private function collectEligibleValues(array $thresholds, float $orderAmount): array
+ {
+ $values = [];
+
+ foreach ($thresholds as $minAmount => $tierValues) {
+ if ($orderAmount < (float)$minAmount) {
+ break;
+ }
+ // Append values from each eligible tier
+ $values = array_merge($values, $tierValues);
+ }
+
+ return $values;
+ }
+
+ private function normalizeInstallmentValues(mixed $installments): array
+ {
+ // If admin config stores something like ['values' => [2,3]]
+ if (is_array($installments) && isset($installments['values']) && is_array($installments['values'])) {
+ $installments = $installments['values'];
+ }
+
+ if (is_numeric($installments)) {
+ return [(int)$installments];
+ }
+
+ if (is_array($installments)) {
+ $out = [];
+ foreach ($installments as $v) {
+ if (is_numeric($v)) {
+ $out[] = (int)$v;
+ }
+ }
+ return $out;
+ }
+
+ return [];
+ }
+
+ private function getBrandMapFromXml(): array
+ {
+ $altData = $this->adyenHelper->getCcTypesAltData();
+
+ $map = [];
+ foreach ($altData as $altCode => $data) {
+ if (!empty($data['code'])) {
+ $map[$data['code']] = $altCode;
+ }
+ }
+
+ return $map;
+ }
+}
diff --git a/Test/Unit/Gateway/Request/InstallmentOptionsDataBuilderTest.php b/Test/Unit/Gateway/Request/InstallmentOptionsDataBuilderTest.php
new file mode 100644
index 000000000..d6924b887
--- /dev/null
+++ b/Test/Unit/Gateway/Request/InstallmentOptionsDataBuilderTest.php
@@ -0,0 +1,266 @@
+configHelper = $this->createMock(Config::class);
+ $this->serializer = $this->createMock(SerializerInterface::class);
+ $this->storeManager = $this->createMock(StoreManagerInterface::class);
+ $this->adyenHelper = $this->createMock(AdyenDataHelper::class);
+
+ $this->store = $this->createMock(StoreInterface::class);
+ $this->paymentDataObject = $this->createMock(PaymentDataObject::class);
+ $this->order = $this->createMock(OrderAdapterInterface::class);
+
+ $this->storeManager->method('getStore')->willReturn($this->store);
+ $this->store->method('getId')->willReturn(1);
+
+ $this->paymentDataObject->method('getOrder')->willReturn($this->order);
+
+ $this->subject = new InstallmentOptionsDataBuilder(
+ $this->configHelper,
+ $this->serializer,
+ $this->storeManager,
+ $this->adyenHelper
+ );
+ }
+
+ public function testBuildReturnsEmptyArrayWhenInstallmentsDisabled(): void
+ {
+ $this->order->method('getGrandTotalAmount')->willReturn(100.0);
+
+ $this->configHelper
+ ->expects($this->once())
+ ->method('getAdyenCcConfigData')
+ ->with('enable_installments', 1)
+ ->willReturn(false);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame([], $result);
+ }
+
+ public function testBuildReturnsEmptyArrayWhenInstallmentsConfigIsEmpty(): void
+ {
+ $this->order->method('getGrandTotalAmount')->willReturn(100.0);
+
+ $this->configHelper
+ ->method('getAdyenCcConfigData')
+ ->willReturnMap([
+ ['enable_installments', 1, true],
+ ['installments', 1, ''], // empty raw
+ ]);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame([], $result);
+ }
+
+ public function testBuildReturnsInstallmentOptionsForEligibleTiersAndAddsOne(): void
+ {
+ $this->order->method('getGrandTotalAmount')->willReturn(65.0);
+
+ $this->configHelper
+ ->method('getAdyenCcConfigData')
+ ->willReturnMap([
+ ['enable_installments', 1, true],
+ ['installments', 1, 'serialized-config'],
+ ]);
+
+ $this->adyenHelper
+ ->method('getCcTypesAltData')
+ ->willReturn([
+ 'visa' => ['code' => 'visa'],
+ 'mc' => ['code' => 'mc'],
+ ]);
+
+ $this->serializer
+ ->expects($this->once())
+ ->method('unserialize')
+ ->with('serialized-config')
+ ->willReturn([
+ // Eligible tiers for 65: >=20 and >=60, not >=100
+ 'visa' => [
+ 20 => [2],
+ 60 => [1],
+ 100 => [3],
+ ],
+ ]);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame(
+ [
+ 'body' => [
+ 'installmentOptions' => [
+ 'visa' => [
+ 'values' => [1, 2],
+ ],
+ ],
+ ],
+ ],
+ $result
+ );
+ }
+
+ public function testBuildSkipsUnknownBrandAndNonArrayRules(): void
+ {
+ $this->order->method('getGrandTotalAmount')->willReturn(200.0);
+
+ $this->configHelper
+ ->method('getAdyenCcConfigData')
+ ->willReturnMap([
+ ['enable_installments', 1, true],
+ ['installments', 1, 'raw'],
+ ]);
+
+ $this->adyenHelper
+ ->method('getCcTypesAltData')
+ ->willReturn([
+ 'visa' => ['code' => 'visa'],
+ ]);
+
+ $this->serializer
+ ->method('unserialize')
+ ->willReturn([
+ 'unknown_brand' => [20 => [2, 3]],
+ 'visa' => 'not-an-array',
+ ]);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame([], $result);
+ }
+
+ public function testBuildSupportsVariousInstallmentValueShapes(): void
+ {
+ $this->order->method('getGrandTotalAmount')->willReturn(250.0);
+
+ $this->configHelper
+ ->method('getAdyenCcConfigData')
+ ->willReturnMap([
+ ['enable_installments', 1, true],
+ ['installments', 1, 'raw'],
+ ]);
+
+ $this->adyenHelper
+ ->method('getCcTypesAltData')
+ ->willReturn([
+ 'mc' => ['code' => 'mc'],
+ ]);
+
+ $this->serializer
+ ->method('unserialize')
+ ->willReturn([
+ 'mc' => [
+ // scalar
+ 20 => 3,
+ // array with numeric + non-numeric
+ 60 => [2, 'x', 4],
+ // nested "values" shape
+ 100 => ['values' => [6, 8]],
+ // empty should be ignored
+ 120 => [],
+ ],
+ ]);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame(
+ [
+ 'body' => [
+ 'installmentOptions' => [
+ 'mc' => [
+ // eligible tiers are all (orderAmount=250)
+ // values merged: [3] + [2,4] + [6,8] + 1, sorted unique
+ 'values' => [1, 2, 3, 4, 6, 8],
+ ],
+ ],
+ ],
+ ],
+ $result
+ );
+ }
+
+ public function testBuildBreaksEarlyWhenOrderAmountBelowFirstThresholdAndReturnsOnlyOne(): void
+ {
+ // orderAmount is below the first minAmount (20)
+ $this->order->method('getGrandTotalAmount')->willReturn(10.0);
+
+ $this->configHelper
+ ->method('getAdyenCcConfigData')
+ ->willReturnMap([
+ ['enable_installments', 1, true],
+ ['installments', 1, 'raw'],
+ ]);
+
+ $this->adyenHelper
+ ->method('getCcTypesAltData')
+ ->willReturn([
+ 'visa' => ['code' => 'visa'],
+ ]);
+
+ $this->serializer
+ ->method('unserialize')
+ ->willReturn([
+ 'visa' => [
+ 20 => [2, 3],
+ 60 => [1],
+ ],
+ ]);
+
+ $result = $this->subject->build([
+ 'payment' => $this->paymentDataObject
+ ]);
+
+ $this->assertSame(
+ [
+ 'body' => [
+ 'installmentOptions' => [
+ 'visa' => [
+ 'values' => [1],
+ ],
+ ],
+ ],
+ ],
+ $result
+ );
+ }
+}
diff --git a/etc/di.xml b/etc/di.xml
index 2abaab82c..7104cc4ed 100755
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -1147,6 +1147,7 @@
- Adyen\Payment\Gateway\Request\PaymentDataBuilder
- Adyen\Payment\Gateway\Request\DescriptionDataBuilder
- Adyen\Payment\Gateway\Request\CheckoutDataBuilder
+ - Adyen\Payment\Gateway\Request\InstallmentOptionsDataBuilder
- Adyen\Payment\Gateway\Request\Header\HeaderDataBuilder
- Adyen\Payment\Gateway\Request\CompanyDataBuilder
- Adyen\Payment\Gateway\Request\LineItemsDataBuilder
@@ -1539,6 +1540,11 @@
Magento\Framework\Serialize\Serializer\Serialize
+
+
+ Magento\Framework\Serialize\Serializer\Serialize
+
+
Magento\Framework\Serialize\Serializer\Serialize