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(); + } }