From 9451990434d483fac9050c172ba9af9a3fb55e40 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 13 Oct 2026 18:02:49 +0200 Subject: [PATCH] feat(payments): add fallback_source to the payment request POST /payments accepts fallback_source, an alternative card source to attempt when the primary source fails. PaymentRequest now exposes it as fallbackSource, typed as RequestCardSource. RequestCardSource gains allowUpdate, which the spec defines on both the primary card source and the fallback source, and its doc comments are rewritten from the spec. PaymentRequest documents the amount range and the deprecated payment_ip and marketplace fields. --- .../payments/request/PaymentRequest.java | 16 +- .../request/source/RequestCardSource.java | 40 +++-- .../PaymentRequestSerializationTest.java | 145 ++++++++++++++++++ 3 files changed, 187 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/checkout/payments/request/PaymentRequest.java b/src/main/java/com/checkout/payments/request/PaymentRequest.java index 7bd5158f0..78913fcb9 100644 --- a/src/main/java/com/checkout/payments/request/PaymentRequest.java +++ b/src/main/java/com/checkout/payments/request/PaymentRequest.java @@ -14,6 +14,7 @@ import com.checkout.payments.ShippingDetails; import com.checkout.payments.ThreeDSRequest; import com.checkout.payments.request.source.AbstractRequestSource; +import com.checkout.payments.request.source.RequestCardSource; import com.checkout.payments.PaymentPlan; import com.checkout.payments.PaymentRouting; import com.checkout.payments.PaymentSubscription; @@ -43,11 +44,17 @@ public final class PaymentRequest { */ private AbstractRequestSource source; + /** + * An alternative card source to attempt if the payment using the primary source fails. + * [Optional] + */ + private RequestCardSource fallbackSource; + /** * The payment amount in minor currency units. To perform a card verification, do not provide * the amount or provide a value of 0. * [Optional] - * min 0 + * min 0, max 9999999999 */ private Long amount; @@ -197,6 +204,11 @@ public final class PaymentRequest { private String failureUrl; /** + * The IP address used to make the payment. Used by Checkout.com's risk engine to check the customer's + * IP address. Only accepts IPv4 and IPv6 addresses. Required if source.type is sequra. + * [Optional] + * Format: ipv4 + * max 45 characters * @deprecated Deprecated in the API on 2025-03-11. Use {@code risk.device.network.ipv4} or {@code risk.device.network.ipv6} instead. */ @Deprecated @@ -215,6 +227,8 @@ public final class PaymentRequest { private PaymentRecipient recipient; /** + * The marketplace data for the payment. No longer in the API specification. + * [Optional] * @deprecated Use {@link PaymentRequest#amountAllocations} instead. */ @Deprecated diff --git a/src/main/java/com/checkout/payments/request/source/RequestCardSource.java b/src/main/java/com/checkout/payments/request/source/RequestCardSource.java index d3ab99301..323c2333f 100644 --- a/src/main/java/com/checkout/payments/request/source/RequestCardSource.java +++ b/src/main/java/com/checkout/payments/request/source/RequestCardSource.java @@ -17,65 +17,77 @@ public final class RequestCardSource extends AbstractRequestSource { /** - * The card number (without separators). Write-only. - * [Optional] + * The card number (without separators). + * [Required] */ private String number; /** * The expiry month of the card. - * [Optional] + * [Required] + * min 1, max 12 */ private Integer expiryMonth; /** - * The expiry year of the card. - * [Optional] + * 4-digit expiry year of the card. + * [Required] */ private Integer expiryYear; /** - * The cardholder's name. Write-only. + * The name of the cardholder. * [Optional] */ private String name; /** - * The card verification value/code. 3 digits, except for American Express (4 digits). Write-only. + * The card verification value/code. 3 digits, except for American Express (4 digits). * [Optional] + * min 3 characters, max 4 characters */ private String cvv; /** - * Set to true for payments that use stored card details. Write-only. + * Indicates whether this card is being submitted from your own stored card-on-file system, rather than + * being entered by the customer at the time of payment. Set to true when you provide card details that + * you already have on file for the customer. * [Optional] */ private Boolean stored; /** - * Set to true if you intend to reuse the payment credentials in subsequent payments. Write-only. + * This must be set to true if you intend to reuse the payment credentials in subsequent payments. + * Setting the field as false will mean that a payment instrument will not be included in the payment + * response. * [Optional] */ private Boolean storeForFutureUse; /** - * The payment source owner's billing address. + * The billing address of the cardholder. * [Optional] */ private Address billingAddress; /** - * The payment source owner's phone number. + * The phone number of the cardholder. * [Optional] */ private Phone phone; /** - * The card account holder's details. + * Information about the account holder of the card. * [Optional] */ private AccountHolder accountHolder; + /** + * Specifies whether to use the Real-Time Account Updater to update the card information. + * [Optional] + */ + private Boolean allowUpdate; + @Builder private RequestCardSource(final String number, final Integer expiryMonth, @@ -86,7 +98,8 @@ private RequestCardSource(final String number, final Boolean storeForFutureUse, final Address billingAddress, final Phone phone, - final AccountHolder accountHolder) { + final AccountHolder accountHolder, + final Boolean allowUpdate) { super(PaymentSourceType.CARD); this.number = number; this.expiryMonth = expiryMonth; @@ -98,6 +111,7 @@ private RequestCardSource(final String number, this.billingAddress = billingAddress; this.phone = phone; this.accountHolder = accountHolder; + this.allowUpdate = allowUpdate; } public RequestCardSource() { diff --git a/src/test/java/com/checkout/payments/request/PaymentRequestSerializationTest.java b/src/test/java/com/checkout/payments/request/PaymentRequestSerializationTest.java index 3e84216d5..6173764a0 100644 --- a/src/test/java/com/checkout/payments/request/PaymentRequestSerializationTest.java +++ b/src/test/java/com/checkout/payments/request/PaymentRequestSerializationTest.java @@ -1,9 +1,18 @@ package com.checkout.payments.request; import com.checkout.GsonSerializer; +import com.checkout.common.AccountHolder; +import com.checkout.common.AccountHolderType; +import com.checkout.common.Address; +import com.checkout.common.CountryCode; import com.checkout.common.Currency; +import com.checkout.common.PaymentSourceType; +import com.checkout.common.Phone; import com.checkout.payments.AuthorizationType; import com.checkout.payments.PaymentType; +import com.checkout.payments.request.source.RequestCardSource; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -253,4 +262,140 @@ void shouldHandleAbsentOptionalFields() { assertNull(request.getMerchantInitiated()); assertNull(request.getPreviousPaymentId()); } + + // ------------------------------------------------------------------------ + // fallback_source + // Covers all 12 properties of the PaymentRequestFallbackSource schema. + // ------------------------------------------------------------------------ + + @Test + void shouldSerializeFallbackSourceWithAllProperties() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .fallbackSource(buildFallbackSource()) + .build(); + + final JsonObject json = JsonParser.parseString(serializer.toJson(request)).getAsJsonObject(); + + assertTrue(json.has("fallback_source")); + final JsonObject fallbackSource = json.getAsJsonObject("fallback_source"); + assertEquals(12, fallbackSource.size()); + assertEquals("card", fallbackSource.get("type").getAsString()); + assertEquals("4543474002249996", fallbackSource.get("number").getAsString()); + assertEquals(6, fallbackSource.get("expiry_month").getAsInt()); + assertEquals(2030, fallbackSource.get("expiry_year").getAsInt()); + assertEquals("Bruce Wayne", fallbackSource.get("name").getAsString()); + assertEquals("956", fallbackSource.get("cvv").getAsString()); + assertTrue(fallbackSource.get("stored").getAsBoolean()); + assertFalse(fallbackSource.get("store_for_future_use").getAsBoolean()); + assertFalse(fallbackSource.get("allow_update").getAsBoolean()); + assertEquals("London", fallbackSource.getAsJsonObject("billing_address").get("city").getAsString()); + assertEquals("44", fallbackSource.getAsJsonObject("phone").get("country_code").getAsString()); + assertEquals("individual", fallbackSource.getAsJsonObject("account_holder").get("type").getAsString()); + assertEquals("Bruce", fallbackSource.getAsJsonObject("account_holder").get("first_name").getAsString()); + } + + @Test + void shouldRoundTripFallbackSource() { + final PaymentRequest original = PaymentRequest.builder() + .currency(Currency.USD) + .fallbackSource(buildFallbackSource()) + .build(); + + final String json = serializer.toJson(original); + final PaymentRequest deserialized = serializer.fromJson(json, PaymentRequest.class); + + assertNotNull(deserialized.getFallbackSource()); + assertEquals(PaymentSourceType.CARD, deserialized.getFallbackSource().getType()); + assertEquals(original.getFallbackSource(), deserialized.getFallbackSource()); + } + + @Test + void shouldDeserializeFallbackSourceSwaggerExample() { + final String json = "{\"currency\":\"USD\",\"fallback_source\":{" + + "\"type\":\"card\",\"number\":\"4543474002249996\",\"expiry_month\":6," + + "\"expiry_year\":2030,\"name\":\"Bruce Wayne\",\"cvv\":\"956\",\"stored\":true," + + "\"store_for_future_use\":false,\"allow_update\":false," + + "\"billing_address\":{\"address_line1\":\"123 High St.\",\"city\":\"London\",\"zip\":\"SW1A 1AA\",\"country\":\"GB\"}," + + "\"phone\":{\"country_code\":\"44\",\"number\":\"207 946 0000\"}," + + "\"account_holder\":{\"type\":\"individual\",\"first_name\":\"Bruce\",\"last_name\":\"Wayne\"}}}"; + + final PaymentRequest request = serializer.fromJson(json, PaymentRequest.class); + + final RequestCardSource fallbackSource = request.getFallbackSource(); + assertNotNull(fallbackSource); + assertEquals(PaymentSourceType.CARD, fallbackSource.getType()); + assertEquals("4543474002249996", fallbackSource.getNumber()); + assertEquals(6, fallbackSource.getExpiryMonth()); + assertEquals(2030, fallbackSource.getExpiryYear()); + assertEquals("Bruce Wayne", fallbackSource.getName()); + assertEquals("956", fallbackSource.getCvv()); + assertEquals(Boolean.TRUE, fallbackSource.getStored()); + assertEquals(Boolean.FALSE, fallbackSource.getStoreForFutureUse()); + assertEquals(Boolean.FALSE, fallbackSource.getAllowUpdate()); + assertEquals("London", fallbackSource.getBillingAddress().getCity()); + assertEquals(CountryCode.GB, fallbackSource.getBillingAddress().getCountry()); + assertEquals("44", fallbackSource.getPhone().getCountryCode()); + assertEquals("207 946 0000", fallbackSource.getPhone().getNumber()); + assertEquals(AccountHolderType.INDIVIDUAL, fallbackSource.getAccountHolder().getType()); + assertEquals("Bruce", fallbackSource.getAccountHolder().getFirstName()); + } + + @Test + void shouldNotSerializeNullFallbackSource() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("\"fallback_source\"")); + } + + @Test + void shouldSerializeAllowUpdateOnPrimaryCardSource() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .source(RequestCardSource.builder() + .number("4543474002249996") + .expiryMonth(6) + .expiryYear(2030) + .allowUpdate(true) + .build()) + .build(); + + final JsonObject json = JsonParser.parseString(serializer.toJson(request)).getAsJsonObject(); + + assertTrue(json.getAsJsonObject("source").get("allow_update").getAsBoolean()); + } + + private static RequestCardSource buildFallbackSource() { + final Address address = Address.builder() + .addressLine1("123 High St.") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build(); + final Phone phone = Phone.builder() + .countryCode("44") + .number("207 946 0000") + .build(); + return RequestCardSource.builder() + .number("4543474002249996") + .expiryMonth(6) + .expiryYear(2030) + .name("Bruce Wayne") + .cvv("956") + .stored(true) + .storeForFutureUse(false) + .billingAddress(address) + .phone(phone) + .accountHolder(AccountHolder.builder() + .type(AccountHolderType.INDIVIDUAL) + .firstName("Bruce") + .lastName("Wayne") + .build()) + .allowUpdate(false) + .build(); + } }