From 47711e6d00dbf188beb23ae24ae6952a7defec89 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 27 Aug 2026 18:02:02 +0200 Subject: [PATCH 01/18] BACS Direct Debit notifications initial dev + tests --- README.md | 77 +++ .../com/checkout/AbstractCheckoutApmApi.java | 8 + .../java/com/checkout/CheckoutApmApi.java | 16 + .../java/com/checkout/GsonSerializer.java | 14 +- .../com/checkout/apm/bacs/BacsClient.java | 29 ++ .../com/checkout/apm/bacs/BacsClientImpl.java | 39 ++ .../apm/bacs/BacsNotificationRequest.java | 91 ++++ .../apm/bacs/BacsNotificationResponse.java | 20 + .../apm/bacs/BacsNotificationType.java | 13 + .../com/checkout/common/InstrumentType.java | 12 +- .../checkout/common/PaymentSourceType.java | 24 + .../checkout/instruments/BacsPaymentType.java | 22 + .../InstrumentAccountHolderType.java | 20 + .../create/CreateAchAccountHolder.java | 46 ++ .../create/CreateAchInstrumentData.java | 58 +++ .../create/CreateBacsAccountHolder.java | 38 ++ .../create/CreateBacsBillingAddress.java | 57 +++ .../create/CreateBacsInstrumentAccount.java | 24 + .../create/CreateBacsInstrumentData.java | 71 +++ .../create/CreateInstrumentAchRequest.java | 51 ++ .../create/CreateInstrumentAchResponse.java | 25 + .../create/CreateInstrumentBacsRequest.java | 59 +++ .../create/CreateInstrumentBacsResponse.java | 25 + .../create/CreateInstrumentResponse.java | 26 + .../instruments/get/GetAchAccountHolder.java | 43 ++ .../instruments/get/GetAchInstrumentData.java | 58 +++ .../get/GetAchInstrumentResponse.java | 58 +++ .../instruments/get/GetBacsAccountHolder.java | 50 ++ .../get/GetBacsBillingAddress.java | 53 +++ .../get/GetBacsInstrumentAccount.java | 29 ++ .../get/GetBacsInstrumentData.java | 93 ++++ .../get/GetBacsInstrumentResponse.java | 73 +++ .../get/GetBankAccountInstrumentResponse.java | 7 + .../instruments/get/GetCardAccountHolder.java | 43 ++ .../get/GetCardInstrumentResponse.java | 6 + .../get/GetInstrumentResponse.java | 25 +- .../instruments/get/GetSepaAccountHolder.java | 50 ++ .../get/GetSepaBillingAddress.java | 50 ++ .../get/GetSepaInstrumentData.java | 80 ++++ .../get/GetSepaInstrumentResponse.java | 44 +- .../update/UpdateBacsAccountHolder.java | 50 ++ .../update/UpdateBacsBillingAddress.java | 57 +++ .../update/UpdateBacsInstrumentData.java | 68 +++ .../update/UpdateInstrumentBacsRequest.java | 45 ++ .../update/UpdateInstrumentBacsResponse.java | 33 ++ .../request/source/apm/RequestSepaSource.java | 15 +- .../request/source/apm/RequestBacsSource.java | 37 ++ .../response/source/BacsResponseSource.java | 24 + .../com/checkout/CheckoutApiImplTest.java | 1 + .../checkout/apm/bacs/BacsClientImplTest.java | 86 ++++ .../apm/bacs/BacsNotificationTestIT.java | 42 ++ .../apm/bacs/BacsSerializationTest.java | 158 +++++++ .../InstrumentTypeSerializationTest.java | 36 ++ .../PaymentSourceTypeSerializationTest.java | 142 ++++++ .../AchInstrumentSerializationTest.java | 258 ++++++++++ .../instruments/AchInstrumentsTestIT.java | 54 +++ .../BacsInstrumentSerializationTest.java | 446 ++++++++++++++++++ .../instruments/BacsInstrumentsTestIT.java | 54 +++ .../InstrumentResponseDispatchTest.java | 96 ++++ .../RequestSepaSourceSerializationTest.java | 54 +++ .../RequestBacsSourceSerializationTest.java | 48 ++ .../BacsResponseSourceSerializationTest.java | 40 ++ 62 files changed, 3455 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/checkout/apm/bacs/BacsClient.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsClientImpl.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationType.java create mode 100644 src/main/java/com/checkout/instruments/BacsPaymentType.java create mode 100644 src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java create mode 100644 src/main/java/com/checkout/payments/response/source/BacsResponseSource.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java create mode 100644 src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java create mode 100644 src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java create mode 100644 src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java create mode 100644 src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java create mode 100644 src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java create mode 100644 src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java create mode 100644 src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java diff --git a/README.md b/README.md index 1c58463ef..09cf4d2ab 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,83 @@ public static void main(String[] args) { } ``` +### Bacs Direct Debit + +Send a pre-notification (advance notice) to a payer before collecting funds from their account. The +endpoint accepts the secret key only, so it is not available on an OAuth-only client. + +```java +import com.checkout.CheckoutApi; +import com.checkout.apm.bacs.BacsNotificationRequest; +import com.checkout.apm.bacs.BacsNotificationResponse; +import com.checkout.apm.bacs.BacsNotificationType; +import com.checkout.common.Currency; + +import java.time.LocalDate; + +final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") // optional + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") // optional + .build(); + +final CompletableFuture notification = + checkoutApi.bacsClient().sendNotification(request); +``` + +A Bacs Direct Debit instrument is stored, updated and retrieved through the instruments client. Note +that the Bacs `payment_type` values are capitalized (`Recurring`, `Regular`), unlike the SEPA ones, +so use `BacsPaymentType` and not `SepaPaymentType`: + +```java +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; + +final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") // 8 characters + .bankCode("040004") // the sort code, 6 characters + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .country(CountryCode.GB) + .build()) + .build()) + .build(); + +final CreateInstrumentBacsResponse created = + checkoutApi.instrumentsClient().create(request).get(); + +final GetBacsInstrumentResponse stored = + (GetBacsInstrumentResponse) checkoutApi.instrumentsClient().get(created.getId()).get(); +``` + +To take a payment against a stored Bacs instrument, use `RequestBacsSource`. The payment response +returns a `BacsResponseSource`. + ## Logging The SDK supports SLF4J as logger provider, you need to provide your configuration file through `resources` folder. diff --git a/src/main/java/com/checkout/AbstractCheckoutApmApi.java b/src/main/java/com/checkout/AbstractCheckoutApmApi.java index 39091e640..9d634ccf4 100644 --- a/src/main/java/com/checkout/AbstractCheckoutApmApi.java +++ b/src/main/java/com/checkout/AbstractCheckoutApmApi.java @@ -1,5 +1,7 @@ package com.checkout; +import com.checkout.apm.bacs.BacsClient; +import com.checkout.apm.bacs.BacsClientImpl; import com.checkout.apm.ideal.IdealClient; import com.checkout.apm.ideal.IdealClientImpl; import com.checkout.apm.previous.klarna.KlarnaClient; @@ -14,12 +16,14 @@ public abstract class AbstractCheckoutApmApi { protected final ApiClient apiClient; private final IdealClient idealClient; + private final BacsClient bacsClient; private final KlarnaClient klarnaClient; private final SepaClient sepaClient; protected AbstractCheckoutApmApi(final CheckoutConfiguration configuration) { this.apiClient = getBaseApiClient(configuration); this.idealClient = new IdealClientImpl(apiClient, configuration); + this.bacsClient = new BacsClientImpl(apiClient, configuration); this.klarnaClient = new KlarnaClientImpl(apiClient, configuration); this.sepaClient = new SepaClientImpl(apiClient, configuration); } @@ -28,6 +32,10 @@ public IdealClient idealClient() { return idealClient; } + public BacsClient bacsClient() { + return bacsClient; + } + public KlarnaClient klarnaClient() { return klarnaClient; } diff --git a/src/main/java/com/checkout/CheckoutApmApi.java b/src/main/java/com/checkout/CheckoutApmApi.java index 0e981d17e..c53d71eea 100644 --- a/src/main/java/com/checkout/CheckoutApmApi.java +++ b/src/main/java/com/checkout/CheckoutApmApi.java @@ -1,9 +1,25 @@ package com.checkout; +import com.checkout.apm.bacs.BacsClient; import com.checkout.apm.ideal.IdealClient; +/** + * The alternative payment method clients that the current platform exposes. + */ public interface CheckoutApmApi { + /** + * Retrieves iDEAL issuer information. + * + * @return the iDEAL client. + */ IdealClient idealClient(); + /** + * Sends Bacs Direct Debit pre-notifications. + * + * @return the Bacs Direct Debit client. + */ + BacsClient bacsClient(); + } diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index d192843ca..ed0c0e0eb 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -167,7 +167,8 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.response.source.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.response.source.AlternativePaymentSourceResponse.class) .registerSubtype(com.checkout.payments.response.source.CardResponseSource.class, identifier(PaymentSourceType.CARD)) .registerSubtype(com.checkout.payments.response.source.CurrencyAccountResponseSource.class, identifier(PaymentSourceType.CURRENCY_ACCOUNT)) - .registerSubtype(com.checkout.payments.response.source.PayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL))) + .registerSubtype(com.checkout.payments.response.source.PayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL)) + .registerSubtype(com.checkout.payments.response.source.BacsResponseSource.class, identifier(PaymentSourceType.BACS))) // Payment Contexts .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.response.source.contexts.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.response.source.contexts.AlternativePaymentSourceResponse.class) .registerSubtype(com.checkout.payments.response.source.contexts.PaymentContextsPayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL)) @@ -186,16 +187,21 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentTokenResponse.class, identifier(InstrumentType.CARD)) - .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA))) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentBacsResponse.class, identifier(InstrumentType.BACS)) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.get.GetBankAccountInstrumentResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.get.GetCardInstrumentResponse.class, identifier(InstrumentType.CARD)) - .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA))) + .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA)) + .registerSubtype(com.checkout.instruments.get.GetBacsInstrumentResponse.class, identifier(InstrumentType.BACS)) + .registerSubtype(com.checkout.instruments.get.GetAchInstrumentResponse.class, identifier(InstrumentType.ACH))) .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentCardResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) - .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) + .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH)) + .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBacsResponse.class, identifier(InstrumentType.BACS))) // Workflows CS2 .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.workflows.actions.response.WebhookWorkflowActionResponse.class, identifier(WorkflowActionType.WEBHOOK))) diff --git a/src/main/java/com/checkout/apm/bacs/BacsClient.java b/src/main/java/com/checkout/apm/bacs/BacsClient.java new file mode 100644 index 000000000..d76ff6db9 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsClient.java @@ -0,0 +1,29 @@ +package com.checkout.apm.bacs; + +import java.util.concurrent.CompletableFuture; + +/** + * Bacs Direct Debit client. + */ +public interface BacsClient { + + /** + * Sends a Bacs Direct Debit pre-notification (advance notice) to a payer ahead of collecting + * funds from their account. + * + * @param bacsNotificationRequest the pre-notification details. + * @return a {@link CompletableFuture} that resolves to the created notification event. + */ + CompletableFuture sendNotification(BacsNotificationRequest bacsNotificationRequest); + + // Synchronous methods + + /** + * Synchronous variant of {@link #sendNotification(BacsNotificationRequest)}. + * + * @param bacsNotificationRequest the pre-notification details. + * @return the created notification event. + */ + BacsNotificationResponse sendNotificationSync(BacsNotificationRequest bacsNotificationRequest); + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java b/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java new file mode 100644 index 000000000..2e30af33c --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java @@ -0,0 +1,39 @@ +package com.checkout.apm.bacs; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorizationType; + +import java.util.concurrent.CompletableFuture; + +import static com.checkout.common.CheckoutUtils.validateParams; + +public class BacsClientImpl extends AbstractClient implements BacsClient { + + private static final String APMS = "apms"; + private static final String BACS_NOTIFICATIONS = "bacs/notifications"; + private static final String BACS_NOTIFICATION_REQUEST = "bacsNotificationRequest"; + + /** + * The operation declares the secret key as its only security scheme, so this client does not + * use the secret-key-or-OAuth variant that the instruments endpoints allow. + */ + public BacsClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY); + } + + @Override + public CompletableFuture sendNotification(final BacsNotificationRequest bacsNotificationRequest) { + validateParams(BACS_NOTIFICATION_REQUEST, bacsNotificationRequest); + return apiClient.postAsync(buildPath(APMS, BACS_NOTIFICATIONS), sdkAuthorization(), BacsNotificationResponse.class, bacsNotificationRequest, null); + } + + // Synchronous methods + @Override + public BacsNotificationResponse sendNotificationSync(final BacsNotificationRequest bacsNotificationRequest) { + validateParams(BACS_NOTIFICATION_REQUEST, bacsNotificationRequest); + return apiClient.post(buildPath(APMS, BACS_NOTIFICATIONS), sdkAuthorization(), BacsNotificationResponse.class, bacsNotificationRequest, null); + } + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java new file mode 100644 index 000000000..e4cfa30e5 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java @@ -0,0 +1,91 @@ +package com.checkout.apm.bacs; + +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * Bacs Direct Debit notification request. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class BacsNotificationRequest { + + /** + * The ID of the Bacs Direct Debit instrument to notify against. + * [Required] + * ^(src)_(\w{26})$ + */ + private String sourceId; + + /** + * The type of pre-notification being sent to the payer. + * [Required] + */ + private BacsNotificationType notificationType; + + /** + * The date the funds will be collected from the payer's account, in the format yyyy-MM-dd. + * [Required] + * Format: yyyy-MM-dd + */ + private LocalDate collectionDate; + + /** + * The amount to be collected, in the currency's minor unit. + * [Required] + * Format: int64 + * min 1 + */ + private Long amount; + + /** + * The three-letter ISO 4217 currency code of the collection. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * A reference you can use to identify the collection. + * [Optional] + * max 50 characters + */ + private String reference; + + /** + * The email address of the payer that the pre-notification is sent to. + * [Required] + * Format: email + */ + private String customerEmail; + + /** + * The billing descriptor that appears on the payer's bank statement. + * [Required] + * max 25 characters + */ + private String billingDescriptor; + + /** + * The support email address included in the pre-notification. + * [Required] + * Format: email + */ + private String supportEmail; + + /** + * The support phone number included in the pre-notification, in E.164 format. The + * specification declares no pattern for this property. + * [Optional] + */ + private String supportPhone; + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java new file mode 100644 index 000000000..dd61992d8 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java @@ -0,0 +1,20 @@ +package com.checkout.apm.bacs; + +import com.checkout.HttpMetadata; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Bacs Direct Debit pre-notification response. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class BacsNotificationResponse extends HttpMetadata { + + /** + * The unique identifier of the notification event. + * [Required] + */ + private String eventId; + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java new file mode 100644 index 000000000..5e6a9dd4d --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java @@ -0,0 +1,13 @@ +package com.checkout.apm.bacs; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of pre-notification being sent to the payer. + */ +public enum BacsNotificationType { + + @SerializedName("advance_notice") + ADVANCE_NOTICE + +} diff --git a/src/main/java/com/checkout/common/InstrumentType.java b/src/main/java/com/checkout/common/InstrumentType.java index e6b606e6a..687dabf94 100644 --- a/src/main/java/com/checkout/common/InstrumentType.java +++ b/src/main/java/com/checkout/common/InstrumentType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of payment instrument. + */ public enum InstrumentType { @SerializedName("bank_account") @@ -13,6 +16,10 @@ public enum InstrumentType { @SerializedName("card") CARD, + /** + * Retained for the previous-platform instruments API. Not a value of the current platform's + * instrument type. + */ @SerializedName("card_token") CARD_TOKEN, @@ -20,6 +27,9 @@ public enum InstrumentType { SEPA, @SerializedName("ach") - ACH + ACH, + + @SerializedName("bacs") + BACS } diff --git a/src/main/java/com/checkout/common/PaymentSourceType.java b/src/main/java/com/checkout/common/PaymentSourceType.java index b359fc233..04565c94d 100644 --- a/src/main/java/com/checkout/common/PaymentSourceType.java +++ b/src/main/java/com/checkout/common/PaymentSourceType.java @@ -2,6 +2,13 @@ import com.google.gson.annotations.SerializedName; +/** + * The payment source type. + * + *

This enum is the union of the source types accepted on payment requests and returned on + * payment responses, on both the current and the previous platform, so it also carries + * previous-platform values that the current API specification no longer declares. + */ public enum PaymentSourceType { @SerializedName("ach") @@ -20,6 +27,8 @@ public enum PaymentSourceType { ALMA, @SerializedName("applepay") APPLEPAY, + @SerializedName("bacs") + BACS, @SerializedName("bancontact") BANCONTACT, @SerializedName("bank_account") @@ -98,6 +107,21 @@ public enum PaymentSourceType { QPAY, @SerializedName("rapipago") RAPIPAGO, + /** + * Shares the wire value of {@link #ID}, because the previous platform references a stored SEPA + * mandate through the generic "id" source. Nothing in the SDK passes this constant any more: + * the previous-platform source now uses {@link #ID} directly, which is how the .NET SDK models + * the same call. + * + *

Gson resolves an incoming "id" to whichever of the two constants is declared last, which + * is this one, so deserializing a previous-platform source yields SEPA rather than ID. Removing + * this constant would correct that but is a breaking change, so it is deprecated instead. Do + * not reorder the two constants: that would silently change what "id" deserializes to. + * + * @deprecated use {@link #ID} for the previous platform and {@link #SEPAV4} for the current + * platform, where the wire value is "sepa". + */ + @Deprecated @SerializedName("id") SEPA, @SerializedName("sepa") diff --git a/src/main/java/com/checkout/instruments/BacsPaymentType.java b/src/main/java/com/checkout/instruments/BacsPaymentType.java new file mode 100644 index 000000000..b254f8d82 --- /dev/null +++ b/src/main/java/com/checkout/instruments/BacsPaymentType.java @@ -0,0 +1,22 @@ +package com.checkout.instruments; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of payment for a Bacs Direct Debit instrument. + * + *

The wire values are capitalized, and the specification allows these two values only. The + * equivalent SEPA field is lowercase in the specification, so do not share one type between the + * two: reusing {@link com.checkout.instruments.update.SepaPaymentType} sends a value the API + * rejects. Do not replace this enum with {@link com.checkout.payments.PaymentType} either, because + * that enum also accepts MOTO, Installment, PayLater and Unscheduled, which Bacs does not allow. + */ +public enum BacsPaymentType { + + @SerializedName("Recurring") + RECURRING, + + @SerializedName("Regular") + REGULAR + +} diff --git a/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java b/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java new file mode 100644 index 000000000..a69abbdfa --- /dev/null +++ b/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java @@ -0,0 +1,20 @@ +package com.checkout.instruments; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of account holder of a stored payment instrument. + * + *

Shared by the Bacs Direct Debit, SEPA and ACH instrument variants, which all declare the same + * two values. This is deliberately not {@link com.checkout.common.AccountHolderType}, which also + * declares {@code government} and is therefore wider than the instrument schemas allow. + */ +public enum InstrumentAccountHolderType { + + @SerializedName("individual") + INDIVIDUAL, + + @SerializedName("corporate") + CORPORATE + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java b/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java new file mode 100644 index 000000000..c2618f40d --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java @@ -0,0 +1,46 @@ +package com.checkout.instruments.create; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of an ACH instrument being stored. + * + *

The specification marks all four properties as required, but the descriptions qualify that: + * the names apply to an individual account holder and the company name to a corporate one. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java b/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java new file mode 100644 index 000000000..58575caf1 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.AchInstrumentAccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the ACH account being stored. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Required] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Required] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Required] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java b/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java new file mode 100644 index 000000000..302eeccd8 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java @@ -0,0 +1,38 @@ +package com.checkout.instruments.create; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a Bacs Direct Debit instrument being stored. + * + *

The store shape declares three properties only. The update and retrieve shapes add a company + * name and an account holder type, so they use their own types. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The billing address of the account holder. + * [Required] + */ + private CreateBacsBillingAddress billingAddress; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java b/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java new file mode 100644 index 000000000..0cc9d24e0 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java @@ -0,0 +1,57 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a Bacs Direct Debit instrument being stored. + * + *

The length constraints differ from the update and retrieve variants of the same address, so + * this type is deliberately not shared with them. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + * max 200 characters + */ + private String addressLine1; + + /** + * The street number. If no number, pass "w/n". + * [Optional] + * max 10 characters + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + * max 35 characters + */ + private String city; + + /** + * The address zip/postal code. + * [Optional] + * max 16 characters + */ + private String zip; + + /** + * The two-letter ISO country code of the address. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java new file mode 100644 index 000000000..3a957d055 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java @@ -0,0 +1,24 @@ +package com.checkout.instruments.create; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account configuration for a Bacs Direct Debit instrument being stored. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsInstrumentAccount { + + /** + * The ID of the processing channel to associate with the instrument. + * [Required] + * ^(pc)_(\w{26})$ + */ + private String processingChannelId; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java new file mode 100644 index 000000000..b810fdfc0 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java @@ -0,0 +1,71 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the Bacs Direct Debit account being stored. + * + *

This is not the SEPA instrument data shape: the account number is a fixed-length UK account + * number rather than an IBAN, the sort code arrives in {@code bank_code}, and there is no mandate + * type, mandate ID or date of signature. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Required] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Required] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. Recurring or Regular. + * [Required] + */ + private BacsPaymentType paymentType; + + /** + * Indicates whether the Bacs instrument is created when account validation returns a partial + * match. When true, the instrument is created on a partial match; when false, instrument + * creation fails on a partial match. + * [Optional] + * Default: false + */ + private Boolean allowPartialMatch; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java new file mode 100644 index 000000000..b818ede50 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java @@ -0,0 +1,51 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Store ACH bank account details. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentAchRequest extends CreateInstrumentRequest { + + /** + * The details of the bank account. + * [Required] + */ + private CreateAchInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private CreateAchAccountHolder accountHolder; + + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ + private CreateCustomerInstrumentRequest customer; + + @Builder + private CreateInstrumentAchRequest(final CreateAchInstrumentData instrumentData, + final CreateAchAccountHolder accountHolder, + final CreateCustomerInstrumentRequest customer) { + super(InstrumentType.ACH); + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + this.customer = customer; + } + + public CreateInstrumentAchRequest() { + super(InstrumentType.ACH); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java new file mode 100644 index 000000000..32b5971ec --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java @@ -0,0 +1,25 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Store ACH bank account instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentAchResponse extends CreateInstrumentResponse { + + /** + * The type of instrument. + * [Required] + */ + private final InstrumentType type = InstrumentType.ACH; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java new file mode 100644 index 000000000..af79fcb60 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java @@ -0,0 +1,59 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Store Bacs Direct Debit account details. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentBacsRequest extends CreateInstrumentRequest { + + /** + * The account configuration for the instrument. + * [Required] + */ + private CreateBacsInstrumentAccount account; + + /** + * The details of the Bacs Direct Debit account. + * [Required] + */ + private CreateBacsInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private CreateBacsAccountHolder accountHolder; + + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ + private CreateCustomerInstrumentRequest customer; + + @Builder + private CreateInstrumentBacsRequest(final CreateBacsInstrumentAccount account, + final CreateBacsInstrumentData instrumentData, + final CreateBacsAccountHolder accountHolder, + final CreateCustomerInstrumentRequest customer) { + super(InstrumentType.BACS); + this.account = account; + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + this.customer = customer; + } + + public CreateInstrumentBacsRequest() { + super(InstrumentType.BACS); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java new file mode 100644 index 000000000..c52d5241b --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java @@ -0,0 +1,25 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Store Bacs Direct Debit account instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentBacsResponse extends CreateInstrumentResponse { + + /** + * The type of instrument. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java index c9a48df39..e23af6050 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java @@ -5,14 +5,40 @@ import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The shared properties of a stored payment instrument. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class CreateInstrumentResponse extends HttpMetadata { + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ protected String id; + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ protected String fingerprint; + /** + * The customer that the instrument is associated with. + * + *

Only the token and bank_account variants declare this property. The sepa, ach and bacs + * store responses do not, so on those three it is always null and does not belong here. + * [Optional] + * + * @deprecated declared on the wrong level. This will move to + * {@link CreateInstrumentTokenResponse} and {@link CreateInstrumentBankAccountResponse}, the + * only two variants the specification declares it on, in the next major version. Read it from + * the concrete response type rather than from this base class. + */ + @Deprecated protected CustomerResponse customer; } diff --git a/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java new file mode 100644 index 000000000..e149f4d81 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java @@ -0,0 +1,43 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a stored ACH instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java new file mode 100644 index 000000000..c297e147e --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.AchInstrumentAccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of a stored ACH bank account. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Required] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Required] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Required] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java new file mode 100644 index 000000000..bf9e0ee98 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.get; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; + +/** + * ACH instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class GetAchInstrumentResponse extends GetInstrumentResponse { + + /** + * The underlying instrument type. + * [Required] + */ + private final InstrumentType type = InstrumentType.ACH; + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant createdOn; + + /** + * The date and time the instrument was last modified. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant modifiedOn; + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ + private String vaultId; + + /** + * The details of the bank account. + * [Required] + */ + private GetAchInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private GetAchAccountHolder accountHolder; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java new file mode 100644 index 000000000..4639d25b7 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder's details of a stored Bacs Direct Debit instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Required] + */ + private GetBacsBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java b/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java new file mode 100644 index 000000000..b5b87053e --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java @@ -0,0 +1,53 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a stored Bacs Direct Debit instrument. + * + *

The retrieve variant of this address declares no length limits other than on the country, so + * it is deliberately not shared with the store and update variants. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + */ + private String addressLine1; + + /** + * The second line of the address. + * [Optional] + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + */ + private String city; + + /** + * The address ZIP or postal code. + * [Optional] + */ + private String zip; + + /** + * The address country. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java new file mode 100644 index 000000000..21253ce97 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java @@ -0,0 +1,29 @@ +package com.checkout.instruments.get; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account configuration for a stored Bacs Direct Debit instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsInstrumentAccount { + + /** + * The ID of the client associated with the instrument. + * [Optional] + */ + private String clientId; + + /** + * The ID of the processing channel associated with the instrument. + * [Optional] + */ + private String processingChannelId; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java new file mode 100644 index 000000000..96eaa8dd8 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java @@ -0,0 +1,93 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of a stored Bacs Direct Debit account. + * + *

The retrieve shape adds four read-back properties that the store and update shapes do not + * declare: status, matchStatus, description and mandateId. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Required] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Required] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. + * [Required] + */ + private BacsPaymentType paymentType; + + /** + * Whether vault accepted a partial match when looking up the Bacs instrument for the supplied + * account details. + * [Optional] + */ + private Boolean allowPartialMatch; + + /** + * The validation status of the account. The specification declares no enum for this property. + * [Optional] + */ + private String status; + + /** + * The result of matching the account holder name against the account owner. The specification + * declares no enum for this property. + * [Optional] + */ + private String matchStatus; + + /** + * A human-readable description of the validation result. + * [Optional] + */ + private String description; + + /** + * The identifier of the Bacs Direct Debit mandate. + * [Optional] + */ + private String mandateId; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java new file mode 100644 index 000000000..3a0529706 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java @@ -0,0 +1,73 @@ +package com.checkout.instruments.get; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * Bacs Direct Debit instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class GetBacsInstrumentResponse extends GetInstrumentResponse { + + /** + * The underlying instrument type. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant createdOn; + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ + private String vaultId; + + /** + * The date and time the instrument was last modified. + * [Optional] + * Format: date-time (RFC 3339) + */ + private Instant modifiedOn; + + /** + * The account configuration for the instrument. + * [Optional] + */ + private GetBacsInstrumentAccount account; + + /** + * The list of validations performed on the instrument. The API publishes no item schema for + * this array, so each entry is exposed as an untyped map. + * [Optional] + */ + private List> validations; + + /** + * The details of the Bacs Direct Debit account. + * [Optional] + */ + private GetBacsInstrumentData instrumentData; + + /** + * The account holder's details. + * [Optional] + */ + private GetBacsAccountHolder accountHolder; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java index 298a57442..a91428ed5 100644 --- a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java @@ -1,5 +1,6 @@ package com.checkout.instruments.get; +import com.checkout.common.AccountHolder; import com.checkout.common.AccountType; import com.checkout.common.BankDetails; import com.checkout.common.CountryCode; @@ -36,4 +37,10 @@ public final class GetBankAccountInstrumentResponse extends GetInstrumentRespons private BankDetails bank; + /** + * The account holder details. + * [Optional] + */ + private AccountHolder accountHolder; + } diff --git a/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java new file mode 100644 index 000000000..149b8529b --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java @@ -0,0 +1,43 @@ +package com.checkout.instruments.get; + +import com.checkout.common.Address; +import com.checkout.common.Phone; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a stored card instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetCardAccountHolder { + + /** + * The first name of the account holder. + * [Optional] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Optional] + */ + private String lastName; + + /** + * The billing address of the account holder. + * [Optional] + */ + private Address billingAddress; + + /** + * The phone number of the account holder. + * [Optional] + */ + private Phone phone; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java index c4e2f9443..0ac264259 100644 --- a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java @@ -119,4 +119,10 @@ public final class GetCardInstrumentResponse extends GetInstrumentResponse { */ private Boolean regulatedIndicator; + /** + * The account holder details. + * [Optional] + */ + private GetCardAccountHolder accountHolder; + } diff --git a/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java index 5122ee0ae..ce72087a2 100644 --- a/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java @@ -1,20 +1,39 @@ package com.checkout.instruments.get; import com.checkout.HttpMetadata; -import com.checkout.common.AccountHolder; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The shared properties of a retrieved payment instrument. + * + *

The account holder is declared by each concrete variant rather than here, because the + * specification gives every variant a different account holder shape: the bank account variant + * refers to the shared AccountHolder, while the card, SEPA and Bacs Direct Debit variants each + * declare their own inline shape. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class GetInstrumentResponse extends HttpMetadata { + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ protected String id; + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ protected String fingerprint; + /** + * The customer that the instrument is associated with. + * [Optional] + */ protected InstrumentCustomerResponse customer; - protected AccountHolder accountHolder; - } diff --git a/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java new file mode 100644 index 000000000..55d12ce03 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder's details of a stored SEPA instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Required] + */ + private GetSepaBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java b/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java new file mode 100644 index 000000000..94c4d5c73 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a stored SEPA instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaBillingAddress { + + /** + * The first line of the address. + * [Required] + */ + private String addressLine1; + + /** + * The second line of the address. + * [Required] + */ + private String addressLine2; + + /** + * The address city. + * [Required] + */ + private String city; + + /** + * The address ZIP or postal code. + * [Required] + */ + private String zip; + + /** + * The address country. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java new file mode 100644 index 000000000..62caaef82 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java @@ -0,0 +1,80 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.payments.request.source.apm.MandateType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * The details of a stored SEPA mandate. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs + * Direct Debit field is capitalized, so the two must not share a type. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaInstrumentData { + + /** + * The type of mandate. + * [Optional] + * Enum: "Core" "B2B" + */ + private MandateType type; + + /** + * The International Bank Account Number (IBAN) of the account. + * [Required] + * min 15 characters + * max 34 characters + */ + private String accountNumber; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. + * [Required] + */ + private SepaPaymentType paymentType; + + /** + * The mandate ID. If this value was not provided when the instrument was created, it may take + * up to five seconds for the generated mandate ID to be available. + * [Required] + * min 1 characters + * max 35 characters + */ + private String mandateId; + + /** + * The date the mandate was signed. If the mandate ID was not provided when the instrument was + * created, it may take up to five seconds for the generated date of signature to be available. + * [Required] + * Format: yyyy-MM-dd + */ + private LocalDate dateOfSignature; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java index b7c501a8e..da6638556 100644 --- a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java @@ -1,26 +1,58 @@ package com.checkout.instruments.get; import com.checkout.common.InstrumentType; -import com.checkout.instruments.create.InstrumentData; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.Instant; +/** + * SEPA instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetSepaInstrumentResponse extends GetInstrumentResponse { + /** + * The underlying instrument type. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; - + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ private Instant createdOn; - + + /** + * The date and time the instrument was last modified. + * [Required] + * Format: date-time (RFC 3339) + */ private Instant modifiedOn; - + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ private String vaultId; - - private InstrumentData instrumentData; + + /** + * The details of the SEPA mandate. + * [Optional] + */ + private GetSepaInstrumentData instrumentData; + + /** + * The account holder's details. + * [Optional] + */ + private GetSepaAccountHolder accountHolder; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java b/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java new file mode 100644 index 000000000..c245257fe --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.update; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a Bacs Direct Debit instrument being updated. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsAccountHolder { + + /** + * The first name of the account holder. + * [Optional] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Optional] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Optional] + */ + private UpdateBacsBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java b/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java new file mode 100644 index 000000000..480ef12c6 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java @@ -0,0 +1,57 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a Bacs Direct Debit instrument being updated. + * + *

The city and zip limits are wider here than on the store request, so this type is + * deliberately not shared with it. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + * max 200 characters + */ + private String addressLine1; + + /** + * The street number. If no number, pass "w/n". + * [Optional] + * max 10 characters + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + * max 50 characters + */ + private String city; + + /** + * The address zip/postal code. + * [Optional] + * max 50 characters + */ + private String zip; + + /** + * The two-letter ISO country code of the address. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java b/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java new file mode 100644 index 000000000..460f017a6 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java @@ -0,0 +1,68 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the Bacs Direct Debit account being updated. + * + *

Every property is optional on update, and allowPartialMatch changes meaning: on the store + * request it instructs the vault, and here it reads back what the vault accepted. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Optional] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Optional] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. Recurring or Regular. + * [Optional] + */ + private BacsPaymentType paymentType; + + /** + * Whether vault accepted a partial match when looking up the Bacs instrument for the supplied + * account details. + * [Optional] + */ + private Boolean allowPartialMatch; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java new file mode 100644 index 000000000..c3a7a6273 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java @@ -0,0 +1,45 @@ +package com.checkout.instruments.update; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Update Bacs Direct Debit account details. + * + *

Nothing in this request is required by the specification. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class UpdateInstrumentBacsRequest extends UpdateInstrumentRequest { + + /** + * The details of the Bacs Direct Debit account. + * [Optional] + */ + private UpdateBacsInstrumentData instrumentData; + + /** + * The account holder details. + * [Optional] + */ + private UpdateBacsAccountHolder accountHolder; + + @Builder + private UpdateInstrumentBacsRequest(final UpdateBacsInstrumentData instrumentData, + final UpdateBacsAccountHolder accountHolder) { + super(InstrumentType.BACS); + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + } + + public UpdateInstrumentBacsRequest() { + super(InstrumentType.BACS); + } + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java new file mode 100644 index 000000000..76fbf0af4 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java @@ -0,0 +1,33 @@ +package com.checkout.instruments.update; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Update Bacs Direct Debit account instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The id is declared here + * because the shared update response does not carry it. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class UpdateInstrumentBacsResponse extends UpdateInstrumentResponse { + + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + +} diff --git a/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java b/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java index c0ea1e845..eb8767b65 100644 --- a/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java +++ b/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java @@ -8,22 +8,33 @@ import lombok.Setter; import lombok.ToString; +/** + * A payment against a stored SEPA mandate on the previous platform. + * + *

The previous platform references the stored mandate through the generic "id" source, so the + * type on the wire is "id" and not "sepa". Use {@link com.checkout.payments.request.source.apm.RequestSepaSource} + * for the current platform, where the type is "sepa". + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestSepaSource extends AbstractRequestSource { + /** + * The ID of the stored SEPA mandate. + * [Required] + */ private String id; @Builder private RequestSepaSource(final String id) { - super(PaymentSourceType.SEPA); + super(PaymentSourceType.ID); this.id = id; } public RequestSepaSource() { - super(PaymentSourceType.SEPA); + super(PaymentSourceType.ID); } } diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java new file mode 100644 index 000000000..34f86f5e2 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java @@ -0,0 +1,37 @@ +package com.checkout.payments.request.source.apm; + +import com.checkout.common.PaymentSourceType; +import com.checkout.payments.request.source.AbstractRequestSource; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Bacs Direct Debit source. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class RequestBacsSource extends AbstractRequestSource { + + /** + * The Bacs Direct Debit instrument ID. + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + @Builder + private RequestBacsSource(final String id) { + super(PaymentSourceType.BACS); + this.id = id; + } + + public RequestBacsSource() { + super(PaymentSourceType.BACS); + } + +} diff --git a/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java b/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java new file mode 100644 index 000000000..78f210349 --- /dev/null +++ b/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java @@ -0,0 +1,24 @@ +package com.checkout.payments.response.source; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import static com.checkout.common.PaymentSourceType.BACS; + +/** + * Bacs Direct Debit source. + * + *

The specification declares a type and an id only, both of which the shared + * {@link AbstractResponseSource} already carries. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BacsResponseSource extends AbstractResponseSource implements ResponseSource { + + public BacsResponseSource() { + this.type = BACS; + } + +} diff --git a/src/test/java/com/checkout/CheckoutApiImplTest.java b/src/test/java/com/checkout/CheckoutApiImplTest.java index 7e9da8faa..f231dc362 100644 --- a/src/test/java/com/checkout/CheckoutApiImplTest.java +++ b/src/test/java/com/checkout/CheckoutApiImplTest.java @@ -40,6 +40,7 @@ void shouldInstantiateAndRetrieveClients() { assertNotNull(checkoutApi.onboardingSimulatorClient()); // APMs assertNotNull(checkoutApi.idealClient()); + assertNotNull(checkoutApi.bacsClient()); } } diff --git a/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java b/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java new file mode 100644 index 000000000..f1354c990 --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java @@ -0,0 +1,86 @@ +package com.checkout.apm.bacs; + +import com.checkout.ApiClient; +import com.checkout.CheckoutArgumentException; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BacsClientImplTest { + + private static final String NOTIFICATIONS_PATH = "apms/bacs/notifications"; + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + @Mock + private BacsNotificationRequest request; + + @Mock + private BacsNotificationResponse response; + + private BacsClient bacsClient; + + @BeforeEach + void setUp() { + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY)).thenReturn(authorization); + lenient().when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + this.bacsClient = new BacsClientImpl(apiClient, configuration); + } + + @Test + void shouldSendNotification() throws ExecutionException, InterruptedException { + + when(apiClient.postAsync(NOTIFICATIONS_PATH, authorization, BacsNotificationResponse.class, request, null)) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = bacsClient.sendNotification(request); + + assertNotNull(future.get()); + assertEquals(response, future.get()); + } + + // Synchronous methods + @Test + void shouldSendNotificationSync() { + + when(apiClient.post(NOTIFICATIONS_PATH, authorization, BacsNotificationResponse.class, request, null)) + .thenReturn(response); + + final BacsNotificationResponse result = bacsClient.sendNotificationSync(request); + + assertNotNull(result); + assertEquals(response, result); + } + + @Test + void shouldFailWhenRequestIsNull() { + assertThrows(CheckoutArgumentException.class, () -> bacsClient.sendNotification(null)); + assertThrows(CheckoutArgumentException.class, () -> bacsClient.sendNotificationSync(null)); + } +} diff --git a/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java b/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java new file mode 100644 index 000000000..d996a815f --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java @@ -0,0 +1,42 @@ +package com.checkout.apm.bacs; + +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.common.Currency; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class BacsNotificationTestIT extends SandboxTestFixture { + + BacsNotificationTestIT() { + super(PlatformType.DEFAULT); + } + + @Test + @Disabled("Requires a merchant enabled for Bacs Direct Debit and an existing Bacs instrument") + void shouldSendNotification() { + + final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") + .build(); + + final BacsNotificationResponse response = + blocking(() -> checkoutApi.bacsClient().sendNotification(request)); + + assertNotNull(response); + assertNotNull(response.getEventId()); + } +} diff --git a/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java b/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java new file mode 100644 index 000000000..97f83f764 --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java @@ -0,0 +1,158 @@ +package com.checkout.apm.bacs; + +import com.checkout.GsonSerializer; +import com.checkout.common.Currency; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the Bacs Direct Debit pre-notification endpoint. + */ +class BacsSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeAllPropertiesToSnakeCaseKeys() { + final String json = serializer.toJson(fullyPopulatedRequest()); + + assertTrue(json.contains("\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + assertTrue(json.contains("\"notification_type\":\"advance_notice\"")); + assertTrue(json.contains("\"collection_date\":\"2026-07-15\"")); + assertTrue(json.contains("\"amount\":4999")); + assertTrue(json.contains("\"currency\":\"GBP\"")); + assertTrue(json.contains("\"reference\":\"INV-12345\"")); + assertTrue(json.contains("\"customer_email\":\"customer@example.com\"")); + assertTrue(json.contains("\"billing_descriptor\":\"CHECKOUT\"")); + assertTrue(json.contains("\"support_email\":\"support@test.com\"")); + assertTrue(json.contains("\"support_phone\":\"+447700900123\"")); + } + + @Test + void shouldRoundTripAllProperties() { + final BacsNotificationRequest original = fullyPopulatedRequest(); + + final BacsNotificationRequest result = + serializer.fromJson(serializer.toJson(original), BacsNotificationRequest.class); + + assertEquals(original.getSourceId(), result.getSourceId()); + assertEquals(original.getNotificationType(), result.getNotificationType()); + assertEquals(original.getCollectionDate(), result.getCollectionDate()); + assertEquals(original.getAmount(), result.getAmount()); + assertEquals(original.getCurrency(), result.getCurrency()); + assertEquals(original.getReference(), result.getReference()); + assertEquals(original.getCustomerEmail(), result.getCustomerEmail()); + assertEquals(original.getBillingDescriptor(), result.getBillingDescriptor()); + assertEquals(original.getSupportEmail(), result.getSupportEmail()); + assertEquals(original.getSupportPhone(), result.getSupportPhone()); + } + + @Test + void shouldDeserializeSwaggerExample() { + final String json = "{" + + "\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"notification_type\":\"advance_notice\"," + + "\"collection_date\":\"2026-07-15\"," + + "\"amount\":4999," + + "\"currency\":\"GBP\"," + + "\"reference\":\"INV-12345\"," + + "\"customer_email\":\"customer@example.com\"," + + "\"billing_descriptor\":\"CHECKOUT\"," + + "\"support_email\":\"support@test.com\"," + + "\"support_phone\":\"+447700900123\"" + + "}"; + + final BacsNotificationRequest request = serializer.fromJson(json, BacsNotificationRequest.class); + + assertNotNull(request); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", request.getSourceId()); + assertEquals(BacsNotificationType.ADVANCE_NOTICE, request.getNotificationType()); + assertEquals(LocalDate.of(2026, 7, 15), request.getCollectionDate()); + assertEquals(4999L, request.getAmount()); + assertEquals(Currency.GBP, request.getCurrency()); + assertEquals("INV-12345", request.getReference()); + assertEquals("customer@example.com", request.getCustomerEmail()); + assertEquals("CHECKOUT", request.getBillingDescriptor()); + assertEquals("support@test.com", request.getSupportEmail()); + assertEquals("+447700900123", request.getSupportPhone()); + } + + @Test + void shouldOmitOptionalPropertiesWhenNotSet() { + final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("reference")); + assertFalse(json.contains("support_phone")); + assertTrue(json.contains("\"source_id\"")); + } + + @Test + void shouldHandleAbsentOptionalProperties() { + final String json = "{" + + "\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"notification_type\":\"advance_notice\"," + + "\"collection_date\":\"2026-07-15\"," + + "\"amount\":1," + + "\"currency\":\"GBP\"," + + "\"customer_email\":\"customer@example.com\"," + + "\"billing_descriptor\":\"CHECKOUT\"," + + "\"support_email\":\"support@test.com\"" + + "}"; + + final BacsNotificationRequest request = serializer.fromJson(json, BacsNotificationRequest.class); + + assertNull(request.getReference()); + assertNull(request.getSupportPhone()); + assertEquals(1L, request.getAmount()); + } + + @Test + void shouldDeserializeNotificationResponse() { + final BacsNotificationResponse response = serializer.fromJson( + "{\"event_id\":\"evt_lzr4csdtddwetactr6phd3kea4\"}", BacsNotificationResponse.class); + + assertNotNull(response); + assertEquals("evt_lzr4csdtddwetactr6phd3kea4", response.getEventId()); + } + + @Test + void shouldSerializeNotificationTypeBothDirections() { + assertEquals(1, BacsNotificationType.values().length); + assertEquals("\"advance_notice\"", serializer.toJson(BacsNotificationType.ADVANCE_NOTICE)); + assertEquals(BacsNotificationType.ADVANCE_NOTICE, + serializer.fromJson("\"advance_notice\"", BacsNotificationType.class)); + } + + private BacsNotificationRequest fullyPopulatedRequest() { + return BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") + .build(); + } +} diff --git a/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java b/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java new file mode 100644 index 000000000..5fd625a22 --- /dev/null +++ b/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java @@ -0,0 +1,36 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link InstrumentType}. + */ +class InstrumentTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeEveryValueBothDirections() { + final Map expected = new LinkedHashMap<>(); + expected.put(InstrumentType.BANK_ACCOUNT, "bank_account"); + expected.put(InstrumentType.TOKEN, "token"); + expected.put(InstrumentType.CARD, "card"); + expected.put(InstrumentType.CARD_TOKEN, "card_token"); + expected.put(InstrumentType.SEPA, "sepa"); + expected.put(InstrumentType.ACH, "ach"); + expected.put(InstrumentType.BACS, "bacs"); + + assertEquals(expected.size(), InstrumentType.values().length); + + expected.forEach((value, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(value)); + assertEquals(value, serializer.fromJson("\"" + wire + "\"", InstrumentType.class)); + }); + } +} diff --git a/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java b/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java new file mode 100644 index 000000000..6b34eaf20 --- /dev/null +++ b/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java @@ -0,0 +1,142 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link PaymentSourceType}. + * + *

The deprecated SEPA constant is asserted deliberately: it stays on the enum for backwards + * compatibility, so its wire value still has to be pinned. + */ +@SuppressWarnings("deprecation") +class PaymentSourceTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + /** + * SEPA and ID deliberately share the wire value "id", because the previous platform references a + * stored SEPA mandate through the generic "id" source. Gson resolves an incoming "id" to the + * constant declared last, so only the serialize direction is asserted for those two. + */ + private static final Map AMBIGUOUS_ON_READ = new LinkedHashMap<>(); + + static { + AMBIGUOUS_ON_READ.put(PaymentSourceType.SEPA, "id"); + AMBIGUOUS_ON_READ.put(PaymentSourceType.ID, "id"); + } + + @Test + void shouldSerializeEveryValueToItsWireString() { + wireValues().forEach((value, wire) -> + assertEquals("\"" + wire + "\"", serializer.toJson(value), value.name())); + } + + @Test + void shouldDeserializeEveryUnambiguousWireStringBackToItsValue() { + wireValues().forEach((value, wire) -> { + if (AMBIGUOUS_ON_READ.containsKey(value)) { + return; + } + assertEquals(value, serializer.fromJson("\"" + wire + "\"", PaymentSourceType.class), wire); + }); + } + + @Test + void shouldResolveTheSharedIdWireValueToTheLastDeclaredConstant() { + // SEPA is declared after ID, so Gson resolves "id" to SEPA. Reordering the two constants + // would silently change this, which is why the enum documents the ordering as load-bearing. + assertEquals(PaymentSourceType.SEPA, serializer.fromJson("\"id\"", PaymentSourceType.class)); + assertEquals("\"id\"", serializer.toJson(PaymentSourceType.ID)); + assertEquals("\"id\"", serializer.toJson(PaymentSourceType.SEPA)); + } + + @Test + void shouldSerializeBacsBothDirections() { + assertEquals("\"bacs\"", serializer.toJson(PaymentSourceType.BACS)); + assertEquals(PaymentSourceType.BACS, serializer.fromJson("\"bacs\"", PaymentSourceType.class)); + } + + @Test + void shouldSerializeCurrentPlatformSepaAsSepa() { + assertEquals("\"sepa\"", serializer.toJson(PaymentSourceType.SEPAV4)); + assertEquals(PaymentSourceType.SEPAV4, serializer.fromJson("\"sepa\"", PaymentSourceType.class)); + } + + private Map wireValues() { + final Map expected = new LinkedHashMap<>(); + expected.put(PaymentSourceType.ACH, "ach"); + expected.put(PaymentSourceType.AFTERPAY, "afterpay"); + expected.put(PaymentSourceType.ALIPAY, "alipay"); + expected.put(PaymentSourceType.ALIPAY_CN, "alipay_cn"); + expected.put(PaymentSourceType.ALIPAY_HK, "alipay_hk"); + expected.put(PaymentSourceType.ALIPAY_PLUS, "alipay_plus"); + expected.put(PaymentSourceType.ALMA, "alma"); + expected.put(PaymentSourceType.APPLEPAY, "applepay"); + expected.put(PaymentSourceType.BACS, "bacs"); + expected.put(PaymentSourceType.BANCONTACT, "bancontact"); + expected.put(PaymentSourceType.BANK_ACCOUNT, "bank_account"); + expected.put(PaymentSourceType.BENEFIT, "benefit"); + expected.put(PaymentSourceType.BENEFITPAY, "benefitpay"); + expected.put(PaymentSourceType.BIZUM, "bizum"); + expected.put(PaymentSourceType.BLIK, "blik"); + expected.put(PaymentSourceType.BOLETO, "boleto"); + expected.put(PaymentSourceType.CARD, "card"); + expected.put(PaymentSourceType.CURRENCY_ACCOUNT, "currency_account"); + expected.put(PaymentSourceType.CUSTOMER, "customer"); + expected.put(PaymentSourceType.CV_CONNECT, "cvconnect"); + expected.put(PaymentSourceType.DANA, "dana"); + expected.put(PaymentSourceType.DLOCAL, "dlocal"); + expected.put(PaymentSourceType.EPS, "eps"); + expected.put(PaymentSourceType.FAWRY, "fawry"); + expected.put(PaymentSourceType.GCASH, "gcash"); + expected.put(PaymentSourceType.GIROPAY, "giropay"); + expected.put(PaymentSourceType.GOOGLEPAY, "googlepay"); + expected.put(PaymentSourceType.ID, "id"); + expected.put(PaymentSourceType.IDEAL, "ideal"); + expected.put(PaymentSourceType.ILLICADO, "illicado"); + expected.put(PaymentSourceType.KAKAOPAY, "kakaopay"); + expected.put(PaymentSourceType.KLARNA, "klarna"); + expected.put(PaymentSourceType.KNET, "knet"); + expected.put(PaymentSourceType.MBWAY, "mbway"); + expected.put(PaymentSourceType.MOBILEPAY, "mobilepay"); + expected.put(PaymentSourceType.MULTIBANCO, "multibanco"); + expected.put(PaymentSourceType.NETWORK_TOKEN, "network_token"); + expected.put(PaymentSourceType.OCTOPUS, "octopus"); + expected.put(PaymentSourceType.OXXO, "oxxo"); + expected.put(PaymentSourceType.P24, "p24"); + expected.put(PaymentSourceType.PAGOFACIL, "pagofacil"); + expected.put(PaymentSourceType.PAYPAL, "paypal"); + expected.put(PaymentSourceType.PLAID, "plaid"); + expected.put(PaymentSourceType.POLI, "poli"); + expected.put(PaymentSourceType.POSTFINANCE, "postfinance"); + expected.put(PaymentSourceType.PROVIDER_TOKEN, "provider_token"); + expected.put(PaymentSourceType.QPAY, "qpay"); + expected.put(PaymentSourceType.RAPIPAGO, "rapipago"); + expected.put(PaymentSourceType.SEPA, "id"); + expected.put(PaymentSourceType.SEPAV4, "sepa"); + expected.put(PaymentSourceType.SEQURA, "sequra"); + expected.put(PaymentSourceType.SOFORT, "sofort"); + expected.put(PaymentSourceType.STCPAY, "stcpay"); + expected.put(PaymentSourceType.SWISH, "swish"); + expected.put(PaymentSourceType.TABBY, "tabby"); + expected.put(PaymentSourceType.TAMARA, "tamara"); + expected.put(PaymentSourceType.TOKEN, "token"); + expected.put(PaymentSourceType.TNG, "tng"); + expected.put(PaymentSourceType.TRUEMONEY, "truemoney"); + expected.put(PaymentSourceType.TRUSTLY, "trustly"); + expected.put(PaymentSourceType.TWINT, "twint"); + expected.put(PaymentSourceType.VIPPS, "vipps"); + expected.put(PaymentSourceType.WECHATPAY, "wechatpay"); + expected.put(PaymentSourceType.PAYNOW, "paynow"); + + assertEquals(expected.size(), PaymentSourceType.values().length, + "every constant must be listed so the enum cannot grow without this test noticing"); + return expected; + } +} diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java new file mode 100644 index 000000000..20122bfa9 --- /dev/null +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -0,0 +1,258 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateAchAccountHolder; +import com.checkout.instruments.create.CreateAchInstrumentData; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.update.AchInstrumentAccountType; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the ACH variants of the instruments endpoints. + */ +class AchInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // StoreAchInstrumentRequest - 13 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryStoreRequestProperty() { + final String json = serializer.toJson(fullyPopulatedStoreRequest()); + + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"account_type\":\"savings\"")); + assertTrue(json.contains("\"account_number\":\"4099999992\"")); + assertTrue(json.contains("\"bank_code\":\"211370545\"")); + assertTrue(json.contains("\"currency\":\"USD\"")); + assertTrue(json.contains("\"country\":\"US\"")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"company_name\":\"Smith Enterprises\"")); + assertTrue(json.contains("\"email\":\"customer@example.com\"")); + assertTrue(json.contains("\"name\":\"John Smith\"")); + assertTrue(json.contains("\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"")); + assertTrue(json.contains("\"default\":true")); + } + + @Test + void shouldRoundTripEveryStoreRequestProperty() { + final CreateInstrumentAchRequest result = serializer.fromJson( + serializer.toJson(fullyPopulatedStoreRequest()), CreateInstrumentAchRequest.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, result.getType()); + assertEquals(AchInstrumentAccountType.SAVINGS, result.getInstrumentData().getAccountType()); + assertEquals("4099999992", result.getInstrumentData().getAccountNumber()); + assertEquals("211370545", result.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, result.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, result.getInstrumentData().getCountry()); + assertEquals("John", result.getAccountHolder().getFirstName()); + assertEquals("Smith", result.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", result.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, result.getAccountHolder().getType()); + assertEquals("customer@example.com", result.getCustomer().getEmail()); + assertEquals("John Smith", result.getCustomer().getName()); + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", result.getCustomer().getId()); + assertTrue(result.getCustomer().isDefaultInstrument()); + } + + @Test + void shouldDeserializeStoreRequestSwaggerExample() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"instrument_data\":{" + + "\"account_type\":\"savings\"," + + "\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\"," + + "\"currency\":\"USD\"," + + "\"country\":\"US\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"company_name\":\"Smith Enterprises\"," + + "\"type\":\"individual\"" + + "}}"; + + final CreateInstrumentAchRequest request = + serializer.fromJson(json, CreateInstrumentAchRequest.class); + + assertNotNull(request); + assertEquals(AchInstrumentAccountType.SAVINGS, request.getInstrumentData().getAccountType()); + assertEquals(InstrumentAccountHolderType.INDIVIDUAL, request.getAccountHolder().getType()); + assertNull(request.getCustomer()); + } + + @Test + void shouldOmitAbsentOptionalStoreRequestProperties() { + final CreateInstrumentAchRequest request = CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.CHECKING) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.INDIVIDUAL) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("\"customer\"")); + assertTrue(json.contains("\"account_type\":\"checking\"")); + } + + // ------------------------------------------------------------------ + // RetrieveAchInstrumentResponse - 15 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeEveryRetrieveResponseProperty() { + final GetAchInstrumentResponse response = + serializer.fromJson(retrieveResponseJson(), GetAchInstrumentResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + assertEquals(Instant.parse("2021-01-01T00:00:00Z"), response.getCreatedOn()); + assertEquals(Instant.parse("2021-01-02T00:00:00Z"), response.getModifiedOn()); + assertEquals("vid_wmlfc3zyhqzehihu7giusaaawu", response.getVaultId()); + + assertEquals(AchInstrumentAccountType.CHECKING, response.getInstrumentData().getAccountType()); + assertEquals("4099999992", response.getInstrumentData().getAccountNumber()); + assertEquals("211370545", response.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, response.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, response.getInstrumentData().getCountry()); + + assertEquals("John", response.getAccountHolder().getFirstName()); + assertEquals("Smith", response.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", response.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, response.getAccountHolder().getType()); + + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", response.getCustomer().getId()); + assertEquals("customer@example.com", response.getCustomer().getEmail()); + assertTrue(response.getCustomer().isDefault()); + } + + @Test + void shouldRoundTripRetrieveResponse() { + final GetAchInstrumentResponse original = + serializer.fromJson(retrieveResponseJson(), GetAchInstrumentResponse.class); + + final GetAchInstrumentResponse result = serializer.fromJson( + serializer.toJson(original), GetAchInstrumentResponse.class); + + assertEquals(original.getVaultId(), result.getVaultId()); + assertEquals(original.getInstrumentData().getAccountType(), + result.getInstrumentData().getAccountType()); + assertEquals(original.getAccountHolder().getCompanyName(), + result.getAccountHolder().getCompanyName()); + } + + // ------------------------------------------------------------------ + // Store response + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeStoreResponse() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final CreateInstrumentAchResponse response = + serializer.fromJson(json, CreateInstrumentAchResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + @Test + void shouldSerializeAchAccountTypeBothDirections() { + assertEquals(2, AchInstrumentAccountType.values().length); + assertEquals("\"savings\"", serializer.toJson(AchInstrumentAccountType.SAVINGS)); + assertEquals("\"checking\"", serializer.toJson(AchInstrumentAccountType.CHECKING)); + assertEquals(AchInstrumentAccountType.SAVINGS, + serializer.fromJson("\"savings\"", AchInstrumentAccountType.class)); + assertEquals(AchInstrumentAccountType.CHECKING, + serializer.fromJson("\"checking\"", AchInstrumentAccountType.class)); + } + + private CreateInstrumentAchRequest fullyPopulatedStoreRequest() { + return CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.CORPORATE) + .build()) + .customer(CreateCustomerInstrumentRequest.builder() + .id("cus_udst2tfldj6upmye2reztkmm4i") + .email("customer@example.com") + .name("John Smith") + .defaultInstrument(true) + .build()) + .build(); + } + + private String retrieveResponseJson() { + return "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"modified_on\":\"2021-01-02T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{" + + "\"account_type\":\"checking\"," + + "\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\"," + + "\"currency\":\"USD\"," + + "\"country\":\"US\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"company_name\":\"Smith Enterprises\"," + + "\"type\":\"corporate\"" + + "}," + + "\"customer\":{" + + "\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"," + + "\"email\":\"customer@example.com\"," + + "\"default\":true" + + "}}"; + } +} diff --git a/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java new file mode 100644 index 000000000..7ec163834 --- /dev/null +++ b/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java @@ -0,0 +1,54 @@ +package com.checkout.instruments; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateAchAccountHolder; +import com.checkout.instruments.create.CreateAchInstrumentData; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.update.AchInstrumentAccountType; +import com.checkout.payments.AbstractPaymentsTestIT; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class AchInstrumentsTestIT extends AbstractPaymentsTestIT { + + @Test + @Disabled("Requires a merchant enabled for ACH Direct Debit") + void shouldCreateAndGetInstrumentAch() { + + final CreateInstrumentAchRequest request = CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.INDIVIDUAL) + .build()) + .build(); + + final CreateInstrumentAchResponse created = + blocking(() -> checkoutApi.instrumentsClient().create(request)); + + assertNotNull(created); + assertNotNull(created.getId()); + assertNotNull(created.getFingerprint()); + + final GetAchInstrumentResponse retrieved = + (GetAchInstrumentResponse) blocking(() -> checkoutApi.instrumentsClient().get(created.getId())); + + assertNotNull(retrieved); + assertNotNull(retrieved.getInstrumentData()); + assertNotNull(retrieved.getAccountHolder()); + assertNotNull(retrieved.getVaultId()); + } +} diff --git a/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java new file mode 100644 index 000000000..ef5cf5779 --- /dev/null +++ b/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java @@ -0,0 +1,446 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.instruments.update.UpdateBacsAccountHolder; +import com.checkout.instruments.update.UpdateBacsBillingAddress; +import com.checkout.instruments.update.UpdateBacsInstrumentData; +import com.checkout.instruments.update.UpdateInstrumentBacsRequest; +import com.checkout.instruments.update.UpdateInstrumentBacsResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the Bacs Direct Debit variants of the instruments endpoints. + */ +class BacsInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // StoreBacsInstrumentRequest - 20 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryStoreRequestProperty() { + final String json = serializer.toJson(fullyPopulatedStoreRequest()); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"processing_channel_id\":\"pc_q4dbxom5jbgudnjzjpz7j2z6uq\"")); + assertTrue(json.contains("\"account_number\":\"86753246\"")); + assertTrue(json.contains("\"bank_code\":\"040004\"")); + assertTrue(json.contains("\"currency\":\"GBP\"")); + assertTrue(json.contains("\"payment_type\":\"Recurring\"")); + assertTrue(json.contains("\"allow_partial_match\":true")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"address_line1\":\"Cloverfield St.\"")); + assertTrue(json.contains("\"address_line2\":\"23A\"")); + assertTrue(json.contains("\"city\":\"London\"")); + assertTrue(json.contains("\"zip\":\"SW1A 1AA\"")); + assertTrue(json.contains("\"country\":\"GB\"")); + assertTrue(json.contains("\"email\":\"customer@example.com\"")); + assertTrue(json.contains("\"name\":\"John Smith\"")); + assertTrue(json.contains("\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"")); + assertTrue(json.contains("\"default\":true")); + } + + @Test + void shouldRoundTripEveryStoreRequestProperty() { + final CreateInstrumentBacsRequest original = fullyPopulatedStoreRequest(); + + final CreateInstrumentBacsRequest result = + serializer.fromJson(serializer.toJson(original), CreateInstrumentBacsRequest.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, result.getType()); + assertEquals("pc_q4dbxom5jbgudnjzjpz7j2z6uq", result.getAccount().getProcessingChannelId()); + assertEquals("86753246", result.getInstrumentData().getAccountNumber()); + assertEquals("040004", result.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, result.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, result.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.RECURRING, result.getInstrumentData().getPaymentType()); + assertTrue(result.getInstrumentData().getAllowPartialMatch()); + assertEquals("John", result.getAccountHolder().getFirstName()); + assertEquals("Smith", result.getAccountHolder().getLastName()); + assertEquals("Cloverfield St.", result.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("23A", result.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", result.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", result.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, result.getAccountHolder().getBillingAddress().getCountry()); + assertEquals("customer@example.com", result.getCustomer().getEmail()); + assertEquals("John Smith", result.getCustomer().getName()); + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", result.getCustomer().getId()); + assertTrue(result.getCustomer().isDefaultInstrument()); + } + + @Test + void shouldDeserializeStoreRequestSwaggerExample() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"account\":{\"processing_channel_id\":\"pc_q4dbxom5jbgudnjzjpz7j2z6uq\"}," + + "\"instrument_data\":{" + + "\"account_number\":\"86753246\"," + + "\"bank_code\":\"040004\"," + + "\"country\":\"GB\"," + + "\"currency\":\"GBP\"," + + "\"payment_type\":\"Recurring\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"billing_address\":{" + + "\"address_line1\":\"Cloverfield St.\"," + + "\"address_line2\":\"23A\"," + + "\"city\":\"London\"," + + "\"zip\":\"SW1A 1AA\"," + + "\"country\":\"GB\"" + + "}}}"; + + final CreateInstrumentBacsRequest request = + serializer.fromJson(json, CreateInstrumentBacsRequest.class); + + assertNotNull(request); + assertEquals(BacsPaymentType.RECURRING, request.getInstrumentData().getPaymentType()); + assertNull(request.getInstrumentData().getAllowPartialMatch()); + assertNull(request.getCustomer()); + } + + @Test + void shouldOmitAbsentOptionalStoreRequestProperties() { + final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.REGULAR) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .country(CountryCode.GB) + .build()) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("allow_partial_match")); + assertFalse(json.contains("\"customer\"")); + assertTrue(json.contains("\"payment_type\":\"Regular\"")); + } + + // ------------------------------------------------------------------ + // UpdateBacsInstrumentRequest - 19 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryUpdateRequestProperty() { + final String json = serializer.toJson(fullyPopulatedUpdateRequest()); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"account_number\":\"86753246\"")); + assertTrue(json.contains("\"bank_code\":\"040004\"")); + assertTrue(json.contains("\"payment_type\":\"Regular\"")); + assertTrue(json.contains("\"allow_partial_match\":true")); + assertTrue(json.contains("\"first_name\":\"Hannah\"")); + assertTrue(json.contains("\"last_name\":\"Bret\"")); + assertTrue(json.contains("\"company_name\":\"Bret Holdings Ltd\"")); + assertTrue(json.contains("\"address_line1\":\"123 High St.\"")); + assertTrue(json.contains("\"address_line2\":\"Flat 456\"")); + assertTrue(json.contains("\"city\":\"London\"")); + assertTrue(json.contains("\"zip\":\"SW1A 1AA\"")); + assertTrue(json.contains("\"country\":\"GB\"")); + assertTrue(json.contains("\"type\":\"corporate\"")); + } + + @Test + void shouldRoundTripEveryUpdateRequestProperty() { + final UpdateInstrumentBacsRequest result = serializer.fromJson( + serializer.toJson(fullyPopulatedUpdateRequest()), UpdateInstrumentBacsRequest.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, result.getType()); + assertEquals("86753246", result.getInstrumentData().getAccountNumber()); + assertEquals("040004", result.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, result.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, result.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.REGULAR, result.getInstrumentData().getPaymentType()); + assertTrue(result.getInstrumentData().getAllowPartialMatch()); + assertEquals("Hannah", result.getAccountHolder().getFirstName()); + assertEquals("Bret", result.getAccountHolder().getLastName()); + assertEquals("Bret Holdings Ltd", result.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, result.getAccountHolder().getType()); + assertEquals("123 High St.", result.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("Flat 456", result.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", result.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", result.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, result.getAccountHolder().getBillingAddress().getCountry()); + } + + @Test + void shouldSerializeEmptyUpdateRequestWithTypeOnly() { + final String json = serializer.toJson(new UpdateInstrumentBacsRequest()); + + assertEquals("{\"type\":\"bacs\"}", json); + } + + // ------------------------------------------------------------------ + // RetrieveBacsInstrumentResponse - 31 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeEveryRetrieveResponseProperty() { + final GetBacsInstrumentResponse response = + serializer.fromJson(retrieveResponseJson(), GetBacsInstrumentResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + assertEquals(Instant.parse("2021-01-01T00:00:00Z"), response.getCreatedOn()); + assertEquals(Instant.parse("2021-01-02T00:00:00Z"), response.getModifiedOn()); + assertEquals("vid_wmlfc3zyhqzehihu7giusaaawu", response.getVaultId()); + + assertEquals("cli_memowvltf7aulpb3poehtiffei", response.getAccount().getClientId()); + assertEquals("pc_jcs4ufa6hrgepcrvhic4bfspay", response.getAccount().getProcessingChannelId()); + + assertEquals(1, response.getValidations().size()); + assertEquals("account_number", response.getValidations().get(0).get("field")); + + assertEquals("86753246", response.getInstrumentData().getAccountNumber()); + assertEquals("040004", response.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, response.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, response.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.RECURRING, response.getInstrumentData().getPaymentType()); + assertTrue(response.getInstrumentData().getAllowPartialMatch()); + assertEquals("INVALID", response.getInstrumentData().getStatus()); + assertEquals("no match", response.getInstrumentData().getMatchStatus()); + assertEquals("The name did not match with the account owner.", + response.getInstrumentData().getDescription()); + assertEquals("6PZ6KFI3KW3UFHAM3J", response.getInstrumentData().getMandateId()); + + assertEquals("Hannah", response.getAccountHolder().getFirstName()); + assertEquals("Bret", response.getAccountHolder().getLastName()); + assertEquals("Bret Holdings Ltd", response.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, response.getAccountHolder().getType()); + assertEquals("123 High St.", response.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("Flat 456", response.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", response.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", response.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, response.getAccountHolder().getBillingAddress().getCountry()); + + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", response.getCustomer().getId()); + assertEquals("customer@example.com", response.getCustomer().getEmail()); + assertTrue(response.getCustomer().isDefault()); + } + + @Test + void shouldRoundTripRetrieveResponseValidations() { + final GetBacsInstrumentResponse original = + serializer.fromJson(retrieveResponseJson(), GetBacsInstrumentResponse.class); + + final GetBacsInstrumentResponse result = serializer.fromJson( + serializer.toJson(original), GetBacsInstrumentResponse.class); + + assertEquals(1, result.getValidations().size()); + assertEquals("no match", result.getValidations().get(0).get("result")); + } + + // ------------------------------------------------------------------ + // Store and update responses + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeStoreResponse() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final CreateInstrumentBacsResponse response = + serializer.fromJson(json, CreateInstrumentBacsResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentBacsResponse response = + serializer.fromJson(json, UpdateInstrumentBacsResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + // ------------------------------------------------------------------ + // BacsPaymentType - the casing regression guard for T1 + // ------------------------------------------------------------------ + + @Test + void shouldSerializeBacsPaymentTypeCapitalized() { + assertEquals("\"Recurring\"", serializer.toJson(BacsPaymentType.RECURRING)); + assertEquals("\"Regular\"", serializer.toJson(BacsPaymentType.REGULAR)); + assertEquals(BacsPaymentType.RECURRING, + serializer.fromJson("\"Recurring\"", BacsPaymentType.class)); + assertEquals(BacsPaymentType.REGULAR, + serializer.fromJson("\"Regular\"", BacsPaymentType.class)); + } + + @Test + void shouldKeepSepaPaymentTypeLowercase() { + assertEquals("\"recurring\"", serializer.toJson(SepaPaymentType.RECURRING)); + assertEquals("\"regular\"", serializer.toJson(SepaPaymentType.REGULAR)); + assertEquals(SepaPaymentType.RECURRING, + serializer.fromJson("\"recurring\"", SepaPaymentType.class)); + assertEquals(SepaPaymentType.REGULAR, + serializer.fromJson("\"regular\"", SepaPaymentType.class)); + } + + @Test + void shouldSerializeInstrumentAccountHolderTypeBothDirections() { + assertEquals("\"individual\"", serializer.toJson(InstrumentAccountHolderType.INDIVIDUAL)); + assertEquals("\"corporate\"", serializer.toJson(InstrumentAccountHolderType.CORPORATE)); + assertEquals(InstrumentAccountHolderType.INDIVIDUAL, + serializer.fromJson("\"individual\"", InstrumentAccountHolderType.class)); + assertEquals(InstrumentAccountHolderType.CORPORATE, + serializer.fromJson("\"corporate\"", InstrumentAccountHolderType.class)); + } + + private CreateInstrumentBacsRequest fullyPopulatedStoreRequest() { + return CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .allowPartialMatch(true) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .addressLine1("Cloverfield St.") + .addressLine2("23A") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .customer(CreateCustomerInstrumentRequest.builder() + .id("cus_udst2tfldj6upmye2reztkmm4i") + .email("customer@example.com") + .name("John Smith") + .defaultInstrument(true) + .build()) + .build(); + } + + private UpdateInstrumentBacsRequest fullyPopulatedUpdateRequest() { + return UpdateInstrumentBacsRequest.builder() + .instrumentData(UpdateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.REGULAR) + .allowPartialMatch(true) + .build()) + .accountHolder(UpdateBacsAccountHolder.builder() + .firstName("Hannah") + .lastName("Bret") + .companyName("Bret Holdings Ltd") + .type(InstrumentAccountHolderType.CORPORATE) + .billingAddress(UpdateBacsBillingAddress.builder() + .addressLine1("123 High St.") + .addressLine2("Flat 456") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .build(); + } + + private String retrieveResponseJson() { + return "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"modified_on\":\"2021-01-02T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"account\":{" + + "\"client_id\":\"cli_memowvltf7aulpb3poehtiffei\"," + + "\"processing_channel_id\":\"pc_jcs4ufa6hrgepcrvhic4bfspay\"" + + "}," + + "\"validations\":[{\"field\":\"account_number\",\"result\":\"no match\"}]," + + "\"instrument_data\":{" + + "\"account_number\":\"86753246\"," + + "\"bank_code\":\"040004\"," + + "\"country\":\"GB\"," + + "\"currency\":\"GBP\"," + + "\"payment_type\":\"Recurring\"," + + "\"allow_partial_match\":true," + + "\"status\":\"INVALID\"," + + "\"match_status\":\"no match\"," + + "\"description\":\"The name did not match with the account owner.\"," + + "\"mandate_id\":\"6PZ6KFI3KW3UFHAM3J\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"Hannah\"," + + "\"last_name\":\"Bret\"," + + "\"company_name\":\"Bret Holdings Ltd\"," + + "\"type\":\"corporate\"," + + "\"billing_address\":{" + + "\"address_line1\":\"123 High St.\"," + + "\"address_line2\":\"Flat 456\"," + + "\"city\":\"London\"," + + "\"zip\":\"SW1A 1AA\"," + + "\"country\":\"GB\"" + + "}}," + + "\"customer\":{" + + "\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"," + + "\"email\":\"customer@example.com\"," + + "\"default\":true" + + "}}"; + } +} diff --git a/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java new file mode 100644 index 000000000..ef960a301 --- /dev/null +++ b/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java @@ -0,0 +1,54 @@ +package com.checkout.instruments; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.payments.AbstractPaymentsTestIT; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class BacsInstrumentsTestIT extends AbstractPaymentsTestIT { + + @Test + @Disabled("Requires a merchant enabled for Bacs Direct Debit") + void shouldCreateInstrumentBacs() { + + final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .addressLine1("Cloverfield St.") + .addressLine2("23A") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .build(); + + final CreateInstrumentBacsResponse response = + blocking(() -> checkoutApi.instrumentsClient().create(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getFingerprint()); + } +} diff --git a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java new file mode 100644 index 000000000..7102e0d5c --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java @@ -0,0 +1,96 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.create.CreateInstrumentResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; +import com.checkout.instruments.get.GetInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentBacsResponse; +import com.checkout.instruments.update.UpdateInstrumentResponse; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Polymorphic dispatch tests for the Bacs Direct Debit and ACH instrument responses. + * + *

Before these subtypes were registered on the three instrument factories, every assertion here + * failed with a JsonParseException, because none of the factories declares a default subtype. ACH + * was already registered on the update factory but not on create or get. + */ +class InstrumentResponseDispatchTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldDispatchCreateResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + + final CreateInstrumentBacsResponse bacs = assertInstanceOf(CreateInstrumentBacsResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); + } + + @Test + void shouldDispatchUpdateResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentBacsResponse bacs = assertInstanceOf(UpdateInstrumentBacsResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", bacs.getFingerprint()); + } + + @Test + void shouldDispatchCreateResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + + final CreateInstrumentAchResponse ach = assertInstanceOf(CreateInstrumentAchResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", ach.getId()); + } + + @Test + void shouldDispatchGetResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{\"account_type\":\"savings\",\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\",\"currency\":\"USD\",\"country\":\"US\"}}"; + + final GetInstrumentResponse response = serializer.fromJson(json, GetInstrumentResponse.class); + + final GetAchInstrumentResponse ach = assertInstanceOf(GetAchInstrumentResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals(com.checkout.instruments.update.AchInstrumentAccountType.SAVINGS, + ach.getInstrumentData().getAccountType()); + } + + @Test + void shouldDispatchGetResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{\"account_number\":\"86753246\",\"bank_code\":\"040004\"," + + "\"country\":\"GB\",\"currency\":\"GBP\",\"payment_type\":\"Recurring\"}}"; + + final GetInstrumentResponse response = serializer.fromJson(json, GetInstrumentResponse.class); + + final GetBacsInstrumentResponse bacs = assertInstanceOf(GetBacsInstrumentResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals(BacsPaymentType.RECURRING, bacs.getInstrumentData().getPaymentType()); + } +} diff --git a/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java b/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java new file mode 100644 index 000000000..2c116a289 --- /dev/null +++ b/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java @@ -0,0 +1,54 @@ +package com.checkout.payments.previous.request.source.apm; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Serialization tests for the previous-platform SEPA source. + * + *

These pin the wire contract that the previous platform references a stored SEPA mandate + * through the generic "id" source. The source class was switched from the deprecated + * PaymentSourceType.SEPA to PaymentSourceType.ID, which both map to "id", so these assertions must + * hold identically before and after that change. + */ +class RequestSepaSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeTypeAsIdAndNotSepa() { + final RequestSepaSource source = RequestSepaSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final String json = serializer.toJson(source); + + assertTrue(json.contains("\"type\":\"id\"")); + assertFalse(json.contains("\"sepa\"")); + assertTrue(json.contains("\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + } + + @Test + void shouldUseTheIdSourceTypeConstant() { + assertEquals(PaymentSourceType.ID, new RequestSepaSource().getType()); + assertEquals(PaymentSourceType.ID, + RequestSepaSource.builder().id("src_wmlfc3zyhqzehihu7giusaaawu").build().getType()); + } + + @Test + void shouldMatchTheCurrentPlatformSourceOnEverythingButTheType() { + final String previousJson = serializer.toJson(RequestSepaSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build()); + final String currentJson = serializer.toJson( + com.checkout.payments.request.source.apm.RequestSepaSource.builder().build()); + + assertTrue(previousJson.contains("\"type\":\"id\"")); + assertTrue(currentJson.contains("\"type\":\"sepa\"")); + } +} diff --git a/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java b/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java new file mode 100644 index 000000000..cfaa692c0 --- /dev/null +++ b/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java @@ -0,0 +1,48 @@ +package com.checkout.payments.request.source.apm; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for {@link RequestBacsSource}. + */ +class RequestBacsSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeTypeAndId() { + final RequestBacsSource source = RequestBacsSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final String json = serializer.toJson(source); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + } + + @Test + void shouldRoundTripTypeAndId() { + final RequestBacsSource original = RequestBacsSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final RequestBacsSource result = + serializer.fromJson(serializer.toJson(original), RequestBacsSource.class); + + assertNotNull(result); + assertEquals(PaymentSourceType.BACS, result.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", result.getId()); + } + + @Test + void shouldDefaultTypeOnNoArgsConstructor() { + assertEquals(PaymentSourceType.BACS, new RequestBacsSource().getType()); + } +} diff --git a/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java b/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java new file mode 100644 index 000000000..3c1a1ef3f --- /dev/null +++ b/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java @@ -0,0 +1,40 @@ +package com.checkout.payments.response.source; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Schema validation tests for {@link BacsResponseSource}, including the polymorphic dispatch that + * selects it instead of the alternative payment source fallback. + */ +class BacsResponseSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldDeserializeTypeAndId() { + final BacsResponseSource source = serializer.fromJson( + "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"}", + BacsResponseSource.class); + + assertNotNull(source); + assertEquals(PaymentSourceType.BACS, source.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", source.getId()); + } + + @Test + void shouldDispatchToTypedSourceAndNotTheFallback() { + final ResponseSource source = serializer.fromJson( + "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"}", + ResponseSource.class); + + final BacsResponseSource bacs = assertInstanceOf(BacsResponseSource.class, source); + assertEquals(PaymentSourceType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); + } +} From 2d702433fcc323aa7a9256470b47ad2d11fb87cd Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 09:57:27 +0200 Subject: [PATCH 02/18] Id field fixes, test fixes and some other serialization fixes --- .../java/com/checkout/GsonSerializer.java | 3 + .../checkout/common/PaymentMethodType.java | 20 ++- .../payments/common/source/SourceType.java | 9 ++ .../common/source/achsource/AchSource.java | 43 +++++++ .../source/alipaycnsource/AlipayCnSource.java | 16 +++ .../common/source/bacssource/BacsSource.java | 43 +++++++ .../bankaccountsource/BankAccountSource.java | 43 +++++++ .../instruments/create/InstrumentData.java | 50 ++++++-- .../update/UpdateInstrumentAchResponse.java | 15 +++ .../update/UpdateInstrumentBacsResponse.java | 3 - .../update/UpdateInstrumentResponse.java | 13 ++ .../update/UpdateInstrumentSepaResponse.java | 15 +++ .../RuntimeTypeAdapterFactory.java | 7 +- .../checkout/ScratchDiscriminatorProbe.java | 85 +++++++++++++ .../PaymentMethodTypeSerializationTest.java | 118 ++++++++++++++++++ ...ayoutResponseCreatedSerializationTest.java | 66 ++++++++++ .../AchInstrumentSerializationTest.java | 20 ++- .../InstrumentResponseDispatchTest.java | 35 +++++- ...a => SepaInstrumentSerializationTest.java} | 86 ++++++++++++- .../instruments/SepaInstrumentsTestIT.java | 4 +- .../schema/LocalDateFieldsRegressionTest.java | 6 +- 21 files changed, 667 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java create mode 100644 src/test/java/com/checkout/ScratchDiscriminatorProbe.java create mode 100644 src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java rename src/test/java/com/checkout/instruments/{CreateInstrumentSepaRequestSerializationTest.java => SepaInstrumentSerializationTest.java} (67%) diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index ed0c0e0eb..5e09d4b75 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -115,6 +115,9 @@ public final class GsonSerializer implements Serializer { CheckoutUtils.TYPE ) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CARD)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ACH)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource.BacsSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BACS)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource.BankAccountSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BANK_ACCOUNT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.afterpaysource.AfterpaySource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.AFTERPAY)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.alipaycnsource.AlipayCnSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ALIPAY_CN)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.alipayhksource.AlipayHkSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ALIPAY_HK)) diff --git a/src/main/java/com/checkout/common/PaymentMethodType.java b/src/main/java/com/checkout/common/PaymentMethodType.java index 95a14ebf2..f18e1baaf 100644 --- a/src/main/java/com/checkout/common/PaymentMethodType.java +++ b/src/main/java/com/checkout/common/PaymentMethodType.java @@ -3,14 +3,19 @@ import com.google.gson.annotations.SerializedName; /** - * Comprehensive enum for all payment method types across different APIs - * Consolidates values from payment methods, flow APIs, and other payment contexts + * The type of payment method. + * + *

This is a consolidated enum covering every payment method type the SDK sees, across + * GET /payment-methods, the flow API and the payment session entities. The specification's + * PaymentMethod.type enum is a subset of it, so the constants declared here that the current + * specification does not list are deliberate rather than invented, and must not be removed by a + * reverse comparison against a single endpoint. */ public enum PaymentMethodType { @SerializedName("accel") ACCEL, - @SerializedName("ach") + @SerializedName("ach") ACH, @SerializedName("alipay_cn") ALIPAY_CN, @@ -24,6 +29,8 @@ public enum PaymentMethodType { AMEX, @SerializedName("applepay") APPLEPAY, + @SerializedName("bacs") + BACS, @SerializedName("bancontact") BANCONTACT, @SerializedName("benefit") @@ -100,6 +107,10 @@ public enum PaymentMethodType { QPAY, @SerializedName("rabbit_line_pay") RABBIT_LINE_PAY, + /** + * Fully supported by the API but deliberately unlisted in the public specification, so that + * merchants do not disable Remember Me en masse. Do not remove it as an unspecified value. + */ @SerializedName("remember_me") REMEMBER_ME, @SerializedName("sepa") @@ -137,7 +148,8 @@ public enum PaymentMethodType { @SerializedName("wechatpay") WECHATPAY, - // Payment method categories + // Payment method categories. These are grouping values used by the flow and payment session + // entities, not values of the specification's PaymentMethod.type enum. @SerializedName("card_scheme") CARD_SCHEME, @SerializedName("bank_redirects") diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java index 2c78a26f9..0e73cf763 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java @@ -7,6 +7,9 @@ public enum SourceType { @SerializedName("card") CARD, + @SerializedName("ach") + ACH, + @SerializedName("afterpay") AFTERPAY, @@ -22,9 +25,15 @@ public enum SourceType { @SerializedName("alma") ALMA, + @SerializedName("bacs") + BACS, + @SerializedName("bancontact") BANCONTACT, + @SerializedName("bank_account") + BANK_ACCOUNT, + @SerializedName("benefit") BENEFIT, diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java new file mode 100644 index 000000000..e75338af3 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.achsource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * ach source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class AchSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the AchSource class. + */ + @Builder + private AchSource( + final String id + ) { + super(SourceType.ACH); + this.id = id; + } + + public AchSource() { + super(SourceType.ACH); + } + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java index 196ada801..328cd939e 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java @@ -2,6 +2,7 @@ import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -17,9 +18,24 @@ @ToString(callSuper = true) public final class AlipayCnSource extends AbstractSource { + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + /** * Initializes a new instance of the AlipayCnSource class. */ + @Builder + private AlipayCnSource( + final String id + ) { + super(SourceType.ALIPAY_CN); + this.id = id; + } + public AlipayCnSource() { super(SourceType.ALIPAY_CN); } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java new file mode 100644 index 000000000..7097b2187 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * bacs source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BacsSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the BacsSource class. + */ + @Builder + private BacsSource( + final String id + ) { + super(SourceType.BACS); + this.id = id; + } + + public BacsSource() { + super(SourceType.BACS); + } + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java new file mode 100644 index 000000000..327fce082 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * bank_account source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BankAccountSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the BankAccountSource class. + */ + @Builder + private BankAccountSource( + final String id + ) { + super(SourceType.BANK_ACCOUNT); + this.id = id; + } + + public BankAccountSource() { + super(SourceType.BANK_ACCOUNT); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/InstrumentData.java b/src/main/java/com/checkout/instruments/create/InstrumentData.java index 0e530e18e..8bde35dd1 100644 --- a/src/main/java/com/checkout/instruments/create/InstrumentData.java +++ b/src/main/java/com/checkout/instruments/create/InstrumentData.java @@ -2,7 +2,8 @@ import com.checkout.common.CountryCode; import com.checkout.common.Currency; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.payments.request.source.apm.MandateType; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; @@ -11,6 +12,13 @@ import java.time.LocalDate; +/** + * The details of the SEPA mandate being stored. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs Direct + * Debit field is capitalized, so the two must not share a type: sending + * {@link com.checkout.instruments.BacsPaymentType} values here produces a request the API rejects. + */ @Data @Builder @AllArgsConstructor @@ -18,38 +26,58 @@ public final class InstrumentData { /** - * The SEPA account number. + * The type of mandate. * [Optional] + * Enum: "Core" "B2B" + */ + private MandateType type; + + /** + * The International Bank Account Number (IBAN) of the account. + * [Required] + * min 15 characters + * max 34 characters + * + *

The Java field name carries a typo. The wire name is correct because of the explicit + * annotation, so this is a source-only defect; renaming the field is breaking and is deferred + * to the next major version. */ @SerializedName("account_number") private String accoountNumber; /** - * The country of the SEPA account. - * [Optional] + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters */ private CountryCode country; /** - * The currency of the SEPA account. - * [Optional] + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters */ private Currency currency; /** - * The payment type for this instrument. - * [Optional] + * The type of payment. recurring or regular. + * [Required] */ - private PaymentType paymentType; + private SepaPaymentType paymentType; /** - * The unique identifier of the SEPA mandate. + * The mandate ID. If a mandate ID is not provided, a new, random mandate ID will be generated. * [Optional] + * min 1 characters + * max 35 characters */ private String mandateId; /** - * The date the mandate was signed. + * The date on which the mandate was signed. Required if mandateId is provided. Ignored and set + * as the current date if mandateId is not provided. * [Optional] * Format: yyyy-MM-dd */ diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java index cb825724e..56b8546a1 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java @@ -5,11 +5,26 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update ACH bank account instrument response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentAchResponse extends UpdateInstrumentResponse { + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ private final InstrumentType type = InstrumentType.ACH; + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java index 76fbf0af4..e5c0be2de 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java @@ -7,9 +7,6 @@ /** * Update Bacs Direct Debit account instrument response. - * - *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The id is declared here - * because the shared update response does not carry it. */ @Data @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java index 72483baa2..b2aa84b82 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java @@ -4,10 +4,23 @@ import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The response for the type of instrument updated. + * + *

The id is deliberately not declared here. The specification declares it on the sepa, ach and + * bacs update responses only, so each of those three variants carries its own. The bank_account and + * card update responses return a type and a fingerprint but no id, and would expose an always-null + * property if the id lived on this base class. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class UpdateInstrumentResponse extends HttpMetadata { + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ private String fingerprint; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java index 87f433785..90c379c55 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java @@ -5,11 +5,26 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update SEPA account instrument response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentSepaResponse extends UpdateInstrumentResponse { + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + } diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 716164053..799577e00 100644 --- a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -270,13 +270,12 @@ public void write(final JsonWriter out, final R value) throws IOException { final JsonObject clone = new JsonObject(); - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } clone.add(typeFieldName, new JsonPrimitive(label)); for (final Map.Entry e : jsonObject.entrySet()) { + if (typeFieldName.equals(e.getKey())) { + continue; + } clone.add(e.getKey(), e.getValue()); } Streams.write(clone, out); diff --git a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java new file mode 100644 index 000000000..f20fac506 --- /dev/null +++ b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java @@ -0,0 +1,85 @@ +package com.checkout; + +import com.checkout.issuing.controls.requests.VelocityLimit; +import com.checkout.issuing.controls.requests.VelocityWindow; +import com.checkout.issuing.controls.requests.VelocityWindowType; +import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; +import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +class ScratchDiscriminatorProbe { + + private final GsonSerializer serializer = new GsonSerializer(); + + private void probe(final String label, final Object value) { + try { + System.out.println("PROBE " + label + " => OK: " + serializer.toJson(value)); + } catch (final RuntimeException e) { + System.out.println("PROBE " + label + " => THROWS: " + e.getMessage()); + } + } + + private void probeRoundTrip(final String label, final String json, final Class type) { + try { + final Object parsed = serializer.fromJson(json, type); + probe(label, parsed); + } catch (final RuntimeException e) { + System.out.println("PROBE " + label + " => READ-THROWS: " + e.getMessage()); + } + } + + @Test + void probeAll() { + // 1. REQUEST path - issuing control groups + probe("CreateControlGroupRequest(request!)", CreateControlGroupRequest.builder() + .description("g") + .controls(Collections.singletonList(VelocityControlGroupControl.builder() + .description("c") + .velocityLimit(VelocityLimit.builder() + .amountLimit(1000) + .velocityWindow(VelocityWindow.builder().type(VelocityWindowType.DAILY).build()) + .build()) + .build())) + .build()); + + // 2. customers - List + probeRoundTrip("CustomerResponse.instruments", + "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\",\"fingerprint\":\"f\"}]}", + com.checkout.customers.CustomerResponse.class); + + // 3. workflows - List + List + probeRoundTrip("GetWorkflowResponse.actions+conditions", + "{\"id\":\"wf_1\",\"name\":\"n\"," + + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," + + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://x.test\"}]}", + com.checkout.workflows.GetWorkflowResponse.class); + + // 4. accounts payout schedule - ScheduleResponse recurrence + probeRoundTrip("CurrencySchedule.recurrence", + "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"daily\"}}", + com.checkout.accounts.payout.schedule.response.CurrencySchedule.class); + + // 5. issuing cardholder cards - List + probeRoundTrip("CardholderCardsResponse.cards", + "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", + com.checkout.issuing.cardholders.CardholderCardsResponse.class); + + // 6. issuing card controls query - List + probeRoundTrip("CardControlsQueryResponse.controls", + "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", + com.checkout.issuing.controls.responses.query.CardControlsQueryResponse.class); + + // 7. issuing control group response - List + probeRoundTrip("ControlGroupResponse.controls", + "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\",\"description\":\"d\"}]}", + com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse.class); + + // 8. the payments source hierarchy touched by this branch + probeRoundTrip("RequestAPaymentOrPayoutResponseCreated.source", + "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", + com.checkout.handlepaymentsandpayouts.payments.postpayments.responses + .requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated.class); + } +} diff --git a/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java b/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java new file mode 100644 index 000000000..e50ea3d4e --- /dev/null +++ b/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java @@ -0,0 +1,118 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link PaymentMethodType}. + * + *

This enum is a consolidated union: it carries every payment method type the SDK sees across + * GET /payment-methods, the flow API and the payment session entities. The specification's + * PaymentMethod.type enum is therefore a subset of it, and the values this enum adds on top + * (applepay, card, googlepay, stored_card, wallet, bnpl, bank_redirects, and the APM values the + * flow API returns) are deliberate rather than invented. remember_me is supported by the API but + * deliberately unlisted in the public specification. + * + *

The full map below is the guard: GET /payment-methods maps a value it does not recognise to + * null rather than failing, so a missing constant is silent. bacs went missing that way. + */ +class PaymentMethodTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeEveryValueBothDirections() { + wireValues().forEach((value, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(value), value.name()); + assertEquals(value, serializer.fromJson("\"" + wire + "\"", PaymentMethodType.class), wire); + }); + } + + @Test + void shouldSerializeBacsBothDirections() { + assertEquals("\"bacs\"", serializer.toJson(PaymentMethodType.BACS)); + assertEquals(PaymentMethodType.BACS, serializer.fromJson("\"bacs\"", PaymentMethodType.class)); + } + + private Map wireValues() { + final Map expected = new LinkedHashMap<>(); + expected.put(PaymentMethodType.ACCEL, "accel"); + expected.put(PaymentMethodType.ACH, "ach"); + expected.put(PaymentMethodType.ALIPAY_CN, "alipay_cn"); + expected.put(PaymentMethodType.ALIPAY_HK, "alipay_hk"); + expected.put(PaymentMethodType.ALIPAY_PLUS, "alipay_plus"); + expected.put(PaymentMethodType.ALMA, "alma"); + expected.put(PaymentMethodType.AMEX, "amex"); + expected.put(PaymentMethodType.APPLEPAY, "applepay"); + expected.put(PaymentMethodType.BACS, "bacs"); + expected.put(PaymentMethodType.BANCONTACT, "bancontact"); + expected.put(PaymentMethodType.BANK_REDIRECTS, "bank_redirects"); + expected.put(PaymentMethodType.BENEFIT, "benefit"); + expected.put(PaymentMethodType.BIZUM, "bizum"); + expected.put(PaymentMethodType.BLIK, "blik"); + expected.put(PaymentMethodType.BNPL, "bnpl"); + expected.put(PaymentMethodType.BOOST, "boost"); + expected.put(PaymentMethodType.BPI, "bpi"); + expected.put(PaymentMethodType.CARD, "card"); + expected.put(PaymentMethodType.CARD_SCHEME, "card_scheme"); + expected.put(PaymentMethodType.CARTES_BANCAIRES, "cartes_bancaires"); + expected.put(PaymentMethodType.CHINA_UNION_PAY, "china_union_pay"); + expected.put(PaymentMethodType.CONNECT_WALLET, "connect_wallet"); + expected.put(PaymentMethodType.DANA, "dana"); + expected.put(PaymentMethodType.DCI, "dci"); + expected.put(PaymentMethodType.DINERS, "diners"); + expected.put(PaymentMethodType.DISCOVER, "discover"); + expected.put(PaymentMethodType.EPS, "eps"); + expected.put(PaymentMethodType.GCASH, "gcash"); + expected.put(PaymentMethodType.GOOGLEPAY, "googlepay"); + expected.put(PaymentMethodType.IDEAL, "ideal"); + expected.put(PaymentMethodType.JCB, "jcb"); + expected.put(PaymentMethodType.KAKAOPAY, "kakaopay"); + expected.put(PaymentMethodType.KLARNA, "klarna"); + expected.put(PaymentMethodType.KNET, "knet"); + expected.put(PaymentMethodType.MADA, "mada"); + expected.put(PaymentMethodType.MASTERCARD, "mastercard"); + expected.put(PaymentMethodType.MBWAY, "mbway"); + expected.put(PaymentMethodType.MOBILEPAY, "mobilepay"); + expected.put(PaymentMethodType.MULTIBANCO, "multibanco"); + expected.put(PaymentMethodType.NYCE, "nyce"); + expected.put(PaymentMethodType.OCTOPUS, "octopus"); + expected.put(PaymentMethodType.OMANNET, "omannet"); + expected.put(PaymentMethodType.P24, "p24"); + expected.put(PaymentMethodType.PAYNOW, "paynow"); + expected.put(PaymentMethodType.PAYPAL, "paypal"); + expected.put(PaymentMethodType.PAYPAY, "paypay"); + expected.put(PaymentMethodType.PLAID, "plaid"); + expected.put(PaymentMethodType.PULSE, "pulse"); + expected.put(PaymentMethodType.QPAY, "qpay"); + expected.put(PaymentMethodType.RABBIT_LINE_PAY, "rabbit_line_pay"); + expected.put(PaymentMethodType.REMEMBER_ME, "remember_me"); + expected.put(PaymentMethodType.SEPA, "sepa"); + expected.put(PaymentMethodType.SEQURA, "sequra"); + expected.put(PaymentMethodType.SHAZAM, "shazam"); + expected.put(PaymentMethodType.SOFORT, "sofort"); + expected.put(PaymentMethodType.STAR, "star"); + expected.put(PaymentMethodType.STCPAY, "stcpay"); + expected.put(PaymentMethodType.STORED_CARD, "stored_card"); + expected.put(PaymentMethodType.SWISH, "swish"); + expected.put(PaymentMethodType.TABBY, "tabby"); + expected.put(PaymentMethodType.TAMARA, "tamara"); + expected.put(PaymentMethodType.TNG, "tng"); + expected.put(PaymentMethodType.TRUEMONEY, "truemoney"); + expected.put(PaymentMethodType.TWINT, "twint"); + expected.put(PaymentMethodType.UPI, "upi"); + expected.put(PaymentMethodType.VIPPS, "vipps"); + expected.put(PaymentMethodType.VISA, "visa"); + expected.put(PaymentMethodType.WALLET, "wallet"); + expected.put(PaymentMethodType.WECHATPAY, "wechatpay"); + + assertEquals(expected.size(), PaymentMethodType.values().length, + "every constant must be listed so the enum cannot grow without this test noticing"); + return expected; + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java index b2c6fb42b..dd3fe6796 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java @@ -7,6 +7,11 @@ import org.junit.jupiter.api.Test; import com.checkout.GsonSerializer; +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.alipaycnsource.AlipayCnSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource.BacsSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource.BankAccountSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.currencyaccountsource.CurrencyAccountSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.klarnasource.KlarnaSource; @@ -150,6 +155,67 @@ void shouldDeserializeSepaSource() { assertEquals("src_sepa_123", sepaSource.getId()); } + // ------------------------------------------------------------------ + // PaymentDeclinedSourceResponse - the ach, alipay_cn, bank_account, sepa + // and bacs branches of the PaymentResponseSource discriminator. The schema + // declares a type and an id, and requires both. + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeAlipayCnSourceWithId() { + AbstractSource source = declinedSourceOfType("alipay_cn", "src_alipay_cn_123"); + + assertInstanceOf(AlipayCnSource.class, source); + assertEquals("src_alipay_cn_123", ((AlipayCnSource) source).getId()); + } + + @Test + void shouldDeserializeAchSourceWithId() { + AbstractSource source = declinedSourceOfType("ach", "src_ach_123"); + + assertInstanceOf(AchSource.class, source); + assertEquals("src_ach_123", ((AchSource) source).getId()); + } + + @Test + void shouldDeserializeBacsSourceWithId() { + AbstractSource source = declinedSourceOfType("bacs", "src_bacs_123"); + + assertInstanceOf(BacsSource.class, source); + assertEquals("src_bacs_123", ((BacsSource) source).getId()); + } + + @Test + void shouldDeserializeBankAccountSourceWithId() { + AbstractSource source = declinedSourceOfType("bank_account", "src_bank_account_123"); + + assertInstanceOf(BankAccountSource.class, source); + assertEquals("src_bank_account_123", ((BankAccountSource) source).getId()); + } + + private AbstractSource declinedSourceOfType(final String type, final String sourceId) { + String json = "{\n" + + " \"id\": \"pay_123\",\n" + + " \"amount\": 1000,\n" + + " \"currency\": \"USD\",\n" + + " \"approved\": false,\n" + + " \"status\": \"Declined\",\n" + + " \"response_code\": \"20005\",\n" + + " \"processed_on\": \"2021-06-08T12:25:01Z\",\n" + + " \"source\": {\n" + + " \"type\": \"" + type + "\",\n" + + " \"id\": \"" + sourceId + "\"\n" + + " }\n" + + "}"; + + RequestAPaymentOrPayoutResponseCreated response = + serializer.fromJson(json, RequestAPaymentOrPayoutResponseCreated.class); + + assertNotNull(response); + assertNotNull(response.getSource()); + return response.getSource(); + } + @Test void shouldDeserializeProcessingSchemeTransactionLinkId() { String json = "{\n" + diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java index 20122bfa9..3ab15a4cb 100644 --- a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -10,6 +10,7 @@ import com.checkout.instruments.create.CreateInstrumentAchResponse; import com.checkout.instruments.get.GetAchInstrumentResponse; import com.checkout.instruments.update.AchInstrumentAccountType; +import com.checkout.instruments.update.UpdateInstrumentAchResponse; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -173,7 +174,7 @@ void shouldRoundTripRetrieveResponse() { } // ------------------------------------------------------------------ - // Store response + // Store and update responses // ------------------------------------------------------------------ @Test @@ -193,6 +194,23 @@ void shouldDeserializeStoreResponse() { assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); } + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentAchResponse response = + serializer.fromJson(json, UpdateInstrumentAchResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + @Test void shouldSerializeAchAccountTypeBothDirections() { assertEquals(2, AchInstrumentAccountType.values().length); diff --git a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java index 7102e0d5c..bf3bdbd7c 100644 --- a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java +++ b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java @@ -7,19 +7,25 @@ import com.checkout.instruments.get.GetAchInstrumentResponse; import com.checkout.instruments.get.GetBacsInstrumentResponse; import com.checkout.instruments.get.GetInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentAchResponse; import com.checkout.instruments.update.UpdateInstrumentBacsResponse; import com.checkout.instruments.update.UpdateInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentSepaResponse; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; /** - * Polymorphic dispatch tests for the Bacs Direct Debit and ACH instrument responses. + * Polymorphic dispatch tests for the Bacs Direct Debit, ACH and SEPA instrument responses. * *

Before these subtypes were registered on the three instrument factories, every assertion here * failed with a JsonParseException, because none of the factories declares a default subtype. ACH * was already registered on the update factory but not on create or get. + * + *

The update assertions also guard the id, which the specification declares on the sepa, ach and + * bacs update responses. It was once declared on the bacs variant only, and was silently dropped on + * the other two. */ class InstrumentResponseDispatchTest { @@ -46,9 +52,36 @@ void shouldDispatchUpdateResponseToBacsSubtype() { final UpdateInstrumentBacsResponse bacs = assertInstanceOf(UpdateInstrumentBacsResponse.class, response); assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", bacs.getFingerprint()); } + @Test + void shouldDispatchUpdateResponseToSepaSubtype() { + final String json = "{\"type\":\"sepa\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentSepaResponse sepa = assertInstanceOf(UpdateInstrumentSepaResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.SEPA, sepa.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", sepa.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", sepa.getFingerprint()); + } + + @Test + void shouldDispatchUpdateResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentAchResponse ach = assertInstanceOf(UpdateInstrumentAchResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", ach.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", ach.getFingerprint()); + } + @Test void shouldDispatchCreateResponseToAchSubtype() { final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," diff --git a/src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java b/src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java similarity index 67% rename from src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java rename to src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java index 019418593..c61f54bc6 100644 --- a/src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java +++ b/src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java @@ -8,7 +8,9 @@ import com.checkout.instruments.create.CreateCustomerInstrumentRequest; import com.checkout.instruments.create.CreateInstrumentSepaRequest; import com.checkout.instruments.create.InstrumentData; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.instruments.update.UpdateInstrumentSepaResponse; +import com.checkout.payments.request.source.apm.MandateType; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -18,10 +20,19 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class CreateInstrumentSepaRequestSerializationTest { +/** + * Schema validation tests for the SEPA variants of the instruments endpoints. + */ +class SepaInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; private final GsonSerializer serializer = new GsonSerializer(); + // ------------------------------------------------------------------ + // StoreSepaInstrumentRequest + // ------------------------------------------------------------------ + @Test void shouldSerializeTypeAsSepa() { final CreateInstrumentSepaRequest request = CreateInstrumentSepaRequest.builder() @@ -49,7 +60,7 @@ void shouldSerializeInstrumentData() { .accoountNumber("DE89370400440532013000") .country(CountryCode.DE) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .firstName("Hans") @@ -63,6 +74,52 @@ void shouldSerializeInstrumentData() { assertTrue(json.contains("\"account_number\":\"DE89370400440532013000\"")); assertTrue(json.contains("\"country\":\"DE\"")); assertTrue(json.contains("\"currency\":\"EUR\"")); + assertTrue(json.contains("\"payment_type\":\"recurring\"")); + } + + /** + * The store request once typed paymentType as the generic payments payment type, whose + * constants serialize capitalized, so a SEPA instrument could not be created at all: the + * specification pins this field to recurring or regular in lowercase. No test asserted the wire + * value, which is why the defect survived. + */ + @Test + void shouldSerializePaymentTypeLowercaseOnTheStoreRequest() { + assertTrue(serializer.toJson(storeRequestWithPaymentType(SepaPaymentType.RECURRING)) + .contains("\"payment_type\":\"recurring\"")); + assertTrue(serializer.toJson(storeRequestWithPaymentType(SepaPaymentType.REGULAR)) + .contains("\"payment_type\":\"regular\"")); + } + + @Test + void shouldSerializeMandateType() { + final CreateInstrumentSepaRequest request = CreateInstrumentSepaRequest.builder() + .instrumentData(InstrumentData.builder() + .type(MandateType.B2B) + .accoountNumber("DE89370400440532013000") + .country(CountryCode.DE) + .currency(Currency.EUR) + .paymentType(SepaPaymentType.REGULAR) + .build()) + .build(); + + final String json = serializer.toJson(request); + final CreateInstrumentSepaRequest result = + serializer.fromJson(json, CreateInstrumentSepaRequest.class); + + assertTrue(json.contains("\"type\":\"B2B\"")); + assertEquals(MandateType.B2B, result.getInstrumentData().getType()); + } + + private CreateInstrumentSepaRequest storeRequestWithPaymentType(final SepaPaymentType paymentType) { + return CreateInstrumentSepaRequest.builder() + .instrumentData(InstrumentData.builder() + .accoountNumber("DE89370400440532013000") + .country(CountryCode.DE) + .currency(Currency.EUR) + .paymentType(paymentType) + .build()) + .build(); } @Test @@ -140,7 +197,7 @@ void shouldRoundTripSerialize() { .accoountNumber("ES9121000418450200051332") .country(CountryCode.ES) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .firstName("Carlos") @@ -191,4 +248,25 @@ void shouldHandleAbsentOptionalCustomer() { assertDoesNotThrow(() -> serializer.toJson(request)); assertNull(request.getCustomer()); } + + // ------------------------------------------------------------------ + // Update response + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"sepa\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentSepaResponse response = + serializer.fromJson(json, UpdateInstrumentSepaResponse.class); + + assertEquals(com.checkout.common.InstrumentType.SEPA, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } } diff --git a/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java index 0dc057fba..1bc684146 100644 --- a/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java +++ b/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java @@ -9,8 +9,8 @@ import com.checkout.instruments.create.CreateInstrumentSepaRequest; import com.checkout.instruments.create.CreateInstrumentSepaResponse; import com.checkout.instruments.create.InstrumentData; +import com.checkout.instruments.update.SepaPaymentType; import com.checkout.payments.AbstractPaymentsTestIT; -import com.checkout.payments.PaymentType; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -25,7 +25,7 @@ void shouldCreateInstrumentSepa() { .accoountNumber("FR7630006000011234567890189") .country(CountryCode.FR) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .type(AccountHolderType.INDIVIDUAL) diff --git a/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java index f7de6548c..20743951e 100644 --- a/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java +++ b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java @@ -6,7 +6,7 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.customer.MerchantAccount; import com.checkout.handlepaymentsandpayouts.setups.entities.order.OrderSubMerchant; import com.checkout.instruments.create.InstrumentData; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; import com.checkout.payments.ProductRequest; import com.checkout.payments.ProductResponse; import com.checkout.payments.request.ItemSubType; @@ -205,7 +205,7 @@ void instrumentData_allProperties_roundTrip() { .accoountNumber("DE89370400440532013000") .country(CountryCode.DE) .currency(Currency.EUR) - .paymentType(PaymentType.REGULAR) + .paymentType(SepaPaymentType.REGULAR) .mandateId("MANDATE-XYZ-999") .dateOfSignature(LocalDate.of(2021, 3, 15)) .build(); @@ -217,7 +217,7 @@ void instrumentData_allProperties_roundTrip() { assertEquals("DE89370400440532013000", deserialized.getAccoountNumber()); assertEquals(CountryCode.DE, deserialized.getCountry()); assertEquals(Currency.EUR, deserialized.getCurrency()); - assertEquals(PaymentType.REGULAR, deserialized.getPaymentType()); + assertEquals(SepaPaymentType.REGULAR, deserialized.getPaymentType()); assertEquals("MANDATE-XYZ-999", deserialized.getMandateId()); assertEquals(LocalDate.of(2021, 3, 15), deserialized.getDateOfSignature()); } From 6d9dbd2435f21b37232028a2aedd06f90dfc458e Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 10:12:13 +0200 Subject: [PATCH 03/18] Java doc + clean up + serialization test --- .../CreateCustomerInstrumentRequest.java | 17 ++ .../create/CreateInstrumentRequest.java | 10 + .../create/CreateInstrumentSepaRequest.java | 3 + .../create/CreateInstrumentSepaResponse.java | 10 + .../get/GetCardInstrumentResponse.java | 9 + .../get/GetSepaInstrumentData.java | 2 + .../get/InstrumentCustomerResponse.java | 11 ++ .../update/AchInstrumentAccountType.java | 7 + .../instruments/update/SepaPaymentType.java | 10 + .../update/UpdateInstrumentAchRequest.java | 48 +++++ .../UpdateInstrumentBankAccountResponse.java | 10 + .../update/UpdateInstrumentCardResponse.java | 10 + .../update/UpdateInstrumentRequest.java | 11 ++ .../update/UpdateInstrumentSepaRequest.java | 11 ++ .../request/source/apm/MandateType.java | 5 + .../checkout/ScratchDiscriminatorProbe.java | 85 --------- ...morphicDiscriminatorSerializationTest.java | 179 ++++++++++++++++++ 17 files changed, 353 insertions(+), 85 deletions(-) delete mode 100644 src/test/java/com/checkout/ScratchDiscriminatorProbe.java create mode 100644 src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java diff --git a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java index 19d2ba6f6..c3a74006b 100644 --- a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java @@ -9,14 +9,31 @@ import lombok.NoArgsConstructor; import lombok.ToString; +/** + * The customer's details. Associates the instrument with an existing or new customer. + * + *

The email, name and phone number are inherited from {@link CustomerRequest}. The email is + * limited to max 255 characters and must be a valid email address, and the name to max 255 + * characters; the name and phone number are only applied when a new customer is created. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @NoArgsConstructor public final class CreateCustomerInstrumentRequest extends CustomerRequest { + /** + * The identifier of an existing customer. + * [Optional] + * ^(cus)_(\w{26})$ + */ private String id; + /** + * If true, this instrument will become the default for the customer. If a new customer is + * created as a result of this request, the instrument will automatically be the default. + * [Optional] + */ @SerializedName("default") private boolean defaultInstrument; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java index 76837d643..80bff3ed1 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java @@ -3,9 +3,19 @@ import com.checkout.common.InstrumentType; import lombok.Data; +/** + * The shared properties of a payment instrument being stored. + * + *

Each concrete variant fixes the type in its constructor, which is what selects the schema the + * API validates the request against. + */ @Data public abstract class CreateInstrumentRequest { + /** + * The type of instrument. + * [Required] + */ protected final InstrumentType type; protected CreateInstrumentRequest(final InstrumentType type) { diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java index b8811d86f..e8c56a321 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Store SEPA mandate details. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java index 88279e9b9..f450ad1fe 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java @@ -11,11 +11,21 @@ import java.time.Instant; +/** + * Store SEPA mandate instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentSepaResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; } diff --git a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java index 0ac264259..e6cd1e929 100644 --- a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java @@ -9,11 +9,20 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Card instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetCardInstrumentResponse extends GetInstrumentResponse { + /** + * The underlying instrument type. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; /** diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java index 62caaef82..ac4e50b4f 100644 --- a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java @@ -74,6 +74,8 @@ public final class GetSepaInstrumentData { * created, it may take up to five seconds for the generated date of signature to be available. * [Required] * Format: yyyy-MM-dd + * min 10 characters + * max 10 characters */ private LocalDate dateOfSignature; diff --git a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java index e752a3d0c..20e729704 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java @@ -6,11 +6,22 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * The customer that a retrieved payment instrument is associated with. + * + *

The id, email and name are inherited from {@link CustomerResponse}. That base class also + * carries a phone number, which the retrieve instrument customer schema does not declare, so it is + * always null here. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class InstrumentCustomerResponse extends CustomerResponse { + /** + * This will be true if this instrument is set as the default for the customer. + * [Optional] + */ @SerializedName("default") private boolean isDefault; diff --git a/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java b/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java index d52c76e9e..9456b487a 100644 --- a/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java +++ b/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java @@ -2,6 +2,13 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of Direct Debit account of an ACH instrument. + * + *

Shared by the store, update and retrieve ACH instrument variants, which all declare the same + * two values. The package is historical: the enum was introduced with the update request and is now + * used by the create and get variants as well. + */ public enum AchInstrumentAccountType { @SerializedName("savings") diff --git a/src/main/java/com/checkout/instruments/update/SepaPaymentType.java b/src/main/java/com/checkout/instruments/update/SepaPaymentType.java index 97b55fd67..8689f561d 100644 --- a/src/main/java/com/checkout/instruments/update/SepaPaymentType.java +++ b/src/main/java/com/checkout/instruments/update/SepaPaymentType.java @@ -2,6 +2,16 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of payment for a SEPA instrument. + * + *

The wire values are lowercase, and the specification allows these two values only. The + * equivalent Bacs Direct Debit field is capitalized, so do not share one type between the two: + * reusing {@link com.checkout.instruments.BacsPaymentType} sends a value the API rejects. Do not + * replace this enum with {@link com.checkout.payments.PaymentType} either, whose constants + * serialize capitalized and which also accepts MOTO, Installment, PayLater and Unscheduled, none of + * which SEPA allows. + */ public enum SepaPaymentType { @SerializedName("recurring") diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java index d9f2148bd..8aa56e6a2 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java @@ -13,14 +13,27 @@ import lombok.Setter; import lombok.ToString; +/** + * Update ACH bank account details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentAchRequest extends UpdateInstrumentRequest { + /** + * The details of the bank account. + * [Optional] + */ private AchInstrumentData instrumentData; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; @Builder @@ -35,20 +48,55 @@ public UpdateInstrumentAchRequest() { super(InstrumentType.ACH); } + /** + * The details of the ACH account being updated. + * + *

Every property is optional on update. The shape is identical to the store and retrieve + * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per + * operation. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public static final class AchInstrumentData { + /** + * The type of Direct Debit account. + * [Optional] + */ private AchInstrumentAccountType accountType; + /** + * The account number of the Direct Debit account. + * [Optional] + * min 4 characters + * max 17 characters + */ private String accountNumber; + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Optional] + * min 8 characters + * max 9 characters + */ private String bankCode; + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ private Currency currency; + /** + * The country of the account. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode country; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java index e85e737ca..3210312ac 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java @@ -5,11 +5,21 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update bank account instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The specification gives + * this variant a type and a fingerprint only, with no id. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentBankAccountResponse extends UpdateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java index 61d77f8e3..4696ec0eb 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java @@ -5,11 +5,21 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update card instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The specification gives + * this variant a type and a fingerprint only, with no id. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentCardResponse extends UpdateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java index 96f6ff3d0..1ad9dc645 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java @@ -3,9 +3,20 @@ import com.checkout.common.InstrumentType; import lombok.Data; +/** + * The shared properties of a payment instrument being updated. + * + *

Each concrete variant fixes the type in its constructor. The specification declares no + * required list on the update variants, but the type is the discriminator that selects which + * variant the API validates against, so it is always sent. + */ @Data public abstract class UpdateInstrumentRequest { + /** + * The type of instrument to be updated. + * [Required] + */ protected final InstrumentType type; protected UpdateInstrumentRequest(final InstrumentType type) { diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java index a3ac4c472..141eea27d 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java @@ -16,6 +16,11 @@ import java.time.LocalDate; +/** + * Update SEPA mandate details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @@ -46,6 +51,12 @@ public UpdateInstrumentSepaRequest() { super(InstrumentType.SEPA); } + /** + * The details of the SEPA mandate being updated. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs + * Direct Debit field is capitalized, so the two must not share a type. + */ @Data @Builder @NoArgsConstructor diff --git a/src/main/java/com/checkout/payments/request/source/apm/MandateType.java b/src/main/java/com/checkout/payments/request/source/apm/MandateType.java index 08e893098..07f4f3048 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/MandateType.java +++ b/src/main/java/com/checkout/payments/request/source/apm/MandateType.java @@ -2,6 +2,11 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of SEPA mandate. + * + *

The wire values are capitalized, and the specification allows these two values only. + */ public enum MandateType { @SerializedName("Core") diff --git a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java deleted file mode 100644 index f20fac506..000000000 --- a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.checkout; - -import com.checkout.issuing.controls.requests.VelocityLimit; -import com.checkout.issuing.controls.requests.VelocityWindow; -import com.checkout.issuing.controls.requests.VelocityWindowType; -import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; -import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -class ScratchDiscriminatorProbe { - - private final GsonSerializer serializer = new GsonSerializer(); - - private void probe(final String label, final Object value) { - try { - System.out.println("PROBE " + label + " => OK: " + serializer.toJson(value)); - } catch (final RuntimeException e) { - System.out.println("PROBE " + label + " => THROWS: " + e.getMessage()); - } - } - - private void probeRoundTrip(final String label, final String json, final Class type) { - try { - final Object parsed = serializer.fromJson(json, type); - probe(label, parsed); - } catch (final RuntimeException e) { - System.out.println("PROBE " + label + " => READ-THROWS: " + e.getMessage()); - } - } - - @Test - void probeAll() { - // 1. REQUEST path - issuing control groups - probe("CreateControlGroupRequest(request!)", CreateControlGroupRequest.builder() - .description("g") - .controls(Collections.singletonList(VelocityControlGroupControl.builder() - .description("c") - .velocityLimit(VelocityLimit.builder() - .amountLimit(1000) - .velocityWindow(VelocityWindow.builder().type(VelocityWindowType.DAILY).build()) - .build()) - .build())) - .build()); - - // 2. customers - List - probeRoundTrip("CustomerResponse.instruments", - "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\",\"fingerprint\":\"f\"}]}", - com.checkout.customers.CustomerResponse.class); - - // 3. workflows - List + List - probeRoundTrip("GetWorkflowResponse.actions+conditions", - "{\"id\":\"wf_1\",\"name\":\"n\"," - + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," - + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://x.test\"}]}", - com.checkout.workflows.GetWorkflowResponse.class); - - // 4. accounts payout schedule - ScheduleResponse recurrence - probeRoundTrip("CurrencySchedule.recurrence", - "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"daily\"}}", - com.checkout.accounts.payout.schedule.response.CurrencySchedule.class); - - // 5. issuing cardholder cards - List - probeRoundTrip("CardholderCardsResponse.cards", - "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", - com.checkout.issuing.cardholders.CardholderCardsResponse.class); - - // 6. issuing card controls query - List - probeRoundTrip("CardControlsQueryResponse.controls", - "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", - com.checkout.issuing.controls.responses.query.CardControlsQueryResponse.class); - - // 7. issuing control group response - List - probeRoundTrip("ControlGroupResponse.controls", - "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\",\"description\":\"d\"}]}", - com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse.class); - - // 8. the payments source hierarchy touched by this branch - probeRoundTrip("RequestAPaymentOrPayoutResponseCreated.source", - "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", - com.checkout.handlepaymentsandpayouts.payments.postpayments.responses - .requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated.class); - } -} diff --git a/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java b/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java new file mode 100644 index 000000000..8309fedad --- /dev/null +++ b/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java @@ -0,0 +1,179 @@ +package com.checkout.schema; + +import com.checkout.GsonSerializer; +import com.checkout.accounts.payout.schedule.response.CurrencySchedule; +import com.checkout.customers.CustomerResponse; +import com.checkout.handlepaymentsandpayouts.payments.postpayments.responses.requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated; +import com.checkout.issuing.cardholders.CardholderCardsResponse; +import com.checkout.issuing.controls.requests.VelocityLimit; +import com.checkout.issuing.controls.requests.VelocityWindow; +import com.checkout.issuing.controls.requests.VelocityWindowType; +import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; +import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; +import com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse; +import com.checkout.issuing.controls.responses.query.CardControlsQueryResponse; +import com.checkout.workflows.GetWorkflowResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression tests for polymorphic types nested inside a field or collection whose declared type is + * the abstract base. + * + *

Every polymorphic base in this SDK declares its discriminator as a real field, so that callers + * get a typed getter. The factories for these hierarchies are registered without + * {@code maintainType}, which means the factory owns the discriminator on the wire: it strips the + * field on read and re-injects it from the registered label on write. Upstream Gson refuses to + * serialize a subtype that already declares that field, so before the local modification in + * {@link com.google.gson.typeadapters.RuntimeTypeAdapterFactory} every case below failed with + * + *

   {@code
+ *   JsonParseException: cannot serialize  because it already defines a field named type
+ * }
+ * + *

Deserialization was never affected, which is why the SDK's response handling hid the defect. + * The write path is reached only when the declared type is the base, so serializing a subtype + * directly always worked and the failure surfaced only through the containers exercised here. + * {@link CreateControlGroupRequest} is the case that mattered most: it is an outbound request body, + * so the exception was thrown before the HTTP call and made the endpoint unusable. + * + *

Each test asserts the discriminator survives exactly once, with the value of the registered + * label, and that no other property is lost in the round trip. + */ +class PolymorphicDiscriminatorSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // Outbound request bodies + // ------------------------------------------------------------------ + + @Test + void shouldSerializeControlGroupRequestWithPolymorphicControls() { + final CreateControlGroupRequest request = CreateControlGroupRequest.builder() + .description("Velocity control group") + .controls(Collections.singletonList(VelocityControlGroupControl.builder() + .description("Daily spend cap") + .velocityLimit(VelocityLimit.builder() + .amountLimit(1000) + .velocityWindow(VelocityWindow.builder() + .type(VelocityWindowType.DAILY) + .build()) + .build()) + .build())) + .build(); + + final String json = serializer.toJson(request); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"description\":\"Velocity control group\"")); + assertTrue(json.contains("\"description\":\"Daily spend cap\"")); + assertTrue(json.contains("\"amount_limit\":1000")); + assertTrue(json.contains("\"type\":\"daily\"")); + } + + // ------------------------------------------------------------------ + // Response bodies, round-tripped + // ------------------------------------------------------------------ + + @Test + void shouldRoundTripPaymentResponseWithPolymorphicSource() { + final String json = roundTrip( + "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", + RequestAPaymentOrPayoutResponseCreated.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"id\":\"src_ach_1\"")); + } + + @Test + void shouldRoundTripCustomerResponseWithPolymorphicInstruments() { + final String json = roundTrip( + "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}]}", + CustomerResponse.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"card\"")); + assertTrue(json.contains("\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"")); + } + + @Test + void shouldRoundTripWorkflowResponseWithPolymorphicActionsAndConditions() { + final String json = roundTrip( + "{\"id\":\"wf_1\",\"name\":\"n\"," + + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," + + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://example.test\"}]}", + GetWorkflowResponse.class); + + assertEquals(2, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"event\"")); + assertTrue(json.contains("\"type\":\"webhook\"")); + assertTrue(json.contains("\"url\":\"https://example.test\"")); + } + + @Test + void shouldRoundTripCurrencyScheduleWithPolymorphicRecurrence() { + final String json = roundTrip( + "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"Daily\"}}", + CurrencySchedule.class); + + assertEquals(1, occurrences(json, "\"frequency\"")); + assertTrue(json.contains("\"frequency\":\"Daily\"")); + assertTrue(json.contains("\"threshold\":100")); + } + + @Test + void shouldRoundTripCardholderCardsWithPolymorphicCardDetails() { + final String json = roundTrip( + "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", + CardholderCardsResponse.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"virtual\"")); + assertTrue(json.contains("\"id\":\"crd_1\"")); + } + + @Test + void shouldRoundTripCardControlsQueryWithPolymorphicControls() { + final String json = roundTrip( + "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", + CardControlsQueryResponse.class); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"id\":\"ctr_1\"")); + } + + @Test + void shouldRoundTripControlGroupResponseWithPolymorphicControls() { + final String json = roundTrip( + "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\"," + + "\"description\":\"Daily spend cap\"}]}", + ControlGroupResponse.class); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"description\":\"Daily spend cap\"")); + } + + private String roundTrip(final String json, final Class type) { + return serializer.toJson(serializer.fromJson(json, type)); + } + + private int occurrences(final String json, final String needle) { + int count = 0; + int from = json.indexOf(needle); + while (from != -1) { + count++; + from = json.indexOf(needle, from + needle.length()); + } + return count; + } +} From 44f07e869d5e4af13857cfb30e88d11fc3965a65 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 11:00:13 +0200 Subject: [PATCH 04/18] Java doc + some fixes --- .../java/com/checkout/GsonSerializer.java | 21 +-- .../instruments/InstrumentsClient.java | 82 +++++++++++ .../instruments/InstrumentsClientImpl.java | 3 + .../CreateInstrumentBankAccountRequest.java | 58 ++++++++ .../CreateInstrumentBankAccountResponse.java | 31 ++++ .../create/CreateInstrumentTokenRequest.java | 16 +++ .../create/CreateInstrumentTokenResponse.java | 70 +++++++++ .../instruments/get/BankAccountField.java | 51 +++++++ .../get/BankAccountFieldAllowedOption.java | 11 ++ .../get/BankAccountFieldDependency.java | 11 ++ .../get/BankAccountFieldQuery.java | 13 ++ .../get/BankAccountFieldResponse.java | 7 + .../instruments/get/BankAccountSection.java | 11 ++ .../get/GetBankAccountInstrumentResponse.java | 52 +++++++ .../get/InstrumentNetworkToken.java | 3 + .../instruments/get/NetworkTokenState.java | 3 + .../instruments/get/PaymentNetwork.java | 3 + .../UpdateInstrumentBankAccountRequest.java | 62 +++++++- .../update/UpdateInstrumentCardRequest.java | 30 ++++ .../update/UpdateInstrumentTokenRequest.java | 11 ++ .../request/source/apm/RequestAchSource.java | 3 + .../source/apm/RequestAfterPaySource.java | 6 + .../source/apm/RequestAlipayCnSource.java | 3 + .../source/apm/RequestAlipayHkSource.java | 3 + .../source/apm/RequestAlipayPlusSource.java | 3 + .../request/source/apm/RequestAlmaSource.java | 3 + .../source/apm/RequestBancontactSource.java | 3 + .../source/apm/RequestBenefitSource.java | 6 + .../source/apm/RequestBizumSource.java | 3 + .../source/apm/RequestCvConnectSource.java | 6 + .../request/source/apm/RequestDanaSource.java | 3 + .../request/source/apm/RequestEpsSource.java | 3 + .../source/apm/RequestFawrySource.java | 3 + .../source/apm/RequestGcashSource.java | 3 + .../source/apm/RequestGiropaySource.java | 6 + .../source/apm/RequestIdealSource.java | 3 + .../source/apm/RequestIllicadoSource.java | 6 + .../source/apm/RequestKakaopaySource.java | 3 + .../source/apm/RequestKlarnaSource.java | 6 + .../request/source/apm/RequestKnetSource.java | 3 + .../source/apm/RequestMbwaySource.java | 3 + .../source/apm/RequestMobilePaySource.java | 3 + .../source/apm/RequestMultiBancoSource.java | 3 + .../source/apm/RequestOctopusSource.java | 5 + .../request/source/apm/RequestP24Source.java | 3 + .../source/apm/RequestPayPalSource.java | 6 + .../source/apm/RequestPlaidSource.java | 3 + .../source/apm/RequestPostFinanceSource.java | 6 + .../request/source/apm/RequestQPaySource.java | 3 + .../request/source/apm/RequestSepaSource.java | 7 + .../source/apm/RequestSequraSource.java | 3 + .../source/apm/RequestStcPaySource.java | 3 + .../source/apm/RequestSwishSource.java | 3 + .../source/apm/RequestTamaraSource.java | 3 + .../request/source/apm/RequestTngSource.java | 3 + .../source/apm/RequestTruemoneySource.java | 3 + .../source/apm/RequestTwintSource.java | 3 + .../source/apm/RequestVippsSource.java | 3 + .../source/apm/RequestWeChatPaySource.java | 3 + .../source/AbstractResponseSource.java | 12 ++ .../AlternativePaymentSourceResponse.java | 9 ++ .../response/source/CardResponseSource.java | 135 +++++++++++++++++- .../source/CurrencyAccountResponseSource.java | 15 ++ .../response/source/PayPalResponseSource.java | 8 ++ .../response/source/ResponseSource.java | 11 ++ .../RuntimeTypeAdapterFactory.java | 7 +- .../InstrumentRequestTypeTest.java | 62 ++++++++ 67 files changed, 933 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 5e09d4b75..42ee1603c 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -112,7 +112,8 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory( RuntimeTypeAdapterFactory.of( com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource.class, - CheckoutUtils.TYPE + CheckoutUtils.TYPE, + true ) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CARD)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ACH)) @@ -187,47 +188,47 @@ public final class GsonSerializer implements Serializer { .registerSubtype(com.checkout.payments.sender.PaymentIndividualSender.class, identifier(SenderType.INDIVIDUAL)) .registerSubtype(com.checkout.payments.sender.PaymentInstrumentSender.class, identifier(SenderType.INSTRUMENT))) // Instruments CS2 - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentTokenResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBacsResponse.class, identifier(InstrumentType.BACS)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.get.GetBankAccountInstrumentResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.get.GetCardInstrumentResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.get.GetBacsInstrumentResponse.class, identifier(InstrumentType.BACS)) .registerSubtype(com.checkout.instruments.get.GetAchInstrumentResponse.class, identifier(InstrumentType.ACH))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentCardResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBacsResponse.class, identifier(InstrumentType.BACS))) // Workflows CS2 - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.workflows.actions.response.WebhookWorkflowActionResponse.class, identifier(WorkflowActionType.WEBHOOK))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.conditions.response.WorkflowConditionResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.conditions.response.WorkflowConditionResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.workflows.conditions.response.EventWorkflowConditionResponse.class, identifier(WorkflowConditionType.EVENT)) .registerSubtype(com.checkout.workflows.conditions.response.EntityWorkflowConditionResponse.class, identifier(WorkflowConditionType.ENTITY)) .registerSubtype(com.checkout.workflows.conditions.response.ProcessingChannelWorkflowConditionResponse.class, identifier(WorkflowConditionType.PROCESSING_CHANNEL))) // Accounts CS2 - PayoutSchedules .registerTypeAdapter(GetScheduleResponse.class, getScheduleResponseDeserializer()) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.accounts.payout.schedule.response.ScheduleResponse.class, CheckoutUtils.FREQUENCY) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.accounts.payout.schedule.response.ScheduleResponse.class, CheckoutUtils.FREQUENCY, true) .registerSubtype(com.checkout.accounts.payout.schedule.response.ScheduleFrequencyDailyResponse.class, CheckoutUtils.DAILY) .registerSubtype(com.checkout.accounts.payout.schedule.response.ScheduleFrequencyWeeklyResponse.class, CheckoutUtils.WEEKLY) .registerSubtype(ScheduleFrequencyMonthlyResponse.class, CheckoutUtils.MONTHLY)) // Issuing CS2 - CardDetailsResponse - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardDetailsResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardDetailsResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(PhysicalCardDetailsResponse.class, identifier(CardType.PHYSICAL)) .registerSubtype(VirtualCardDetailsResponse.class, identifier(CardType.VIRTUAL))) // Issuing CS2 - CardControlsResponse - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardControlResponse.class, CheckoutUtils.CONTROL_TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardControlResponse.class, CheckoutUtils.CONTROL_TYPE, true) .registerSubtype(VelocityCardControlResponse.class, identifier(ControlType.VELOCITY_LIMIT)) .registerSubtype(MccCardControlResponse.class, identifier(ControlType.MCC_LIMIT))) // Issuing CS2 - ControlGroupControl - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.issuing.controls.requests.controlgroup.ControlGroupControl.class, CheckoutUtils.CONTROL_TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.issuing.controls.requests.controlgroup.ControlGroupControl.class, CheckoutUtils.CONTROL_TYPE, true) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl.class, identifier(ControlType.VELOCITY_LIMIT)) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.MccControlGroupControl.class, identifier(ControlType.MCC_LIMIT)) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.MidControlGroupControl.class, identifier(ControlType.MID_LIMIT))) diff --git a/src/main/java/com/checkout/instruments/InstrumentsClient.java b/src/main/java/com/checkout/instruments/InstrumentsClient.java index 7d89fdd7e..338841152 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClient.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClient.java @@ -13,14 +13,51 @@ import java.util.concurrent.CompletableFuture; +/** + * The payment instruments client. + * + *

The create, update and retrieve operations are polymorphic: the concrete request type selects + * the instrument variant, and the response deserializes to the matching concrete type. Cast or + * parameterize to the variant you sent. + */ public interface InstrumentsClient { + /** + * Stores a payment instrument. + * + * @param createInstrumentRequest the instrument details, as one of the concrete variants of + * {@link CreateInstrumentRequest}. + * @param the concrete response variant matching the request type. + * @return a {@link CompletableFuture} that resolves to the stored instrument. + */ CompletableFuture create(CreateInstrumentRequest createInstrumentRequest); + /** + * Retrieves a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @return a {@link CompletableFuture} that resolves to the instrument, as one of the concrete + * variants of {@link GetInstrumentResponse}. + */ CompletableFuture get(String instrumentId); + /** + * Updates a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @param updateInstrumentRequest the properties to update, as one of the concrete variants of + * {@link UpdateInstrumentRequest}. + * @param the concrete response variant matching the request type. + * @return a {@link CompletableFuture} that resolves to the updated instrument. + */ CompletableFuture update(String instrumentId, UpdateInstrumentRequest updateInstrumentRequest); + /** + * Deletes a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @return a {@link CompletableFuture} that resolves to an empty response on success. + */ CompletableFuture delete(String instrumentId); /** @@ -32,15 +69,51 @@ public interface InstrumentsClient { */ CompletableFuture revoke(String instrumentId); + /** + * Retrieves the bank account field formatting requirements for a country and currency. + * + * @param country the two-letter ISO country code. + * @param currency the three-letter ISO currency code. + * @param query the optional filters on account holder type and payment network. + * @return a {@link CompletableFuture} that resolves to the required field sections. + */ CompletableFuture getBankAccountFieldFormatting(CountryCode country, Currency currency, BankAccountFieldQuery query); // Synchronous methods + + /** + * Synchronous variant of {@link #create(CreateInstrumentRequest)}. + * + * @param createInstrumentRequest the instrument details. + * @param the concrete response variant matching the request type. + * @return the stored instrument. + */ T createSync(CreateInstrumentRequest createInstrumentRequest); + /** + * Synchronous variant of {@link #get(String)}. + * + * @param instrumentId the payment instrument ID. + * @return the instrument. + */ GetInstrumentResponse getSync(String instrumentId); + /** + * Synchronous variant of {@link #update(String, UpdateInstrumentRequest)}. + * + * @param instrumentId the payment instrument ID. + * @param updateInstrumentRequest the properties to update. + * @param the concrete response variant matching the request type. + * @return the updated instrument. + */ T updateSync(String instrumentId, UpdateInstrumentRequest updateInstrumentRequest); + /** + * Synchronous variant of {@link #delete(String)}. + * + * @param instrumentId the payment instrument ID. + * @return an empty response on success. + */ EmptyResponse deleteSync(String instrumentId); /** @@ -51,5 +124,14 @@ public interface InstrumentsClient { */ EmptyResponse revokeSync(String instrumentId); + /** + * Synchronous variant of + * {@link #getBankAccountFieldFormatting(CountryCode, Currency, BankAccountFieldQuery)}. + * + * @param country the two-letter ISO country code. + * @param currency the three-letter ISO currency code. + * @param query the optional filters on account holder type and payment network. + * @return the required field sections. + */ BankAccountFieldResponse getBankAccountFieldFormattingSync(CountryCode country, Currency currency, BankAccountFieldQuery query); } diff --git a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java index 54934fb31..48684137d 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java @@ -20,6 +20,9 @@ import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +/** + * The default {@link InstrumentsClient} implementation. + */ public class InstrumentsClientImpl extends AbstractClient implements InstrumentsClient { private static final String INSTRUMENTS_PATH = "instruments"; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java index d038b3fbc..23c34edbe 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java @@ -12,36 +12,94 @@ import lombok.Setter; import lombok.ToString; +/** + * Store bank account details. + * + *

The bank_account instrument type only supports payouts. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentBankAccountRequest extends CreateInstrumentRequest { + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Required] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Required] + */ private CountryCode country; + /** + * The ID of the primary processing channel this instrument is intended to be used for. + * [Optional] + */ private String processingChannelId; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ private CreateCustomerInstrumentRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java index b6e6bd706..ce59414b2 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java @@ -6,21 +6,52 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Store bank account instrument response. + * + *

The id, the fingerprint and the customer are inherited from + * {@link CreateInstrumentResponse}. This is one of only two store responses the specification + * declares a customer on, the other being {@link CreateInstrumentTokenResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentBankAccountResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java index bc3b8b628..7372905e5 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java @@ -8,16 +8,32 @@ import lombok.Setter; import lombok.ToString; +/** + * Store token details. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentTokenRequest extends CreateInstrumentRequest { + /** + * The Checkout.com token. + * [Required] + * ^(tok)_(\w{26})$|^(card_tok)_(\w{12})$ + */ private String token; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ private CreateCustomerInstrumentRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java index 90127b0ce..0318860c0 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java @@ -8,35 +8,105 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Card instrument response. + * + *

The id, the fingerprint and the customer are inherited from + * {@link CreateInstrumentResponse}. The instrument type is card rather than token: the + * specification maps the token store request onto a card store response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentTokenResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; + /** + * The expiry month. + * [Required] + * max 2 characters + * min 1 + */ private Integer expiryMonth; + /** + * The expiry year. + * [Required] + * min 4 characters + * max 4 characters + */ private Integer expiryYear; + /** + * The card scheme. + * [Optional] + */ private String scheme; + /** + * The local co-branded card scheme. + * [Optional] + * Enum: "cartes_bancaires" + */ private String schemeLocal; + /** + * The last four digits of the card number. + * [Required] + * min 4 characters + * max 4 characters + */ private String last4; + /** + * The card issuer's bank identification number (BIN). + * [Required] + */ private String bin; + /** + * The card type. + * [Optional] + * Enum: "CREDIT" "DEBIT" "PREPAID" "CHARGE" + */ private CardType cardType; + /** + * The card category. + * [Optional] + * Enum: "CONSUMER" "COMMERCIAL" + */ private CardCategory cardCategory; + /** + * The name of the card issuer. + * [Optional] + */ private String issuer; + /** + * The card issuer's country, as a two-letter ISO code. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode issuerCountry; + /** + * The issuer or card scheme product identifier. + * [Optional] + */ private String productId; + /** + * The issuer or card scheme product type. + * [Optional] + */ private String productType; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountField.java b/src/main/java/com/checkout/instruments/get/BankAccountField.java index beb0a4b73..62b42919b 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountField.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountField.java @@ -5,30 +5,81 @@ import java.util.List; +/** + * A bank account field to collect. + */ @Data public final class BankAccountField { + /** + * The field identifier. + * [Required] + */ private String id; + /** + * The section to display the field in. + * [Optional] + */ private String section; + /** + * The field's display name. + * [Required] + */ private String display; + /** + * The help text that explains the purpose of the field. + * [Optional] + */ private String helpText; + /** + * The type of field. + * [Required] + */ private String type; + /** + * Whether the field is required. + * [Required] + */ private boolean required; + /** + * A regular expression that can be used to validate the input of the field. + * [Optional] + */ private String validationRegex; + /** + * The minimum length of the field. + * [Optional] + */ private int minLength; + /** + * The maximum length of the field. + * [Optional] + * + *

The Java field name is lowercase where the neighbouring minLength is camel case. The wire + * name is correct because of the explicit annotation, so this is a source-only defect; + * renaming the field is breaking and is deferred to the next major version. + */ @SerializedName("max_length") private int maxlength; + /** + * The allowed options for the field. + * [Optional] + */ private List allowedOptions; + /** + * The field's dependencies. + * [Optional] + */ private List dependencies; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java index 49ede5ac6..52283fa6f 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java @@ -2,11 +2,22 @@ import lombok.Data; +/** + * An allowed option for a bank account field. + */ @Data public final class BankAccountFieldAllowedOption { + /** + * The option identifier. + * [Optional] + */ private String id; + /** + * The option display value. + * [Optional] + */ private String display; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java index 3d3daa532..b3a6ced1c 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java @@ -2,11 +2,22 @@ import lombok.Data; +/** + * A dependency that controls whether a bank account field is displayed. + */ @Data public final class BankAccountFieldDependency { + /** + * The field identifier. + * [Optional] + */ private String fieldId; + /** + * The value of the dependent field that must match in order for this field to be displayed. + * [Optional] + */ private String value; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java index fbcb79ad7..73414039d 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java @@ -5,13 +5,26 @@ import lombok.Builder; import lombok.Data; +/** + * The optional query parameters for the bank account field formatting request. + */ @Data @Builder public final class BankAccountFieldQuery { + /** + * The type of account holder that will be used to filter the fields returned. + * [Optional] + * Enum: "individual" "corporate" "government" + */ @SerializedName("account-holder-type") private AccountHolderType accountHolderType; + /** + * The banking network that will be used to filter the fields returned. + * [Optional] + * Enum: "local" "sepa" "fps" "ach" "fedwire" "swift" + */ @SerializedName("payment-network") private PaymentNetwork paymentNetwork; diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java index fda7168c7..1bd5505ec 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java @@ -6,10 +6,17 @@ import java.util.List; +/** + * The bank account field formatting requirements for a country and currency. + */ @Data @EqualsAndHashCode(callSuper = true) public final class BankAccountFieldResponse extends HttpMetadata { + /** + * The sections of fields to collect. + * [Optional] + */ private List sections; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountSection.java b/src/main/java/com/checkout/instruments/get/BankAccountSection.java index 570020880..21d28c4bc 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountSection.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountSection.java @@ -4,11 +4,22 @@ import java.util.List; +/** + * A section of bank account fields to collect. + */ @Data public final class BankAccountSection { + /** + * The name of the section. + * [Required] + */ private String name; + /** + * The fields to collect in this section. + * [Optional] + */ private List fields; } diff --git a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java index a91428ed5..9d38893a5 100644 --- a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java @@ -10,31 +10,83 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Bank account details. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. This is the only + * retrieve variant whose account holder is the shared {@link AccountHolder} type; the card, SEPA, + * ACH and Bacs Direct Debit variants each declare their own shape. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetBankAccountInstrumentResponse extends GetInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Required] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Required] + */ private CountryCode country; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; /** diff --git a/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java b/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java index 740fccc54..471c9e6c3 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java @@ -2,6 +2,9 @@ import lombok.Data; +/** + * The network token provisioned for a stored card instrument. + */ @Data public final class InstrumentNetworkToken { diff --git a/src/main/java/com/checkout/instruments/get/NetworkTokenState.java b/src/main/java/com/checkout/instruments/get/NetworkTokenState.java index 5871e9bd8..168a47c55 100644 --- a/src/main/java/com/checkout/instruments/get/NetworkTokenState.java +++ b/src/main/java/com/checkout/instruments/get/NetworkTokenState.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The state of a network token. + */ public enum NetworkTokenState { @SerializedName("active") diff --git a/src/main/java/com/checkout/instruments/get/PaymentNetwork.java b/src/main/java/com/checkout/instruments/get/PaymentNetwork.java index 39707e17e..5070f04d8 100644 --- a/src/main/java/com/checkout/instruments/get/PaymentNetwork.java +++ b/src/main/java/com/checkout/instruments/get/PaymentNetwork.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The banking network used to filter the bank account fields returned. + */ public enum PaymentNetwork { @SerializedName("local") diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java index d72a6ff50..f026d6ff5 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java @@ -13,36 +13,94 @@ import lombok.Setter; import lombok.ToString; +/** + * Update bank account details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentBankAccountRequest extends UpdateInstrumentRequest { + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Optional] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Optional] + */ private CountryCode country; + /** + * The ID of the primary processing channel this instrument is intended to be used for. + * [Optional] + */ private String processingChannelId; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * The customer's details. + * [Optional] + */ private UpdateCustomerRequest customer; @Builder @@ -59,7 +117,7 @@ private UpdateInstrumentBankAccountRequest(final AccountType accountType, final AccountHolder accountHolder, final BankDetails bank, final UpdateCustomerRequest customer) { - super(InstrumentType.TOKEN); + super(InstrumentType.BANK_ACCOUNT); this.accountType = accountType; this.accountNumber = accountNumber; this.bankCode = bankCode; @@ -76,7 +134,7 @@ private UpdateInstrumentBankAccountRequest(final AccountType accountType, } public UpdateInstrumentBankAccountRequest() { - super(InstrumentType.TOKEN); + super(InstrumentType.BANK_ACCOUNT); } } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java index c23723cc7..ed61b3e47 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java @@ -9,20 +9,50 @@ import lombok.Setter; import lombok.ToString; +/** + * Update card details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentCardRequest extends UpdateInstrumentRequest { + /** + * The expiry month of the card. + * [Optional] + * min 1 characters + * max 2 characters + * min 1 + */ private Integer expiryMonth; + /** + * The expiry year of the card. + * [Optional] + * min 4 characters + * max 4 characters + */ private Integer expiryYear; + /** + * Name of the cardholder. + * [Optional] + */ private String name; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * The customer's details. + * [Optional] + */ private UpdateCustomerRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java index 9d69af90f..6fed07a0d 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java @@ -7,12 +7,23 @@ import lombok.Setter; import lombok.ToString; +/** + * Update an instrument from a Checkout.com token. + * + *

The current specification's update discriminator declares card, bank_account, sepa, ach and + * bacs only, so there is no token schema to align this request against. It is retained for + * backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentTokenRequest extends UpdateInstrumentRequest { + /** + * The Checkout.com token. + * [Optional] + */ private String token; @Builder diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java index 10a68a156..e053baecc 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java @@ -11,6 +11,9 @@ import lombok.Setter; import lombok.ToString; +/** + * An ach payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java index 9edea45c4..217a9d87d 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * Afterpay source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java index ff8ce7380..a85d24de7 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay CN source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestAlipayCnSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java index 342f6d6e7..ebc895a54 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay HK source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestAlipayHkSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java index aae882d06..7943334c9 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay Plus source. + */ @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public final class RequestAlipayPlusSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java index 06b6b728d..51d7b8893 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Alma payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java index 4e09d30c2..885d7f016 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Bancontact source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java index aebf95644..6c7edf1b5 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java @@ -5,6 +5,12 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Benefit source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestBenefitSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java index 180f0052e..935461066 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Bizum source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java index c2481d806..59990e724 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * CVConnect source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java index 9be03296f..4b09c5aa0 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Dana source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestDanaSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java index 69fb81c6c..250604479 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * EPS source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java index 5a9e44c10..00f3e3a82 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java @@ -15,6 +15,9 @@ import java.time.Instant; import java.util.List; +/** + * Fawry source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java index 15ec2bc47..d659044f4 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * GCash source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestGcashSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java index aabaeaec8..253924219 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java @@ -14,6 +14,12 @@ import java.util.List; +/** + * giropay source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java index 6427c50b7..606dcc8b8 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * iDEAL source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java index c41cbde15..094b999fd 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java @@ -5,6 +5,12 @@ import com.checkout.payments.request.source.AbstractRequestSource; import lombok.*; +/** + * Illicado source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java index b59bf7f6f..e4842ac95 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Kakaopay source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestKakaopaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java index 2ffd7f97c..c47c2d0a1 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * Klarna source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java index b0e068137..40f097cfa 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java @@ -10,6 +10,9 @@ import lombok.Setter; import lombok.ToString; +/** + * KNet source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java index 0adc4d229..9e003f842 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * MBWay payment request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestMbwaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java index 6f016a9d6..135d01c50 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * MobilePay request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestMobilePaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java index 498ac7ab9..898da4d7a 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Multibanco source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java index 654423dc8..7120acbfc 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java @@ -5,6 +5,11 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Octopus Pay source. + * + *

The specification calls this schema PaymentRequestOctopusPaySource. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestOctopusSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java b/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java index 1feadf0b8..6e4826dfe 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * P24 source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java index ceec52c76..3c2daec7f 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * PayPal source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java index 4c03c49c4..3232180c0 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A Plaid source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java index eb4be737b..168f9d3c9 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * PostFinance source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java index 78ecf1d0a..9989b5a71 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * QPay source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java index ec86cf4fb..b933856ec 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java @@ -11,6 +11,13 @@ import lombok.Setter; import lombok.ToString; +/** + * SEPA Direct Debit source. + * + *

This is the current-platform source, whose type on the wire is "sepa". The previous platform + * references a stored mandate through the generic "id" source instead; use + * {@link com.checkout.payments.previous.request.source.apm.RequestSepaSource} for that. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java index 295d0e275..372b7afb7 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A seQura payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java index 51b972b75..6a28b6df5 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * STC Pay request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestStcPaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java index 46813b623..791042133 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java @@ -10,6 +10,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Swish source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java index a73029ab9..5ada85f7e 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A Tamara payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java index 70f939016..bdac6efcf 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * TNG source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTngSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java index 976ccb353..9df913186 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * TrueMoney source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTruemoneySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java index e2ad48bff..9c48d5aad 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Twint request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTwintSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java index 2456a6aae..51fcfba97 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Vipps request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestVippsSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java index 245496be0..3e7c66c25 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * WeChat Pay source. + */ @Getter @Setter @ToString(callSuper = true) diff --git a/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java b/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java index 4778a555f..44139e36f 100644 --- a/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java @@ -3,11 +3,23 @@ import com.checkout.common.PaymentSourceType; import lombok.Data; +/** + * The properties every typed payment response source shares. + */ @Data public abstract class AbstractResponseSource { + /** + * The payment source type. + * [Required] + */ public PaymentSourceType type; + /** + * The payment source identifier that can be used for subsequent payments. For new sources, + * this is only returned if the payment was approved. + * [Optional] + */ public String id; } diff --git a/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java b/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java index d896e650a..855c1c2eb 100644 --- a/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java +++ b/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java @@ -9,11 +9,20 @@ import java.util.HashMap; +/** + * The fallback payment response source, used for any source type the SDK does not model as a + * dedicated class. The raw JSON is exposed as map entries. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class AlternativePaymentSourceResponse extends HashMap implements ResponseSource { + /** + * Returns the payment source type resolved from the raw type entry. + * + * @return the payment source type, or null if the type entry is absent or unrecognised. + */ @Override public PaymentSourceType getType() { return EnumUtils.getEnumIgnoreCase(PaymentSourceType.class, (String) get(CheckoutUtils.TYPE)); diff --git a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java index 2267f12a2..a6b765ed3 100644 --- a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java @@ -1,6 +1,5 @@ package com.checkout.payments.response.source; -import com.checkout.common.AccountHolder; import com.checkout.common.AccountHolderResponse; import com.checkout.common.Address; import com.checkout.common.CardCategory; @@ -15,66 +14,188 @@ import java.util.List; +/** + * A card payment source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CardResponseSource extends AbstractResponseSource implements ResponseSource { + /** + * The payment source owner's billing address. + * [Optional] + */ private Address billingAddress; + /** + * The payment source owner's phone number. + * [Optional] + */ private Phone phone; - // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization - // issues if it is set to Instant + /** + * The expiry month. + * [Required] + * min 1 characters + * max 2 characters + * min 1 + * + *

The specification types this as an integer, but the API masks the value with asterisks on + * some responses, so it is exposed as a string to keep deserialization working. + */ private String expiryMonth; - // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization - // issues if it is set to Instant + /** + * The expiry year. + * [Required] + * min 4 characters + * max 4 characters + * + *

The specification types this as an integer, but the API masks the value with asterisks on + * some responses, so it is exposed as a string to keep deserialization working. + */ private String expiryYear; + /** + * The cardholder's name. + * [Optional] + */ private String name; + /** + * The card scheme. + * [Optional] + */ private String scheme; /** - * @deprecated This property will be removed in the future, and should be used - * {@link CardResponseSource#localSchemes} instead + * The local co-branded card scheme. + * [Optional] + * + * @deprecated replaced by {@link CardResponseSource#localSchemes}. This property will be + * removed in a future version. */ @Deprecated private String schemeLocal; + /** + * The local co-branded card schemes. + * [Optional] + */ private List localSchemes; + /** + * The last four digits of the card number. + * [Required] + */ private String last4; + /** + * Uniquely identifies this particular card number. You can use this to compare cards across + * customers. + * [Required] + */ private String fingerprint; + /** + * The card issuer's Bank Identification Number (BIN). + * [Required] + * max 8 characters + */ private String bin; + /** + * The card type. + * [Optional] + * Enum: "CREDIT" "DEBIT" "PREPAID" "CHARGE" "DEFERRED DEBIT" + */ private CardType cardType; + /** + * The card category. + * [Optional] + * Enum: "CONSUMER" "COMMERCIAL" + */ private CardCategory cardCategory; + /** + * The card wallet type. + * [Optional] + * Enum: "applepay" "googlepay" + */ private CardWalletType cardWalletType; + /** + * The name of the card issuer. + * [Optional] + */ private String issuer; + /** + * The card issuer's country, as a two-letter ISO code. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode issuerCountry; + /** + * The issuer or card scheme product identifier. + * [Optional] + */ private String productId; + /** + * The issuer or card scheme product type. + * [Optional] + */ private String productType; + /** + * The Address Verification System check result. + * [Optional] + */ private String avsCheck; + /** + * The card verification value (CVV) check result. + * [Optional] + */ private String cvvCheck; + /** + * A unique reference to the underlying card for network tokens, such as Apple Pay or Google + * Pay. + * [Optional] + */ private String paymentAccountReference; + /** + * The JWE encrypted full card number that has been updated by the real-time account updater. + * [Optional] + */ private String encryptedCardNumber; + /** + * Specifies what card information was updated by the real-time account updater. + * [Optional] + * Enum: "card_updated" "card_expiry_updated" "card_closed" "contact_cardholder" + */ private AccountUpdateStatusType accountUpdateStatus; + /** + * Provides the failure code if the real-time account update failed. + * [Optional] + */ + private String accountUpdateFailureCode; + + /** + * Information about the account holder of the card. + * [Optional] + */ private AccountHolderResponse accountHolder; } diff --git a/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java b/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java index 3e7296bac..06f2d7d55 100644 --- a/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java @@ -4,13 +4,28 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Currency account source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. For this variant the + * specification declares the id required, with the pattern {@code ^(ca)_(\w{26})$}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CurrencyAccountResponseSource extends AbstractResponseSource implements ResponseSource { + /** + * If specified, indicates the amount in the source currency to be paid out. If omitted, the + * root amount in the destination currency is used. + * [Optional] + */ private Long amount; + /** + * The currency of the currency account. + * [Optional] + */ private String currency; } diff --git a/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java b/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java index cc6a4e13a..0690f0369 100644 --- a/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java @@ -6,6 +6,11 @@ import static com.checkout.common.PaymentSourceType.PAYPAL; +/** + * PayPal source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -21,6 +26,9 @@ public PayPalResponseSource() { this.type = PAYPAL; } + /** + * The PayPal account holder details. + */ @Data public static final class AccountHolder { diff --git a/src/main/java/com/checkout/payments/response/source/ResponseSource.java b/src/main/java/com/checkout/payments/response/source/ResponseSource.java index bdff48539..af7a63cd2 100644 --- a/src/main/java/com/checkout/payments/response/source/ResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/ResponseSource.java @@ -2,8 +2,19 @@ import com.checkout.common.PaymentSourceType; +/** + * The payment source returned on a payment response. + * + *

Implemented by the typed variants the specification declares, and by + * {@link AlternativePaymentSourceResponse} for any source type the SDK does not model. + */ public interface ResponseSource { + /** + * Returns the payment source type. + * + * @return the payment source type. + */ PaymentSourceType getType(); } diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 799577e00..716164053 100644 --- a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -270,12 +270,13 @@ public void write(final JsonWriter out, final R value) throws IOException { final JsonObject clone = new JsonObject(); + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } clone.add(typeFieldName, new JsonPrimitive(label)); for (final Map.Entry e : jsonObject.entrySet()) { - if (typeFieldName.equals(e.getKey())) { - continue; - } clone.add(e.getKey(), e.getValue()); } Streams.write(clone, out); diff --git a/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java b/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java new file mode 100644 index 000000000..657ac90a7 --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java @@ -0,0 +1,62 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.InstrumentType; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBankAccountRequest; +import com.checkout.instruments.create.CreateInstrumentSepaRequest; +import com.checkout.instruments.create.CreateInstrumentTokenRequest; +import com.checkout.instruments.update.UpdateInstrumentAchRequest; +import com.checkout.instruments.update.UpdateInstrumentBacsRequest; +import com.checkout.instruments.update.UpdateInstrumentBankAccountRequest; +import com.checkout.instruments.update.UpdateInstrumentCardRequest; +import com.checkout.instruments.update.UpdateInstrumentSepaRequest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Pins the type discriminator every instrument request sends. + * + *

The type selects the schema the API validates the request against, so a wrong value makes the + * whole request fail regardless of its other properties. UpdateInstrumentBankAccountRequest sent + * "token" until this test was added, and no test covered that class at all. + */ +class InstrumentRequestTypeTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSendTheCorrectTypeOnEveryCreateRequest() { + assertType("bank_account", new CreateInstrumentBankAccountRequest()); + assertType("token", new CreateInstrumentTokenRequest()); + assertType("sepa", new CreateInstrumentSepaRequest()); + assertType("ach", new CreateInstrumentAchRequest()); + assertType("bacs", new CreateInstrumentBacsRequest()); + } + + @Test + void shouldSendTheCorrectTypeOnEveryUpdateRequest() { + assertType("bank_account", new UpdateInstrumentBankAccountRequest()); + assertType("sepa", new UpdateInstrumentSepaRequest()); + assertType("ach", new UpdateInstrumentAchRequest()); + assertType("bacs", new UpdateInstrumentBacsRequest()); + } + + @Test + void shouldSendCardOnTheCardUpdateRequest() { + final UpdateInstrumentCardRequest request = UpdateInstrumentCardRequest.builder().build(); + + assertEquals(InstrumentType.CARD, request.getType()); + assertTrue(serializer.toJson(request).contains("\"type\":\"card\"")); + } + + private void assertType(final String expectedWireValue, final Object request) { + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"type\":\"" + expectedWireValue + "\""), + "expected type " + expectedWireValue + " but serialized " + json); + } +} From 74ad42bfed7f14c6562d1aa258b20cdfdda09fec Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 11:17:05 +0200 Subject: [PATCH 05/18] Gson fix --- src/main/java/com/checkout/GsonSerializer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 42ee1603c..0bea1e310 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -156,10 +156,10 @@ public final class GsonSerializer implements Serializer { .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.twintsource.TwintSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.TWINT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.vippssource.VippsSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.VIPPS)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.wechatpaysource.WechatpaySource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.WECHATPAY)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsegiropaysourcesource.PaymentGetResponseGiropaySourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_GIROPAY_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponseklarnasourcesource.PaymentGetResponseKlarnaSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_KLARNA_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsesepavfoursourcesource.PaymentGetResponseSEPAVFourSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_SEPAV4_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentresponsesourcesource.PaymentResponseSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_RESPONSE_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsegiropaysourcesource.PaymentGetResponseGiropaySourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_GIROPAY_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponseklarnasourcesource.PaymentGetResponseKlarnaSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_KLARNA_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsesepavfoursourcesource.PaymentGetResponseSEPAVFourSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_SEPAV4_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentresponsesourcesource.PaymentResponseSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_RESPONSE_SOURCE)) ) // Payments PREVIOUS - source .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.previous.response.source.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.previous.response.source.AlternativePaymentSourceResponse.class) From 4dbe7540386c149f0040daf5e29a54859e995253 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 31 Aug 2026 11:23:22 +0200 Subject: [PATCH 06/18] Some fixes --- .../CreateCustomerInstrumentRequest.java | 12 +++- .../CreateInstrumentBankAccountResponse.java | 13 +++- .../create/CreateInstrumentResponse.java | 16 ----- .../create/CreateInstrumentTokenResponse.java | 27 +++++++- .../instruments/create/InstrumentData.java | 33 +++++++-- .../instruments/get/BankAccountField.java | 13 +--- .../get/InstrumentCustomerResponse.java | 6 +- .../source/apm/RequestSwishAccountHolder.java | 30 +++++++++ .../apm/RequestSwishBillingDescriptor.java | 23 +++++++ .../source/apm/RequestSwishSource.java | 20 +++--- .../InstrumentSchemaRegressionTest.java | 67 +++++++++++++++++++ 11 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java create mode 100644 src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java diff --git a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java index c3a74006b..ad8178677 100644 --- a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java @@ -35,14 +35,22 @@ public final class CreateCustomerInstrumentRequest extends CustomerRequest { * [Optional] */ @SerializedName("default") - private boolean defaultInstrument; + private Boolean defaultInstrument; + + /** + * @deprecated Use {@link #getDefaultInstrument()}. + */ + @Deprecated + public Boolean isDefaultInstrument() { + return defaultInstrument; + } @Builder private CreateCustomerInstrumentRequest(final String email, final String name, final Phone phone, final String id, - final boolean defaultInstrument) { + final Boolean defaultInstrument) { super(email, name, phone); this.id = id; this.defaultInstrument = defaultInstrument; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java index ce59414b2..d9585c9b5 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java @@ -1,6 +1,7 @@ package com.checkout.instruments.create; import com.checkout.common.BankDetails; +import com.checkout.common.CustomerResponse; import com.checkout.common.InstrumentType; import lombok.Data; import lombok.EqualsAndHashCode; @@ -9,9 +10,9 @@ /** * Store bank account instrument response. * - *

The id, the fingerprint and the customer are inherited from - * {@link CreateInstrumentResponse}. This is one of only two store responses the specification - * declares a customer on, the other being {@link CreateInstrumentTokenResponse}. + *

The id and fingerprint are inherited from {@link CreateInstrumentResponse}. This is one of + * only two store responses the specification declares a customer on, the other being + * {@link CreateInstrumentTokenResponse}. */ @Data @EqualsAndHashCode(callSuper = true) @@ -54,4 +55,10 @@ public final class CreateInstrumentBankAccountResponse extends CreateInstrumentR */ private String iban; + /** + * The customer that the instrument is associated with. + * [Optional] + */ + private CustomerResponse customer; + } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java index e23af6050..cdf55cd88 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java @@ -1,7 +1,6 @@ package com.checkout.instruments.create; import com.checkout.HttpMetadata; -import com.checkout.common.CustomerResponse; import lombok.Data; import lombok.EqualsAndHashCode; @@ -26,19 +25,4 @@ public abstract class CreateInstrumentResponse extends HttpMetadata { */ protected String fingerprint; - /** - * The customer that the instrument is associated with. - * - *

Only the token and bank_account variants declare this property. The sepa, ach and bacs - * store responses do not, so on those three it is always null and does not belong here. - * [Optional] - * - * @deprecated declared on the wrong level. This will move to - * {@link CreateInstrumentTokenResponse} and {@link CreateInstrumentBankAccountResponse}, the - * only two variants the specification declares it on, in the next major version. Read it from - * the concrete response type rather than from this base class. - */ - @Deprecated - protected CustomerResponse customer; - } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java index 0318860c0..877d33441 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java @@ -3,7 +3,10 @@ import com.checkout.common.CardCategory; import com.checkout.common.CardType; import com.checkout.common.CountryCode; +import com.checkout.common.AccountHolderResponse; +import com.checkout.common.CustomerResponse; import com.checkout.common.InstrumentType; +import com.checkout.instruments.get.InstrumentNetworkToken; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -11,9 +14,9 @@ /** * Card instrument response. * - *

The id, the fingerprint and the customer are inherited from - * {@link CreateInstrumentResponse}. The instrument type is card rather than token: the - * specification maps the token store request onto a card store response. + *

The id and fingerprint are inherited from {@link CreateInstrumentResponse}. The instrument + * type is card rather than token: the specification maps the token store request onto a card + * store response. */ @Data @EqualsAndHashCode(callSuper = true) @@ -109,4 +112,22 @@ public final class CreateInstrumentTokenResponse extends CreateInstrumentRespons */ private String productType; + /** + * The account holder details. + * [Optional] + */ + private AccountHolderResponse accountHolder; + + /** + * The customer that the instrument is associated with. + * [Optional] + */ + private CustomerResponse customer; + + /** + * The network token provisioned for this instrument. + * [Optional] + */ + private InstrumentNetworkToken networkToken; + } diff --git a/src/main/java/com/checkout/instruments/create/InstrumentData.java b/src/main/java/com/checkout/instruments/create/InstrumentData.java index 8bde35dd1..9705d84bc 100644 --- a/src/main/java/com/checkout/instruments/create/InstrumentData.java +++ b/src/main/java/com/checkout/instruments/create/InstrumentData.java @@ -37,13 +37,36 @@ public final class InstrumentData { * [Required] * min 15 characters * max 34 characters - * - *

The Java field name carries a typo. The wire name is correct because of the explicit - * annotation, so this is a source-only defect; renaming the field is breaking and is deferred - * to the next major version. */ @SerializedName("account_number") - private String accoountNumber; + private String accountNumber; + + /** + * @deprecated Use {@link #getAccountNumber()}. + */ + @Deprecated + public String getAccoountNumber() { + return accountNumber; + } + + /** + * @deprecated Use {@link #setAccountNumber(String)}. + */ + @Deprecated + public void setAccoountNumber(final String accountNumber) { + this.accountNumber = accountNumber; + } + + public static class InstrumentDataBuilder { + + /** + * @deprecated Use {@link #accountNumber(String)}. + */ + @Deprecated + public InstrumentDataBuilder accoountNumber(final String accountNumber) { + return accountNumber(accountNumber); + } + } /** * The country of the account. diff --git a/src/main/java/com/checkout/instruments/get/BankAccountField.java b/src/main/java/com/checkout/instruments/get/BankAccountField.java index 62b42919b..66534082e 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountField.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountField.java @@ -1,6 +1,4 @@ package com.checkout.instruments.get; - -import com.google.gson.annotations.SerializedName; import lombok.Data; import java.util.List; @@ -45,7 +43,7 @@ public final class BankAccountField { * Whether the field is required. * [Required] */ - private boolean required; + private Boolean required; /** * A regular expression that can be used to validate the input of the field. @@ -57,18 +55,13 @@ public final class BankAccountField { * The minimum length of the field. * [Optional] */ - private int minLength; + private Integer minLength; /** * The maximum length of the field. * [Optional] - * - *

The Java field name is lowercase where the neighbouring minLength is camel case. The wire - * name is correct because of the explicit annotation, so this is a source-only defect; - * renaming the field is breaking and is deferred to the next major version. */ - @SerializedName("max_length") - private int maxlength; + private Integer maxLength; /** * The allowed options for the field. diff --git a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java index 20e729704..1658c307a 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java @@ -23,6 +23,10 @@ public final class InstrumentCustomerResponse extends CustomerResponse { * [Optional] */ @SerializedName("default") - private boolean isDefault; + private Boolean isDefault; + + public Boolean isDefault() { + return isDefault; + } } diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java new file mode 100644 index 000000000..f4f81e148 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java @@ -0,0 +1,30 @@ +package com.checkout.payments.request.source.apm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details for a Swish payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestSwishAccountHolder { + + /** + * The account holder's first name. + * [Required] + * max 50 characters + */ + private String firstName; + + /** + * The account holder's last name. + * [Required] + * max 50 characters + */ + private String lastName; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java new file mode 100644 index 000000000..3c826f085 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java @@ -0,0 +1,23 @@ +package com.checkout.payments.request.source.apm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing descriptor for a Swish payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestSwishBillingDescriptor { + + /** + * A description for the payment, which displays on the customer's statement. + * [Required] + * max 120 characters + */ + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java index 791042133..4f3a53e05 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java @@ -1,8 +1,7 @@ package com.checkout.payments.request.source.apm; -import com.checkout.common.AccountHolder; +import com.checkout.common.CountryCode; import com.checkout.common.PaymentSourceType; -import com.checkout.payments.BillingDescriptor; import com.checkout.payments.request.source.AbstractRequestSource; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -21,26 +20,27 @@ public final class RequestSwishSource extends AbstractRequestSource { /** * The two-letter ISO country code of the payment. - * [Optional] + * [Required] + * Enum: "SE" */ - private String paymentCountry; + private CountryCode paymentCountry; /** * The account holder's details. - * [Optional] + * [Required] */ - private AccountHolder accountHolder; + private RequestSwishAccountHolder accountHolder; /** * A description of the purchase shown on the customer's statement. * [Optional] */ - private BillingDescriptor billingDescriptor; + private RequestSwishBillingDescriptor billingDescriptor; @Builder - private RequestSwishSource(final String paymentCountry, - final AccountHolder accountHolder, - final BillingDescriptor billingDescriptor) { + private RequestSwishSource(final CountryCode paymentCountry, + final RequestSwishAccountHolder accountHolder, + final RequestSwishBillingDescriptor billingDescriptor) { super(PaymentSourceType.SWISH); this.paymentCountry = paymentCountry; this.accountHolder = accountHolder; diff --git a/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java b/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java new file mode 100644 index 000000000..e55286406 --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java @@ -0,0 +1,67 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentResponse; +import com.checkout.instruments.create.CreateInstrumentTokenResponse; +import com.checkout.instruments.get.BankAccountField; +import com.checkout.instruments.get.InstrumentCustomerResponse; +import com.checkout.payments.request.source.apm.RequestSwishAccountHolder; +import com.checkout.payments.request.source.apm.RequestSwishBillingDescriptor; +import com.checkout.payments.request.source.apm.RequestSwishSource; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class InstrumentSchemaRegressionTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldOmitUnsetInstrumentCustomerDefaultsAndBankAccountFieldValues() { + final String customerJson = serializer.toJson(CreateCustomerInstrumentRequest.builder().build()); + final BankAccountField field = serializer.fromJson("{\"id\":\"iban\",\"display\":\"IBAN\",\"type\":\"string\"}", BankAccountField.class); + final InstrumentCustomerResponse customer = serializer.fromJson("{}", InstrumentCustomerResponse.class); + + assertFalse(customerJson.contains("\"default\"")); + assertNull(field.getRequired()); + assertNull(field.getMinLength()); + assertNull(field.getMaxLength()); + assertNull(customer.isDefault()); + } + + @Test + void shouldDeserializeAllTokenStoreResponseProperties() { + final String json = "{\"type\":\"card\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\",\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\",\"expiry_month\":6,\"expiry_year\":2025,\"last4\":\"9996\",\"bin\":\"454347\",\"account_holder\":{\"first_name\":\"Hannah\",\"last_name\":\"Bret\"},\"network_token\":{\"id\":\"nt_y3oqhf46pyzuxjbcn2giaqnb44\",\"state\":\"active\"}}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + final CreateInstrumentTokenResponse token = assertInstanceOf(CreateInstrumentTokenResponse.class, response); + + assertEquals("Hannah", token.getAccountHolder().getFirstName()); + assertEquals("nt_y3oqhf46pyzuxjbcn2giaqnb44", token.getNetworkToken().getId()); + } + + @Test + void shouldSerializeAndDeserializeOnlySwishSchemaFields() { + final RequestSwishSource source = RequestSwishSource.builder() + .paymentCountry(CountryCode.SE) + .accountHolder(RequestSwishAccountHolder.builder().firstName("Bruce").lastName("Wayne").build()) + .billingDescriptor(RequestSwishBillingDescriptor.builder().name("CKO Store").build()) + .build(); + + final String json = serializer.toJson(source); + final RequestSwishSource deserialized = serializer.fromJson(json, RequestSwishSource.class); + + assertTrue(json.contains("\"payment_country\":\"SE\"")); + assertTrue(json.contains("\"account_holder\":{\"first_name\":\"Bruce\",\"last_name\":\"Wayne\"}")); + assertTrue(json.contains("\"billing_descriptor\":{\"name\":\"CKO Store\"}")); + assertEquals(CountryCode.SE, deserialized.getPaymentCountry()); + assertEquals("Bruce", deserialized.getAccountHolder().getFirstName()); + assertEquals("CKO Store", deserialized.getBillingDescriptor().getName()); + } +} \ No newline at end of file From e4a6770bd30bf836c60149c462b956363fd37088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:32:46 +0200 Subject: [PATCH 07/18] feat: require environmentSubdomain or an explicit useLegacyDomain opt-out (#649) * feat: require environmentSubdomain or an explicit useLegacyDomain opt-out The merchant-specific subdomain is how merchants should reach the API, but it was optional and an unset value silently fell back to api.checkout.com, so a forgotten subdomain looked exactly like a deliberate opt-out and the SDK could not warn about either. Callers must now choose: set environmentSubdomain, or call the already-deprecated useLegacyDomain(). Both, or neither, throws. An invalid subdomain now throws instead of being quietly ignored, which is a second breaking change: callers passing a malformed value are currently served by the shared host and never find out. environmentSubdomain no longer needs environment() to be set first, since the EnvironmentSubdomain is now built when the configuration is assembled. The Previous (ABC) platform predates merchant-specific subdomains and stays exempt via requiresEnvironmentSubdomain(). Mirrors checkout-sdk-net#590. Refs INT-1688. * test: route every client the suite builds through TestDomainConfiguration Seven integration fixtures build their own clients outside SandboxTestFixture (OAuth, Issuing, Accounts, Accounts payout schedules, APM previews, card metadata), so the mandatory subdomain would have failed them at construction. They now share TestDomainConfiguration.configureDomain, which uses the shared hosts. Applying the merchant-specific subdomain instead looked better, since it is the path merchants are being moved to, but the sandbox OAuth clients are not provisioned for it: .NET CI failed 224 integration tests with invalid_client when the token request went to {subdomain}.access.sandbox.checkout.com. The reason is recorded on the class so nobody repeats the experiment. * test: route the remaining OAuth integration tests through TestDomainConfiguration CI runs the full suite, so three OAuthTestIT cases that build their own client were still failing at construction. Local runs excluded integration tests, which is why they were missed. * test: stop passing a subdomain to the Previous platform builder Flagged in review, and fair: a bulk edit added the subdomain to the Previous (ABC) builder, the one platform that is exempt from needing one. That made the test misleading and, worse, removed the only coverage of the exemption actually working. It builds without a subdomain again. * test: add a switch to run the suite against the merchant subdomain The suite could only run against the shared hosts, so the subdomain path this PR makes mandatory had no integration coverage. Reviewers flagged that on every SDK, and it is the right thing to flag. The domain helper now has two modes. Default is unchanged, the shared hosts, because the sandbox OAuth clients are not provisioned for the subdomain and the token request returns invalid_client. Set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against CHECKOUT_MERCHANT_SUBDOMAIN instead, so once sandbox is provisioned like production it is a one-line change in the workflows, already wired and documented, rather than a rewrite of every fixture. The switch is deliberately separate from CHECKOUT_MERCHANT_SUBDOMAIN, which CI already exports: provisioning should drive the behaviour, not the presence of a secret. * revert: leave the version bump to the release Versions are bumped on master during the release, not in a feature branch, per the release workflow. This branch should carry only the change itself; the major bump is classified and applied when the release is cut. * revert: drop the test domain helpers and the workflow variable Two problems with the previous approach. It needed a new variable in 21 workflow files, which is not viable without access to create secrets. And it wrapped the builder chain in a configureDomain helper that is not part of the public API, so the tests stopped looking like the code a merchant would actually write. Every fixture now calls the real opt-out inline, in the chain, with a comment saying why: the sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the token request comes back invalid_client. When sandbox is provisioned, those calls become the subdomain setter. The unit tests covering all four combinations are untouched: they already used the public API directly. * chore: align the version file with master, the major bump happens at release * test: run the issuing and payout schedule suites with the default OAuth client The dedicated sandbox clients are not provisioned for the merchant subdomain; the default client now carries every scope the suites need. * Revert "test: run the issuing and payout schedule suites with the default OAuth client" This reverts commit 2e14a23ad864f2fdc9a69965341831f6ff9fc94d. * fix: address the review findings on the subdomain PR - Sandbox test fixture: static-keys clients now use the subdomain from CHECKOUT_MERCHANT_SUBDOMAIN; the legacy-domain opt-out stays only on the OAuth client (sandbox OAuth clients lack subdomain provisioning), and the Previous platform applies neither since it is exempt - Hoist the subdomain validation regex to a private static final constant (Sonar S4248) - Reword the subdomain error messages: typically your client ID excluding the cli_ prefix - Treat environmentSubdomain(null) as unset and fail at build time with the standard required-subdomain error, matching the other SDKs; test added - README: note that Private Link merchants use their pl- prefixed subdomain, which the SDK also accepts * fix: reject an explicit authorization URI combined with the environment subdomain * fix: suppress the deliberate legacy-domain deprecation warnings in tests Also fixes a real miss: CardMetadataIT.createStaticKeyApi() used the legacy opt-out on a static-keys client, which never calls the token endpoint and so was never blocked by the sandbox OAuth provisioning gap - it now runs against the real merchant subdomain like the other static-keys fixtures. The other six sites are genuine OAuth clients whose sandbox clients lack subdomain provisioning; @SuppressWarnings("deprecation") marks that as deliberate instead of leaving 15 code-scanning findings open. --- README.md | 31 ++++- .../checkout/AbstractCheckoutSdkBuilder.java | 45 ++++++- .../checkout/CheckoutPreviousSdkBuilder.java | 7 ++ .../java/com/checkout/CheckoutSdkBuilder.java | 9 +- .../com/checkout/EnvironmentSubdomain.java | 36 +++--- .../CheckoutSdkBuilderSynchronousTest.java | 5 + .../com/checkout/CheckoutSdkBuilderTest.java | 116 +++++++++++++++++- .../CheckoutSdkTelemetryIntegrationTest.java | 1 + .../DefaultCheckoutConfigurationTest.java | 12 +- src/test/java/com/checkout/OAuthTestIT.java | 13 ++ .../java/com/checkout/SandboxTestFixture.java | 8 +- .../SynchronousAsyncClientComparisonTest.java | 6 + .../checkout/_external/CheckoutSdkTest.java | 3 + .../accounts/AccountsPayoutSchedulesIT.java | 4 + .../com/checkout/accounts/AccountsTestIT.java | 4 + .../checkout/issuing/BaseIssuingTestIT.java | 4 + .../com/checkout/metadata/CardMetadataIT.java | 3 + .../payments/RequestApmPaymentsIT.java | 4 + 18 files changed, 266 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 1c58463ef..be4e0ebd7 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ If you don't have your own API keys, you can sign up for a test account [here](h **PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.** +### Subdomain value + +Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environmentSubdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID. Private Link merchants use their `pl-` prefixed subdomain (for example `pl-vkuhvk4v`), which the SDK also accepts. + ### Default Default keys client instantiation can be done as follows: @@ -99,7 +103,7 @@ public static void main(String[] args) { .publicKey("public_key") // optional, only required for operations related with tokens .secretKey("secret_key") .environment(Environment.PRODUCTION) // required - .environmentSubdomain("subdomain") // optional, Merchant-specific DNS name + .environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID .executor() // optional for a custom Executor Service .build(); @@ -121,11 +125,9 @@ public static void main(String[] args) { final CheckoutApi checkoutApi = CheckoutSdk.builder() .oAuth() .clientCredentials("client_id", "client_secret") - // for a specific authorization endpoint - //.clientCredentials(new URI("https://access.sandbox.checkout.com/connect/token"), "client_id", "client_secret") .scopes(OAuthScope.GATEWAY, OAuthScope.VAULT, OAuthScope.FX) .environment(Environment.PRODUCTION) // required - .environmentSubdomain("subdomain") // optional, Merchant-specific DNS name + .environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID .executor() // optional for a custom Executor Service .build(); @@ -149,7 +151,7 @@ public static void main(String[] args) { .publicKey("public_key") // optional, only required for operations related with tokens .secretKey("secret_key") .environment(Environment.PRODUCTION) // required - .environmentSubdomain("subdomain") // optional, Merchant-specific DNS name + .environmentSubdomain("subdomain") // optional for the Previous platform, Merchant-specific DNS name .executor() // optional for a custom Executor Service .build(); @@ -399,7 +401,7 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder() .staticKeys() .secretKey("secret_key") .environment(Environment.PRODUCTION) // required - .environmentSubdomain("subdomain") // optional, Merchant-specific DNS name + .environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID .httpClientBuilder(customHttpClient) // optional for a custom HttpClient .build(); ``` @@ -656,6 +658,23 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder() - All resilience patterns are optional - configure only what you need - Rate limiter helps respect API rate limits and prevent overwhelming the service +## Legacy domain (emergency use only) + +> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated. + +If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `useLegacyDomain()` instead of `environmentSubdomain(...)`: + +```java +final CheckoutApi checkoutApi = CheckoutSdk.builder() + .staticKeys() + .secretKey("secret_key") + .environment(Environment.SANDBOX) + .useLegacyDomain() // deprecated, emergency fallback only + .build(); +``` + +This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method is annotated `@Deprecated` and produces a compile-time warning. Exactly one of `environmentSubdomain(...)` or `useLegacyDomain()` must be set: the SDK throws a `CheckoutArgumentException` if both, or neither, are set. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement. + ## Code of Conduct Please refer to [Code of Conduct](CODE_OF_CONDUCT.md) diff --git a/src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java b/src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java index 0a193487f..4bf2a9583 100644 --- a/src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java +++ b/src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java @@ -9,7 +9,8 @@ public abstract class AbstractCheckoutSdkBuilder { protected HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); private IEnvironment environment; - private EnvironmentSubdomain environmentSubdomain; + private String subdomain; + private boolean useLegacyDomain; private Executor executor = ForkJoinPool.commonPool(); private TransportConfiguration transportConfiguration; private Boolean recordTelemetry = true; @@ -22,10 +23,23 @@ public AbstractCheckoutSdkBuilder environment(final IEnvironment environment) } public AbstractCheckoutSdkBuilder environmentSubdomain(final String subdomain) { - if (subdomain == null) { - throw new CheckoutArgumentException("subdomain must be specified"); - } - this.environmentSubdomain = new EnvironmentSubdomain(this.environment, subdomain); + this.subdomain = subdomain; + return this; + } + + /** + * Opts out of the merchant-specific subdomain, sending every request to the shared + * hosts instead ({@code api.checkout.com} and {@code access.checkout.com}, or their + * sandbox equivalents). + * + * @deprecated this is an emergency fallback for the rare case where the + * merchant-specific subdomain cannot be used, and will be removed in a future release. + * Call {@link #environmentSubdomain(String)} instead. + * See Base URLs. + */ + @Deprecated + public AbstractCheckoutSdkBuilder useLegacyDomain() { + this.useLegacyDomain = true; return this; } @@ -49,7 +63,16 @@ protected IEnvironment getEnvironment() { } protected EnvironmentSubdomain getEnvironmentSubdomain() { - return environmentSubdomain; + return subdomain != null ? new EnvironmentSubdomain(environment, subdomain) : null; + } + + /** + * Whether this builder requires the merchant-specific subdomain to be configured. + * The Previous (ABC) platform predates merchant-specific subdomains, so it overrides + * this to {@code false}. + */ + protected boolean requiresEnvironmentSubdomain() { + return true; } public AbstractCheckoutSdkBuilder recordTelemetry(final Boolean recordTelemetry) { @@ -73,6 +96,7 @@ protected CheckoutConfiguration getCheckoutConfiguration() { if (environment == null) { throw new CheckoutArgumentException("environment must be specified"); } + validateEnvironmentSettings(); final SdkCredentials sdkCredentials = getSdkCredentials(); if (transportConfiguration == null) { transportConfiguration = new DefaultTransportConfiguration(); @@ -80,6 +104,15 @@ protected CheckoutConfiguration getCheckoutConfiguration() { return buildCheckoutConfiguration(sdkCredentials); } + private void validateEnvironmentSettings() { + if (subdomain != null && useLegacyDomain) { + throw new CheckoutArgumentException("environmentSubdomain and useLegacyDomain cannot both be set - provide only your merchant-specific subdomain"); + } + if (subdomain == null && !useLegacyDomain && requiresEnvironmentSubdomain()) { + throw new CheckoutArgumentException("environmentSubdomain is required - provide your merchant-specific subdomain (typically your client ID excluding the cli_ prefix, see https://api-reference.checkout.com/#section/Base-URLs), or call useLegacyDomain() to opt out only if merchant specific sub domains are causing issues"); + } + } + private CheckoutConfiguration buildCheckoutConfiguration(final SdkCredentials sdkCredentials) { return new DefaultCheckoutConfiguration(sdkCredentials, getEnvironment(), getEnvironmentSubdomain(), httpClientBuilder, executor, transportConfiguration, recordTelemetry, synchronous, resilience4jConfiguration); } diff --git a/src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java b/src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java index 196f4ba2a..f4462a18b 100644 --- a/src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java +++ b/src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java @@ -13,6 +13,13 @@ public static class CheckoutStaticKeysSdkBuilder extends AbstractCheckoutSdkBuil private String publicKey; private String secretKey; + // The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt + // from the mandatory environmentSubdomain/useLegacyDomain configuration. + @Override + protected boolean requiresEnvironmentSubdomain() { + return false; + } + public CheckoutStaticKeysSdkBuilder publicKey(final String publicKey) { this.publicKey = publicKey; return this; diff --git a/src/main/java/com/checkout/CheckoutSdkBuilder.java b/src/main/java/com/checkout/CheckoutSdkBuilder.java index b8bfeb189..11421b2c2 100644 --- a/src/main/java/com/checkout/CheckoutSdkBuilder.java +++ b/src/main/java/com/checkout/CheckoutSdkBuilder.java @@ -50,14 +50,17 @@ public CheckoutOAuthSdkBuilder scopes(final OAuthScope... scopes) { @Override protected SdkCredentials getSdkCredentials() { + final EnvironmentSubdomain environmentSubdomain = getEnvironmentSubdomain(); + if (this.authorizationUri != null && environmentSubdomain != null) { + throw new CheckoutArgumentException("AuthorizationUri and environmentSubdomain cannot both be set - the token endpoint is derived from your subdomain. Combine authorizationUri with useLegacyDomain() if you need a custom token host."); + } if (this.authorizationUri == null) { final IEnvironment environment = getEnvironment(); if (environment == null) { throw new CheckoutArgumentException("Invalid configuration. Please specify an Environment or a specific OAuth authorizationURI."); } - final EnvironmentSubdomain envSubdomain = getEnvironmentSubdomain(); - if (envSubdomain != null) { - this.authorizationUri = envSubdomain.getOAuthAuthorizationApi(); + if (environmentSubdomain != null) { + this.authorizationUri = environmentSubdomain.getOAuthAuthorizationApi(); } else { this.authorizationUri = environment.getOAuthAuthorizationApi(); } diff --git a/src/main/java/com/checkout/EnvironmentSubdomain.java b/src/main/java/com/checkout/EnvironmentSubdomain.java index 54841a6ed..40aed4b51 100644 --- a/src/main/java/com/checkout/EnvironmentSubdomain.java +++ b/src/main/java/com/checkout/EnvironmentSubdomain.java @@ -7,6 +7,8 @@ public final class EnvironmentSubdomain { + private static final Pattern SUBDOMAIN_PATTERN = Pattern.compile("^(?:pl-)?[a-z0-9]+$"); + private URI checkoutApi; private URI oAuthAuthorizationApi; @@ -24,36 +26,28 @@ public URI getOAuthAuthorizationApi() { } /** - * Applies subdomain transformation to any given URI. - * If the subdomain is valid (alphanumeric pattern), prepends it to the host. - * Otherwise, returns the original URI unchanged. + * Applies subdomain transformation to any given URI, prepending the subdomain to the host. * * @param originalUrl the original URI to transform * @param subdomain the subdomain to prepend - * @return the transformed URI with subdomain, or original URI if subdomain is invalid + * @return the transformed URI with subdomain + * @throws CheckoutArgumentException if the subdomain is not a valid merchant-specific subdomain */ private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) { - URI newEnvironment = null; + Matcher matcher = subdomain == null ? null : SUBDOMAIN_PATTERN.matcher(subdomain); + if (matcher == null || !matcher.matches()) { + throw new CheckoutArgumentException("invalid environment subdomain - provide your merchant-specific subdomain, typically your client ID excluding the cli_ prefix (see https://api-reference.checkout.com/#section/Base-URLs)"); + } + + String host = originalUrl.getHost(); + String scheme = originalUrl.getScheme(); + int port = originalUrl.getPort(); + String newHost = subdomain + "." + host; try { - newEnvironment = new URI(originalUrl.toString()); + return new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment()); } catch (final URISyntaxException e) { throw new CheckoutException(e); } - - Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$"); - Matcher matcher = pattern.matcher(subdomain); - if (matcher.matches()) { - String host = originalUrl.getHost(); - String scheme = originalUrl.getScheme(); - int port = originalUrl.getPort(); - String newHost = subdomain + "." + host; - try { - newEnvironment = new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment()); - } catch (final URISyntaxException e) { - throw new CheckoutException(e); - } - } - return newEnvironment; } } diff --git a/src/test/java/com/checkout/CheckoutSdkBuilderSynchronousTest.java b/src/test/java/com/checkout/CheckoutSdkBuilderSynchronousTest.java index 859bc8fc4..77efd4744 100644 --- a/src/test/java/com/checkout/CheckoutSdkBuilderSynchronousTest.java +++ b/src/test/java/com/checkout/CheckoutSdkBuilderSynchronousTest.java @@ -18,6 +18,7 @@ void shouldCreateCheckoutApiWithSynchronousMode() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) .build(); @@ -35,6 +36,7 @@ void shouldCreateCheckoutApiWithResilience4jConfiguration() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .resilience4jConfiguration(resilience4jConfig) .build(); @@ -49,6 +51,7 @@ void shouldCreateCheckoutApiWithSynchronousAndResilience4j() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) .resilience4jConfiguration(resilience4jConfig) .build(); @@ -63,6 +66,7 @@ void shouldCreateCheckoutApiWithoutNewParameters() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); assertNotNull(checkoutApi); @@ -90,6 +94,7 @@ void shouldCreateCheckoutApiWithCustomResilience4jConfiguration() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) .resilience4jConfiguration(resilience4jConfig) .build(); diff --git a/src/test/java/com/checkout/CheckoutSdkBuilderTest.java b/src/test/java/com/checkout/CheckoutSdkBuilderTest.java index 77bdee7df..72af3aae0 100644 --- a/src/test/java/com/checkout/CheckoutSdkBuilderTest.java +++ b/src/test/java/com/checkout/CheckoutSdkBuilderTest.java @@ -11,6 +11,7 @@ import static com.checkout.TestHelper.VALID_DEFAULT_SK; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -23,6 +24,7 @@ void shouldCreateStaticKeysCheckoutSdks() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); assertNotNull(checkoutApi1); @@ -30,6 +32,7 @@ void shouldCreateStaticKeysCheckoutSdks() { final CheckoutApi checkoutApi2 = new CheckoutSdkBuilder().staticKeys() .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); assertNotNull(checkoutApi2); @@ -59,16 +62,37 @@ void shouldCreateStaticKeysCheckoutWithSubdomainSdks() { } @Test - void shouldCreateCheckoutAndInitOAuthSdk() throws URISyntaxException { + void shouldFailToCreateOAuthSdkWithBothAuthorizationUriAndSubdomain() throws URISyntaxException { + + final URI authorizationUri = new URI("https://access.sandbox.checkout.com/connect/token"); + + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new CheckoutSdkBuilder().oAuth() + .clientCredentials(authorizationUri, "client_id", "client_secret") + .scopes(OAuthScope.GATEWAY) + .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") + .build()); + + assertEquals("AuthorizationUri and environmentSubdomain cannot both be set - the token endpoint is derived from your subdomain. Combine authorizationUri with useLegacyDomain() if you need a custom token host.", exception.getMessage()); + + } + + @SuppressWarnings("deprecation") + @Test + void shouldCreateOAuthSdkWithExplicitAuthorizationUriAndLegacyDomain() throws URISyntaxException { try { new CheckoutSdkBuilder().oAuth() .clientCredentials(new URI("test"), "client_id", "client_secret") .scopes(OAuthScope.GATEWAY) .environment(Environment.SANDBOX) + .useLegacyDomain() .build(); fail(); } catch (final CheckoutException e) { + // The generic failure (no invalid_client from the shared sandbox host) proves the + // token request was sent to the explicit authorization URI, not the environment default assertEquals("OAuth client_credentials authentication failed", e.getMessage()); } @@ -94,6 +118,93 @@ void shouldCreateOAuthSdkWithSubdomain() throws URISyntaxException { } + @SuppressWarnings("deprecation") + @Test + void shouldCreateStaticKeysCheckoutSdkWithLegacyDomain() { + + final CheckoutApi checkoutApi = new CheckoutSdkBuilder().staticKeys() + .publicKey(VALID_DEFAULT_PK) + .secretKey(VALID_DEFAULT_SK) + .environment(Environment.SANDBOX) + .useLegacyDomain() + .build(); + + assertNotNull(checkoutApi); + + } + + @Test + void shouldFailToCreateCheckoutSdkWithoutSubdomainOrLegacyDomain() { + + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new CheckoutSdkBuilder().staticKeys() + .publicKey(VALID_DEFAULT_PK) + .secretKey(VALID_DEFAULT_SK) + .environment(Environment.SANDBOX) + .build()); + + assertTrue(exception.getMessage().contains("environmentSubdomain is required")); + + } + + @Test + void shouldTreatNullSubdomainAsUnsetAndFailAtBuildTime() { + + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new CheckoutSdkBuilder().staticKeys() + .publicKey(VALID_DEFAULT_PK) + .secretKey(VALID_DEFAULT_SK) + .environment(Environment.SANDBOX) + .environmentSubdomain(null) + .build()); + + assertTrue(exception.getMessage().contains("environmentSubdomain is required")); + + } + + @SuppressWarnings("deprecation") + @Test + void shouldFailToCreateCheckoutSdkWithBothSubdomainAndLegacyDomain() { + + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new CheckoutSdkBuilder().staticKeys() + .publicKey(VALID_DEFAULT_PK) + .secretKey(VALID_DEFAULT_SK) + .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") + .useLegacyDomain() + .build()); + + assertTrue(exception.getMessage().contains("cannot both be set")); + + } + + @Test + void shouldFailToCreateCheckoutSdkWithInvalidSubdomain() { + + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new CheckoutSdkBuilder().staticKeys() + .publicKey(VALID_DEFAULT_PK) + .secretKey(VALID_DEFAULT_SK) + .environment(Environment.SANDBOX) + .environmentSubdomain("not a subdomain") + .build()); + + assertTrue(exception.getMessage().contains("invalid environment subdomain")); + + } + + @Test + void shouldCreatePreviousSdkWithoutSubdomain() { + + assertNotNull(new CheckoutSdkBuilder().previous().staticKeys() + .publicKey(TestHelper.VALID_PREVIOUS_PK) + .secretKey(TestHelper.VALID_PREVIOUS_SK) + .environment(Environment.SANDBOX) + .build()); + + } + @Test void shouldFailToCreateCheckoutSdks() { @@ -102,6 +213,7 @@ void shouldFailToCreateCheckoutSdks() { .publicKey(INVALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); } catch (final Exception e) { assertTrue(e instanceof CheckoutArgumentException); @@ -113,6 +225,7 @@ void shouldFailToCreateCheckoutSdks() { .publicKey(VALID_DEFAULT_PK) .secretKey(INVALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); } catch (final Exception e) { assertTrue(e instanceof CheckoutArgumentException); @@ -123,6 +236,7 @@ void shouldFailToCreateCheckoutSdks() { new CheckoutSdkBuilder().staticKeys() .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) + .environmentSubdomain("1234doma") .build(); } catch (final Exception e) { assertTrue(e instanceof CheckoutArgumentException); diff --git a/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java b/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java index d6061afe6..6d4376067 100644 --- a/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java +++ b/src/test/java/com/checkout/CheckoutSdkTelemetryIntegrationTest.java @@ -67,6 +67,7 @@ private CheckoutApi buildCheckoutApi(CloseableHttpClient httpClientMock, boolean .secretKey(requireNonNull(System.getenv("CHECKOUT_DEFAULT_SECRET_KEY"))) .recordTelemetry(telemetryEnabled) .environment(SANDBOX) + .environmentSubdomain("1234doma") .httpClientBuilder(httpClientBuilderMock) .build(); } diff --git a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java index e8f9da6e4..23dd8e852 100644 --- a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java +++ b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -66,14 +67,11 @@ void shouldCreateConfigurationWithSubdomain(String subdomain) { @ParameterizedTest @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1", "foo-", "-foo", "ABC123", "FOO", "test-123", "foo-bar", "pl-"}) - void shouldCreateConfigurationWithBadSubdomain(String subdomain) { + void shouldFailWithBadSubdomain(String subdomain) { - final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); - final EnvironmentSubdomain environmentSubdomain = new EnvironmentSubdomain(Environment.SANDBOX, subdomain); - - final CheckoutConfiguration configuration = new DefaultCheckoutConfiguration(credentials, Environment.SANDBOX, environmentSubdomain, DEFAULT_CLIENT_BUILDER, DEFAULT_EXECUTOR, DEFAULT_TRANSPORT_CONFIGURATION, false); - assertEquals("https://api.sandbox.checkout.com/", configuration.getEnvironmentSubdomain().getCheckoutApi().toString()); - assertEquals("https://access.sandbox.checkout.com/connect/token", configuration.getEnvironmentSubdomain().getOAuthAuthorizationApi().toString()); + final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class, + () -> new EnvironmentSubdomain(Environment.SANDBOX, subdomain)); + assertTrue(exception.getMessage().contains("invalid environment subdomain")); } @Test diff --git a/src/test/java/com/checkout/OAuthTestIT.java b/src/test/java/com/checkout/OAuthTestIT.java index 2fe1f0bf8..e3e349971 100644 --- a/src/test/java/com/checkout/OAuthTestIT.java +++ b/src/test/java/com/checkout/OAuthTestIT.java @@ -70,6 +70,7 @@ void shouldMakeOAuthCall() { } + @SuppressWarnings("deprecation") @Test void shouldInitAuthorization() { @@ -82,6 +83,7 @@ void shouldInitAuthorization() { "fake") .scopes(OAuthScope.GATEWAY) .environment(Environment.SANDBOX) + .useLegacyDomain() .build(); fail(); } catch (final Exception e) { @@ -109,6 +111,7 @@ void shouldFailInitAuthorization() { } @Test + @SuppressWarnings("deprecation") void shouldInstantiateCheckoutApiWithOAuth_defaultAuthorizeUrl() { final CheckoutApi checkoutApi = CheckoutSdk.builder() @@ -118,6 +121,9 @@ void shouldInstantiateCheckoutApiWithOAuth_defaultAuthorizeUrl() { System.getenv("CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET")) .scopes(OAuthScope.GATEWAY) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); assertNotNull(checkoutApi); @@ -136,6 +142,9 @@ void shouldInstantiateCheckoutApiWithOAuth_customAuthorizeUrl() throws URISyntax System.getenv("CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET")) .scopes(OAuthScope.GATEWAY) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); assertNotNull(checkoutApi); @@ -155,6 +164,10 @@ void shouldFailInitAuthorizationWithCustomEnvironment() { .environment(CustomEnvironment.builder() .oAuthAuthorizationApi(create("https://the.oauth.uri/connect/token")) .build()) + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + .useLegacyDomain() .build(); fail(); } catch (final Exception e) { diff --git a/src/test/java/com/checkout/SandboxTestFixture.java b/src/test/java/com/checkout/SandboxTestFixture.java index a88588b4d..3b8bdfac0 100644 --- a/src/test/java/com/checkout/SandboxTestFixture.java +++ b/src/test/java/com/checkout/SandboxTestFixture.java @@ -42,6 +42,7 @@ public abstract class SandboxTestFixture { protected TokensClient tokensClient; + @SuppressWarnings("deprecation") public SandboxTestFixture(final PlatformType platformType) { switch (platformType) { case PREVIOUS: @@ -68,6 +69,7 @@ public SandboxTestFixture(final PlatformType platformType) { .environment(Environment.SANDBOX) .executor(Executors.newFixedThreadPool(100)) .httpClientBuilder(httpClientBuilder) + .environmentSubdomain(requireNonNull(System.getenv("CHECKOUT_MERCHANT_SUBDOMAIN"))) .build(); break; case DEFAULT: @@ -77,6 +79,7 @@ public SandboxTestFixture(final PlatformType platformType) { .secretKey(requireNonNull(System.getenv("CHECKOUT_DEFAULT_SECRET_KEY"))) .environment(Environment.SANDBOX) .executor(CUSTOM_EXECUTOR) + .environmentSubdomain(requireNonNull(System.getenv("CHECKOUT_MERCHANT_SUBDOMAIN"))) .build(); break; case DEFAULT_OAUTH: @@ -93,8 +96,11 @@ public SandboxTestFixture(final PlatformType platformType) { OAuthScope.FORWARD_SECRETS, OAuthScope.PAYMENTS_SEARCH) .environment(Environment.SANDBOX) .executor(CUSTOM_EXECUTOR) + // The sandbox OAuth clients lack subdomain provisioning, so the token request + // would come back invalid_client. Opting out explicitly until they are provisioned. + .useLegacyDomain() .build(); - case CUSTOM: + case CUSTOM: break; } } diff --git a/src/test/java/com/checkout/SynchronousAsyncClientComparisonTest.java b/src/test/java/com/checkout/SynchronousAsyncClientComparisonTest.java index ef7c15467..0950d6e2a 100644 --- a/src/test/java/com/checkout/SynchronousAsyncClientComparisonTest.java +++ b/src/test/java/com/checkout/SynchronousAsyncClientComparisonTest.java @@ -28,6 +28,7 @@ void shouldUseSameMethodsForSyncAndAsyncClients() throws ExecutionException, Int .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) // Synchronous mode .build(); @@ -36,6 +37,7 @@ void shouldUseSameMethodsForSyncAndAsyncClients() throws ExecutionException, Int .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(false) // Asynchronous mode (default) .build(); @@ -111,6 +113,7 @@ void shouldHaveSameInterfaceForSyncAndAsyncClients() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) .build(); @@ -118,6 +121,7 @@ void shouldHaveSameInterfaceForSyncAndAsyncClients() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(false) .build(); @@ -142,6 +146,7 @@ void shouldGetSameResponseTypeFromBothClients() throws ExecutionException, Inter .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(true) .build(); @@ -149,6 +154,7 @@ void shouldGetSameResponseTypeFromBothClients() throws ExecutionException, Inter .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .synchronous(false) .build(); diff --git a/src/test/java/com/checkout/_external/CheckoutSdkTest.java b/src/test/java/com/checkout/_external/CheckoutSdkTest.java index 84a4b3a3e..9f5bc18f8 100644 --- a/src/test/java/com/checkout/_external/CheckoutSdkTest.java +++ b/src/test/java/com/checkout/_external/CheckoutSdkTest.java @@ -37,6 +37,8 @@ class CheckoutSdkTest { @Test void shouldCreatePreviousSdk() { + // No subdomain here on purpose: the Previous (ABC) platform predates merchant-specific + // subdomains and is exempt, so this also covers that exemption. final CheckoutApi defaultCheckoutApi = CheckoutSdk.builder().previous().staticKeys() .publicKey(VALID_PREVIOUS_PK) .secretKey(VALID_PREVIOUS_SK) @@ -69,6 +71,7 @@ void shouldCreateSdk() { .publicKey(VALID_DEFAULT_PK) .secretKey(VALID_DEFAULT_SK) .environment(Environment.SANDBOX) + .environmentSubdomain("1234doma") .build(); assertNotNull(checkoutApi); diff --git a/src/test/java/com/checkout/accounts/AccountsPayoutSchedulesIT.java b/src/test/java/com/checkout/accounts/AccountsPayoutSchedulesIT.java index 5af2d7004..84e9a65c1 100644 --- a/src/test/java/com/checkout/accounts/AccountsPayoutSchedulesIT.java +++ b/src/test/java/com/checkout/accounts/AccountsPayoutSchedulesIT.java @@ -193,6 +193,7 @@ private void validateScheduleResponseBase(final GetScheduleResponse response) { assertNotNull(schedule.getRecurrence().getFrequency()); } + @SuppressWarnings("deprecation") private CheckoutApi getPayoutSchedulesCheckoutApi() { return CheckoutSdk.builder() .oAuth() @@ -201,6 +202,9 @@ private CheckoutApi getPayoutSchedulesCheckoutApi() { requireNonNull(System.getenv("CHECKOUT_DEFAULT_OAUTH_PAYOUT_SCHEDULE_CLIENT_SECRET"))) .scopes(OAuthScope.MARKETPLACE) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); } diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index 61ec0fd1f..433385218 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -793,6 +793,7 @@ private IdResponse uploadFile() throws URISyntaxException { return fileResponse; } + @SuppressWarnings("deprecation") private CheckoutApi getAccountsCheckoutApi() { return CheckoutSdk.builder() .oAuth() @@ -801,6 +802,9 @@ private CheckoutApi getAccountsCheckoutApi() { requireNonNull(System.getenv("CHECKOUT_DEFAULT_OAUTH_ACCOUNTS_CLIENT_SECRET"))) .scopes(OAuthScope.ACCOUNTS) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); } diff --git a/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java b/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java index 9b4ac598d..96b8cdb94 100644 --- a/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java +++ b/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java @@ -33,6 +33,7 @@ public BaseIssuingTestIT() { issuingApi = getIssuingCheckoutApi(); } + @SuppressWarnings("deprecation") private CheckoutApi getIssuingCheckoutApi() { return CheckoutSdk.builder() .oAuth() @@ -43,6 +44,9 @@ private CheckoutApi getIssuingCheckoutApi() { OAuthScope.ISSUING_CONTROLS_READ, OAuthScope.ISSUING_CONTROLS_WRITE, OAuthScope.ISSUING_TRANSACTIONS_READ, OAuthScope.ISSUING_TRANSACTIONS_WRITE) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); } diff --git a/src/test/java/com/checkout/metadata/CardMetadataIT.java b/src/test/java/com/checkout/metadata/CardMetadataIT.java index a780687bf..c40b6f17e 100644 --- a/src/test/java/com/checkout/metadata/CardMetadataIT.java +++ b/src/test/java/com/checkout/metadata/CardMetadataIT.java @@ -160,10 +160,13 @@ void shouldRequestCardMetadataForTokenSync() { // ─── Helpers ──────────────────────────────────────────────────────────── private CheckoutApiImpl createStaticKeyApi() { + // Static keys never call the token endpoint, so this client runs against the real + // merchant subdomain: only the sandbox OAuth clients lack provisioning. return CheckoutSdk.builder().staticKeys() .publicKey(System.getenv("CHECKOUT_DEFAULT_PUBLIC_KEY")) .secretKey(System.getenv("CHECKOUT_DEFAULT_SECRET_KEY")) .environment(Environment.SANDBOX) + .environmentSubdomain(System.getenv("CHECKOUT_MERCHANT_SUBDOMAIN")) .build(); } diff --git a/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java b/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java index b7507f148..df26a9bde 100644 --- a/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java +++ b/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java @@ -933,6 +933,7 @@ private ProductRequest createTamaraProduct() { } // API builders + @SuppressWarnings("deprecation") private CheckoutApi createPreviewApi() { return CheckoutSdk.builder() .oAuth() @@ -940,6 +941,9 @@ private CheckoutApi createPreviewApi() { requireNonNull(System.getenv("CHECKOUT_PREVIEW_OAUTH_CLIENT_ID")), requireNonNull(System.getenv("CHECKOUT_PREVIEW_OAUTH_CLIENT_SECRET"))) .environment(Environment.SANDBOX) + // The sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so + // the token request would come back invalid_client. Opting out explicitly until they are. + .useLegacyDomain() .build(); } From d3c739fef43cc509e3dfcbedda2203363da14c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:10:24 +0200 Subject: [PATCH 08/18] Release 8.0.0 (#660) - The merchant-specific subdomain (environmentSubdomain) is now required. Set it, or call the deprecated useLegacyDomain() to keep using the shared checkout.com hosts. - An invalid subdomain now throws instead of being silently ignored. - Add optional amount to VoidRequest to support partial voids. - Add the ISV (SaaS seller) payout schedule fields balanceMinimum, carryForwardEnabled and paymentInstrumentId. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8541513d8..2802eb50e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=com.checkout -version=7.16.2 +version=8.0.0 project_name=Checkout SDK Java project_description=Checkout SDK for Java https://checkout.com From 15aca494bd85d9c8dcae7f3e8b384817f20b12b4 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 1 Sep 2026 10:03:41 +0200 Subject: [PATCH 09/18] Arch instrument data detailed --- .../update/UpdateAchAccountHolder.java | 51 +++++++++++ .../update/UpdateAchInstrumentData.java | 60 +++++++++++++ .../update/UpdateInstrumentAchRequest.java | 67 +-------------- .../AchInstrumentSerializationTest.java | 85 +++++++++++++++++++ 4 files changed, 200 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java diff --git a/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java b/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java new file mode 100644 index 000000000..dea75a4ed --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java @@ -0,0 +1,51 @@ +package com.checkout.instruments.update; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of an ACH instrument being updated. + * + *

The specification marks all four properties as required, but the descriptions qualify that: + * the names apply to an individual account holder and the company name to a corporate one. That is a + * conditional requirement the specification cannot express, so it is not enforced here. + * + *

This deliberately does not use {@code com.checkout.common.AccountHolder}, which is a superset + * carrying a phone number, identification, a date of birth and a tax ID that the ACH instrument + * schema does not declare. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java b/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java new file mode 100644 index 000000000..fc3d4911d --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java @@ -0,0 +1,60 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the ACH account being updated. + * + *

Every property is optional on update. The shape is identical to the store and retrieve + * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per operation. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Optional] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Optional] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Optional] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java index 8aa56e6a2..da4bc228c 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java @@ -1,15 +1,9 @@ package com.checkout.instruments.update; -import com.checkout.common.AccountHolder; -import com.checkout.common.CountryCode; -import com.checkout.common.Currency; import com.checkout.common.InstrumentType; -import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @@ -28,17 +22,17 @@ public final class UpdateInstrumentAchRequest extends UpdateInstrumentRequest { * The details of the bank account. * [Optional] */ - private AchInstrumentData instrumentData; + private UpdateAchInstrumentData instrumentData; /** * The account holder details. * [Optional] */ - private AccountHolder accountHolder; + private UpdateAchAccountHolder accountHolder; @Builder - private UpdateInstrumentAchRequest(final AchInstrumentData instrumentData, - final AccountHolder accountHolder) { + private UpdateInstrumentAchRequest(final UpdateAchInstrumentData instrumentData, + final UpdateAchAccountHolder accountHolder) { super(InstrumentType.ACH); this.instrumentData = instrumentData; this.accountHolder = accountHolder; @@ -48,57 +42,4 @@ public UpdateInstrumentAchRequest() { super(InstrumentType.ACH); } - /** - * The details of the ACH account being updated. - * - *

Every property is optional on update. The shape is identical to the store and retrieve - * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per - * operation. - */ - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static final class AchInstrumentData { - - /** - * The type of Direct Debit account. - * [Optional] - */ - private AchInstrumentAccountType accountType; - - /** - * The account number of the Direct Debit account. - * [Optional] - * min 4 characters - * max 17 characters - */ - private String accountNumber; - - /** - * The bank code of the Direct Debit account, also known as the routing number. - * [Optional] - * min 8 characters - * max 9 characters - */ - private String bankCode; - - /** - * The currency of the account. - * [Optional] - * min 3 characters - * max 3 characters - */ - private Currency currency; - - /** - * The country of the account. - * [Optional] - * min 2 characters - * max 2 characters - */ - private CountryCode country; - - } - } diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java index 3ab15a4cb..1ec7c3927 100644 --- a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -20,6 +20,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.checkout.instruments.update.UpdateAchAccountHolder; +import com.checkout.instruments.update.UpdateAchInstrumentData; +import com.checkout.instruments.update.UpdateInstrumentAchRequest; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Schema validation tests for the ACH variants of the instruments endpoints. @@ -273,4 +281,81 @@ private String retrieveResponseJson() { + "\"default\":true" + "}}"; } + // ------------------------------------------------------------------ + // UpdateAchInstrumentRequest - A2c + // The update variant predated this work. Its instrument data was a nested class inside the + // request and its account holder reused the shared com.checkout.common.AccountHolder, a superset + // of the four properties the schema declares. + // ------------------------------------------------------------------ + + @Test + void shouldRoundTripEveryUpdateRequestProperty() { + final UpdateInstrumentAchRequest request = UpdateInstrumentAchRequest.builder() + .instrumentData(UpdateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(UpdateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.CORPORATE) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"account_type\":\"savings\"")); + assertTrue(json.contains("\"account_number\":\"4099999992\"")); + assertTrue(json.contains("\"bank_code\":\"211370545\"")); + assertTrue(json.contains("\"currency\":\"USD\"")); + assertTrue(json.contains("\"country\":\"US\"")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"company_name\":\"Smith Enterprises\"")); + assertTrue(json.contains("\"type\":\"corporate\"")); + + final UpdateInstrumentAchRequest back = + serializer.fromJson(json, UpdateInstrumentAchRequest.class); + + assertEquals(AchInstrumentAccountType.SAVINGS, back.getInstrumentData().getAccountType()); + assertEquals("4099999992", back.getInstrumentData().getAccountNumber()); + assertEquals("211370545", back.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, back.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, back.getInstrumentData().getCountry()); + assertEquals("John", back.getAccountHolder().getFirstName()); + assertEquals("Smith", back.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", back.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, back.getAccountHolder().getType()); + } + + @Test + void shouldNotExposeSharedAccountHolderFieldsOnTheAchUpdateVariant() { + // UpdateAchInstrumentRequest.account_holder declares exactly four properties. The shared + // com.checkout.common.AccountHolder carries a phone number, identification, a date of birth + // and a tax ID that this schema does not declare. + final Field[] fields = UpdateAchAccountHolder.class.getDeclaredFields(); + final Set names = new HashSet<>(); + for (final Field field : fields) { + if (!field.isSynthetic()) { + names.add(field.getName()); + } + } + + assertEquals(new HashSet<>(asList("firstName", "lastName", "companyName", "type")), names); + } + + @Test + void shouldModelTheAchUpdateInstrumentDataInItsOwnFile() { + // The instrument data used to be a nested class inside UpdateInstrumentAchRequest, which + // broke the one-type-per-file rule. + assertNull(UpdateAchInstrumentData.class.getEnclosingClass()); + assertThrows(ClassNotFoundException.class, () -> Class.forName( + "com.checkout.instruments.update.UpdateInstrumentAchRequest$AchInstrumentData")); + } + } From 08be324d3ec4e4d19050d543892bf94edeab195a Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 27 Aug 2026 18:02:02 +0200 Subject: [PATCH 10/18] BACS Direct Debit notifications initial dev + tests --- README.md | 77 +++ .../com/checkout/AbstractCheckoutApmApi.java | 8 + .../java/com/checkout/CheckoutApmApi.java | 16 + .../java/com/checkout/GsonSerializer.java | 14 +- .../com/checkout/apm/bacs/BacsClient.java | 29 ++ .../com/checkout/apm/bacs/BacsClientImpl.java | 39 ++ .../apm/bacs/BacsNotificationRequest.java | 91 ++++ .../apm/bacs/BacsNotificationResponse.java | 20 + .../apm/bacs/BacsNotificationType.java | 13 + .../com/checkout/common/InstrumentType.java | 12 +- .../checkout/common/PaymentSourceType.java | 24 + .../checkout/instruments/BacsPaymentType.java | 22 + .../InstrumentAccountHolderType.java | 20 + .../create/CreateAchAccountHolder.java | 46 ++ .../create/CreateAchInstrumentData.java | 58 +++ .../create/CreateBacsAccountHolder.java | 38 ++ .../create/CreateBacsBillingAddress.java | 57 +++ .../create/CreateBacsInstrumentAccount.java | 24 + .../create/CreateBacsInstrumentData.java | 71 +++ .../create/CreateInstrumentAchRequest.java | 51 ++ .../create/CreateInstrumentAchResponse.java | 25 + .../create/CreateInstrumentBacsRequest.java | 59 +++ .../create/CreateInstrumentBacsResponse.java | 25 + .../create/CreateInstrumentResponse.java | 26 + .../instruments/get/GetAchAccountHolder.java | 43 ++ .../instruments/get/GetAchInstrumentData.java | 58 +++ .../get/GetAchInstrumentResponse.java | 58 +++ .../instruments/get/GetBacsAccountHolder.java | 50 ++ .../get/GetBacsBillingAddress.java | 53 +++ .../get/GetBacsInstrumentAccount.java | 29 ++ .../get/GetBacsInstrumentData.java | 93 ++++ .../get/GetBacsInstrumentResponse.java | 73 +++ .../get/GetBankAccountInstrumentResponse.java | 7 + .../instruments/get/GetCardAccountHolder.java | 43 ++ .../get/GetCardInstrumentResponse.java | 6 + .../get/GetInstrumentResponse.java | 25 +- .../instruments/get/GetSepaAccountHolder.java | 50 ++ .../get/GetSepaBillingAddress.java | 50 ++ .../get/GetSepaInstrumentData.java | 80 ++++ .../get/GetSepaInstrumentResponse.java | 44 +- .../update/UpdateBacsAccountHolder.java | 50 ++ .../update/UpdateBacsBillingAddress.java | 57 +++ .../update/UpdateBacsInstrumentData.java | 68 +++ .../update/UpdateInstrumentBacsRequest.java | 45 ++ .../update/UpdateInstrumentBacsResponse.java | 33 ++ .../request/source/apm/RequestSepaSource.java | 15 +- .../request/source/apm/RequestBacsSource.java | 37 ++ .../response/source/BacsResponseSource.java | 24 + .../com/checkout/CheckoutApiImplTest.java | 1 + .../checkout/apm/bacs/BacsClientImplTest.java | 86 ++++ .../apm/bacs/BacsNotificationTestIT.java | 42 ++ .../apm/bacs/BacsSerializationTest.java | 158 +++++++ .../InstrumentTypeSerializationTest.java | 36 ++ .../PaymentSourceTypeSerializationTest.java | 142 ++++++ .../AchInstrumentSerializationTest.java | 258 ++++++++++ .../instruments/AchInstrumentsTestIT.java | 54 +++ .../BacsInstrumentSerializationTest.java | 446 ++++++++++++++++++ .../instruments/BacsInstrumentsTestIT.java | 54 +++ .../InstrumentResponseDispatchTest.java | 96 ++++ .../RequestSepaSourceSerializationTest.java | 54 +++ .../RequestBacsSourceSerializationTest.java | 48 ++ .../BacsResponseSourceSerializationTest.java | 40 ++ 62 files changed, 3455 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/checkout/apm/bacs/BacsClient.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsClientImpl.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java create mode 100644 src/main/java/com/checkout/apm/bacs/BacsNotificationType.java create mode 100644 src/main/java/com/checkout/instruments/BacsPaymentType.java create mode 100644 src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java create mode 100644 src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java create mode 100644 src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java create mode 100644 src/main/java/com/checkout/payments/response/source/BacsResponseSource.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java create mode 100644 src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java create mode 100644 src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java create mode 100644 src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java create mode 100644 src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java create mode 100644 src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java create mode 100644 src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java create mode 100644 src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java create mode 100644 src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java create mode 100644 src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java diff --git a/README.md b/README.md index be4e0ebd7..2ce185db1 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,83 @@ public static void main(String[] args) { } ``` +### Bacs Direct Debit + +Send a pre-notification (advance notice) to a payer before collecting funds from their account. The +endpoint accepts the secret key only, so it is not available on an OAuth-only client. + +```java +import com.checkout.CheckoutApi; +import com.checkout.apm.bacs.BacsNotificationRequest; +import com.checkout.apm.bacs.BacsNotificationResponse; +import com.checkout.apm.bacs.BacsNotificationType; +import com.checkout.common.Currency; + +import java.time.LocalDate; + +final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") // optional + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") // optional + .build(); + +final CompletableFuture notification = + checkoutApi.bacsClient().sendNotification(request); +``` + +A Bacs Direct Debit instrument is stored, updated and retrieved through the instruments client. Note +that the Bacs `payment_type` values are capitalized (`Recurring`, `Regular`), unlike the SEPA ones, +so use `BacsPaymentType` and not `SepaPaymentType`: + +```java +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; + +final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") // 8 characters + .bankCode("040004") // the sort code, 6 characters + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .country(CountryCode.GB) + .build()) + .build()) + .build(); + +final CreateInstrumentBacsResponse created = + checkoutApi.instrumentsClient().create(request).get(); + +final GetBacsInstrumentResponse stored = + (GetBacsInstrumentResponse) checkoutApi.instrumentsClient().get(created.getId()).get(); +``` + +To take a payment against a stored Bacs instrument, use `RequestBacsSource`. The payment response +returns a `BacsResponseSource`. + ## Logging The SDK supports SLF4J as logger provider, you need to provide your configuration file through `resources` folder. diff --git a/src/main/java/com/checkout/AbstractCheckoutApmApi.java b/src/main/java/com/checkout/AbstractCheckoutApmApi.java index 39091e640..9d634ccf4 100644 --- a/src/main/java/com/checkout/AbstractCheckoutApmApi.java +++ b/src/main/java/com/checkout/AbstractCheckoutApmApi.java @@ -1,5 +1,7 @@ package com.checkout; +import com.checkout.apm.bacs.BacsClient; +import com.checkout.apm.bacs.BacsClientImpl; import com.checkout.apm.ideal.IdealClient; import com.checkout.apm.ideal.IdealClientImpl; import com.checkout.apm.previous.klarna.KlarnaClient; @@ -14,12 +16,14 @@ public abstract class AbstractCheckoutApmApi { protected final ApiClient apiClient; private final IdealClient idealClient; + private final BacsClient bacsClient; private final KlarnaClient klarnaClient; private final SepaClient sepaClient; protected AbstractCheckoutApmApi(final CheckoutConfiguration configuration) { this.apiClient = getBaseApiClient(configuration); this.idealClient = new IdealClientImpl(apiClient, configuration); + this.bacsClient = new BacsClientImpl(apiClient, configuration); this.klarnaClient = new KlarnaClientImpl(apiClient, configuration); this.sepaClient = new SepaClientImpl(apiClient, configuration); } @@ -28,6 +32,10 @@ public IdealClient idealClient() { return idealClient; } + public BacsClient bacsClient() { + return bacsClient; + } + public KlarnaClient klarnaClient() { return klarnaClient; } diff --git a/src/main/java/com/checkout/CheckoutApmApi.java b/src/main/java/com/checkout/CheckoutApmApi.java index 0e981d17e..c53d71eea 100644 --- a/src/main/java/com/checkout/CheckoutApmApi.java +++ b/src/main/java/com/checkout/CheckoutApmApi.java @@ -1,9 +1,25 @@ package com.checkout; +import com.checkout.apm.bacs.BacsClient; import com.checkout.apm.ideal.IdealClient; +/** + * The alternative payment method clients that the current platform exposes. + */ public interface CheckoutApmApi { + /** + * Retrieves iDEAL issuer information. + * + * @return the iDEAL client. + */ IdealClient idealClient(); + /** + * Sends Bacs Direct Debit pre-notifications. + * + * @return the Bacs Direct Debit client. + */ + BacsClient bacsClient(); + } diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index d192843ca..ed0c0e0eb 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -167,7 +167,8 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.response.source.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.response.source.AlternativePaymentSourceResponse.class) .registerSubtype(com.checkout.payments.response.source.CardResponseSource.class, identifier(PaymentSourceType.CARD)) .registerSubtype(com.checkout.payments.response.source.CurrencyAccountResponseSource.class, identifier(PaymentSourceType.CURRENCY_ACCOUNT)) - .registerSubtype(com.checkout.payments.response.source.PayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL))) + .registerSubtype(com.checkout.payments.response.source.PayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL)) + .registerSubtype(com.checkout.payments.response.source.BacsResponseSource.class, identifier(PaymentSourceType.BACS))) // Payment Contexts .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.response.source.contexts.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.response.source.contexts.AlternativePaymentSourceResponse.class) .registerSubtype(com.checkout.payments.response.source.contexts.PaymentContextsPayPalResponseSource.class, identifier(PaymentSourceType.PAYPAL)) @@ -186,16 +187,21 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentTokenResponse.class, identifier(InstrumentType.CARD)) - .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA))) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentBacsResponse.class, identifier(InstrumentType.BACS)) + .registerSubtype(com.checkout.instruments.create.CreateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.get.GetBankAccountInstrumentResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.get.GetCardInstrumentResponse.class, identifier(InstrumentType.CARD)) - .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA))) + .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA)) + .registerSubtype(com.checkout.instruments.get.GetBacsInstrumentResponse.class, identifier(InstrumentType.BACS)) + .registerSubtype(com.checkout.instruments.get.GetAchInstrumentResponse.class, identifier(InstrumentType.ACH))) .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentCardResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) - .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) + .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH)) + .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBacsResponse.class, identifier(InstrumentType.BACS))) // Workflows CS2 .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE) .registerSubtype(com.checkout.workflows.actions.response.WebhookWorkflowActionResponse.class, identifier(WorkflowActionType.WEBHOOK))) diff --git a/src/main/java/com/checkout/apm/bacs/BacsClient.java b/src/main/java/com/checkout/apm/bacs/BacsClient.java new file mode 100644 index 000000000..d76ff6db9 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsClient.java @@ -0,0 +1,29 @@ +package com.checkout.apm.bacs; + +import java.util.concurrent.CompletableFuture; + +/** + * Bacs Direct Debit client. + */ +public interface BacsClient { + + /** + * Sends a Bacs Direct Debit pre-notification (advance notice) to a payer ahead of collecting + * funds from their account. + * + * @param bacsNotificationRequest the pre-notification details. + * @return a {@link CompletableFuture} that resolves to the created notification event. + */ + CompletableFuture sendNotification(BacsNotificationRequest bacsNotificationRequest); + + // Synchronous methods + + /** + * Synchronous variant of {@link #sendNotification(BacsNotificationRequest)}. + * + * @param bacsNotificationRequest the pre-notification details. + * @return the created notification event. + */ + BacsNotificationResponse sendNotificationSync(BacsNotificationRequest bacsNotificationRequest); + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java b/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java new file mode 100644 index 000000000..2e30af33c --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsClientImpl.java @@ -0,0 +1,39 @@ +package com.checkout.apm.bacs; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorizationType; + +import java.util.concurrent.CompletableFuture; + +import static com.checkout.common.CheckoutUtils.validateParams; + +public class BacsClientImpl extends AbstractClient implements BacsClient { + + private static final String APMS = "apms"; + private static final String BACS_NOTIFICATIONS = "bacs/notifications"; + private static final String BACS_NOTIFICATION_REQUEST = "bacsNotificationRequest"; + + /** + * The operation declares the secret key as its only security scheme, so this client does not + * use the secret-key-or-OAuth variant that the instruments endpoints allow. + */ + public BacsClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY); + } + + @Override + public CompletableFuture sendNotification(final BacsNotificationRequest bacsNotificationRequest) { + validateParams(BACS_NOTIFICATION_REQUEST, bacsNotificationRequest); + return apiClient.postAsync(buildPath(APMS, BACS_NOTIFICATIONS), sdkAuthorization(), BacsNotificationResponse.class, bacsNotificationRequest, null); + } + + // Synchronous methods + @Override + public BacsNotificationResponse sendNotificationSync(final BacsNotificationRequest bacsNotificationRequest) { + validateParams(BACS_NOTIFICATION_REQUEST, bacsNotificationRequest); + return apiClient.post(buildPath(APMS, BACS_NOTIFICATIONS), sdkAuthorization(), BacsNotificationResponse.class, bacsNotificationRequest, null); + } + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java new file mode 100644 index 000000000..e4cfa30e5 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationRequest.java @@ -0,0 +1,91 @@ +package com.checkout.apm.bacs; + +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * Bacs Direct Debit notification request. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class BacsNotificationRequest { + + /** + * The ID of the Bacs Direct Debit instrument to notify against. + * [Required] + * ^(src)_(\w{26})$ + */ + private String sourceId; + + /** + * The type of pre-notification being sent to the payer. + * [Required] + */ + private BacsNotificationType notificationType; + + /** + * The date the funds will be collected from the payer's account, in the format yyyy-MM-dd. + * [Required] + * Format: yyyy-MM-dd + */ + private LocalDate collectionDate; + + /** + * The amount to be collected, in the currency's minor unit. + * [Required] + * Format: int64 + * min 1 + */ + private Long amount; + + /** + * The three-letter ISO 4217 currency code of the collection. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * A reference you can use to identify the collection. + * [Optional] + * max 50 characters + */ + private String reference; + + /** + * The email address of the payer that the pre-notification is sent to. + * [Required] + * Format: email + */ + private String customerEmail; + + /** + * The billing descriptor that appears on the payer's bank statement. + * [Required] + * max 25 characters + */ + private String billingDescriptor; + + /** + * The support email address included in the pre-notification. + * [Required] + * Format: email + */ + private String supportEmail; + + /** + * The support phone number included in the pre-notification, in E.164 format. The + * specification declares no pattern for this property. + * [Optional] + */ + private String supportPhone; + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java new file mode 100644 index 000000000..dd61992d8 --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationResponse.java @@ -0,0 +1,20 @@ +package com.checkout.apm.bacs; + +import com.checkout.HttpMetadata; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Bacs Direct Debit pre-notification response. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class BacsNotificationResponse extends HttpMetadata { + + /** + * The unique identifier of the notification event. + * [Required] + */ + private String eventId; + +} diff --git a/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java b/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java new file mode 100644 index 000000000..5e6a9dd4d --- /dev/null +++ b/src/main/java/com/checkout/apm/bacs/BacsNotificationType.java @@ -0,0 +1,13 @@ +package com.checkout.apm.bacs; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of pre-notification being sent to the payer. + */ +public enum BacsNotificationType { + + @SerializedName("advance_notice") + ADVANCE_NOTICE + +} diff --git a/src/main/java/com/checkout/common/InstrumentType.java b/src/main/java/com/checkout/common/InstrumentType.java index e6b606e6a..687dabf94 100644 --- a/src/main/java/com/checkout/common/InstrumentType.java +++ b/src/main/java/com/checkout/common/InstrumentType.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of payment instrument. + */ public enum InstrumentType { @SerializedName("bank_account") @@ -13,6 +16,10 @@ public enum InstrumentType { @SerializedName("card") CARD, + /** + * Retained for the previous-platform instruments API. Not a value of the current platform's + * instrument type. + */ @SerializedName("card_token") CARD_TOKEN, @@ -20,6 +27,9 @@ public enum InstrumentType { SEPA, @SerializedName("ach") - ACH + ACH, + + @SerializedName("bacs") + BACS } diff --git a/src/main/java/com/checkout/common/PaymentSourceType.java b/src/main/java/com/checkout/common/PaymentSourceType.java index b359fc233..04565c94d 100644 --- a/src/main/java/com/checkout/common/PaymentSourceType.java +++ b/src/main/java/com/checkout/common/PaymentSourceType.java @@ -2,6 +2,13 @@ import com.google.gson.annotations.SerializedName; +/** + * The payment source type. + * + *

This enum is the union of the source types accepted on payment requests and returned on + * payment responses, on both the current and the previous platform, so it also carries + * previous-platform values that the current API specification no longer declares. + */ public enum PaymentSourceType { @SerializedName("ach") @@ -20,6 +27,8 @@ public enum PaymentSourceType { ALMA, @SerializedName("applepay") APPLEPAY, + @SerializedName("bacs") + BACS, @SerializedName("bancontact") BANCONTACT, @SerializedName("bank_account") @@ -98,6 +107,21 @@ public enum PaymentSourceType { QPAY, @SerializedName("rapipago") RAPIPAGO, + /** + * Shares the wire value of {@link #ID}, because the previous platform references a stored SEPA + * mandate through the generic "id" source. Nothing in the SDK passes this constant any more: + * the previous-platform source now uses {@link #ID} directly, which is how the .NET SDK models + * the same call. + * + *

Gson resolves an incoming "id" to whichever of the two constants is declared last, which + * is this one, so deserializing a previous-platform source yields SEPA rather than ID. Removing + * this constant would correct that but is a breaking change, so it is deprecated instead. Do + * not reorder the two constants: that would silently change what "id" deserializes to. + * + * @deprecated use {@link #ID} for the previous platform and {@link #SEPAV4} for the current + * platform, where the wire value is "sepa". + */ + @Deprecated @SerializedName("id") SEPA, @SerializedName("sepa") diff --git a/src/main/java/com/checkout/instruments/BacsPaymentType.java b/src/main/java/com/checkout/instruments/BacsPaymentType.java new file mode 100644 index 000000000..b254f8d82 --- /dev/null +++ b/src/main/java/com/checkout/instruments/BacsPaymentType.java @@ -0,0 +1,22 @@ +package com.checkout.instruments; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of payment for a Bacs Direct Debit instrument. + * + *

The wire values are capitalized, and the specification allows these two values only. The + * equivalent SEPA field is lowercase in the specification, so do not share one type between the + * two: reusing {@link com.checkout.instruments.update.SepaPaymentType} sends a value the API + * rejects. Do not replace this enum with {@link com.checkout.payments.PaymentType} either, because + * that enum also accepts MOTO, Installment, PayLater and Unscheduled, which Bacs does not allow. + */ +public enum BacsPaymentType { + + @SerializedName("Recurring") + RECURRING, + + @SerializedName("Regular") + REGULAR + +} diff --git a/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java b/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java new file mode 100644 index 000000000..a69abbdfa --- /dev/null +++ b/src/main/java/com/checkout/instruments/InstrumentAccountHolderType.java @@ -0,0 +1,20 @@ +package com.checkout.instruments; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of account holder of a stored payment instrument. + * + *

Shared by the Bacs Direct Debit, SEPA and ACH instrument variants, which all declare the same + * two values. This is deliberately not {@link com.checkout.common.AccountHolderType}, which also + * declares {@code government} and is therefore wider than the instrument schemas allow. + */ +public enum InstrumentAccountHolderType { + + @SerializedName("individual") + INDIVIDUAL, + + @SerializedName("corporate") + CORPORATE + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java b/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java new file mode 100644 index 000000000..c2618f40d --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateAchAccountHolder.java @@ -0,0 +1,46 @@ +package com.checkout.instruments.create; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of an ACH instrument being stored. + * + *

The specification marks all four properties as required, but the descriptions qualify that: + * the names apply to an individual account holder and the company name to a corporate one. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java b/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java new file mode 100644 index 000000000..58575caf1 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateAchInstrumentData.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.AchInstrumentAccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the ACH account being stored. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Required] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Required] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Required] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java b/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java new file mode 100644 index 000000000..302eeccd8 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsAccountHolder.java @@ -0,0 +1,38 @@ +package com.checkout.instruments.create; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a Bacs Direct Debit instrument being stored. + * + *

The store shape declares three properties only. The update and retrieve shapes add a company + * name and an account holder type, so they use their own types. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The billing address of the account holder. + * [Required] + */ + private CreateBacsBillingAddress billingAddress; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java b/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java new file mode 100644 index 000000000..0cc9d24e0 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsBillingAddress.java @@ -0,0 +1,57 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a Bacs Direct Debit instrument being stored. + * + *

The length constraints differ from the update and retrieve variants of the same address, so + * this type is deliberately not shared with them. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + * max 200 characters + */ + private String addressLine1; + + /** + * The street number. If no number, pass "w/n". + * [Optional] + * max 10 characters + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + * max 35 characters + */ + private String city; + + /** + * The address zip/postal code. + * [Optional] + * max 16 characters + */ + private String zip; + + /** + * The two-letter ISO country code of the address. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java new file mode 100644 index 000000000..3a957d055 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentAccount.java @@ -0,0 +1,24 @@ +package com.checkout.instruments.create; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account configuration for a Bacs Direct Debit instrument being stored. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsInstrumentAccount { + + /** + * The ID of the processing channel to associate with the instrument. + * [Required] + * ^(pc)_(\w{26})$ + */ + private String processingChannelId; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java new file mode 100644 index 000000000..b810fdfc0 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateBacsInstrumentData.java @@ -0,0 +1,71 @@ +package com.checkout.instruments.create; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the Bacs Direct Debit account being stored. + * + *

This is not the SEPA instrument data shape: the account number is a fixed-length UK account + * number rather than an IBAN, the sort code arrives in {@code bank_code}, and there is no mandate + * type, mandate ID or date of signature. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class CreateBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Required] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Required] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. Recurring or Regular. + * [Required] + */ + private BacsPaymentType paymentType; + + /** + * Indicates whether the Bacs instrument is created when account validation returns a partial + * match. When true, the instrument is created on a partial match; when false, instrument + * creation fails on a partial match. + * [Optional] + * Default: false + */ + private Boolean allowPartialMatch; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java new file mode 100644 index 000000000..b818ede50 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchRequest.java @@ -0,0 +1,51 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Store ACH bank account details. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentAchRequest extends CreateInstrumentRequest { + + /** + * The details of the bank account. + * [Required] + */ + private CreateAchInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private CreateAchAccountHolder accountHolder; + + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ + private CreateCustomerInstrumentRequest customer; + + @Builder + private CreateInstrumentAchRequest(final CreateAchInstrumentData instrumentData, + final CreateAchAccountHolder accountHolder, + final CreateCustomerInstrumentRequest customer) { + super(InstrumentType.ACH); + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + this.customer = customer; + } + + public CreateInstrumentAchRequest() { + super(InstrumentType.ACH); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java new file mode 100644 index 000000000..32b5971ec --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentAchResponse.java @@ -0,0 +1,25 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Store ACH bank account instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentAchResponse extends CreateInstrumentResponse { + + /** + * The type of instrument. + * [Required] + */ + private final InstrumentType type = InstrumentType.ACH; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java new file mode 100644 index 000000000..af79fcb60 --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsRequest.java @@ -0,0 +1,59 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Store Bacs Direct Debit account details. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentBacsRequest extends CreateInstrumentRequest { + + /** + * The account configuration for the instrument. + * [Required] + */ + private CreateBacsInstrumentAccount account; + + /** + * The details of the Bacs Direct Debit account. + * [Required] + */ + private CreateBacsInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private CreateBacsAccountHolder accountHolder; + + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ + private CreateCustomerInstrumentRequest customer; + + @Builder + private CreateInstrumentBacsRequest(final CreateBacsInstrumentAccount account, + final CreateBacsInstrumentData instrumentData, + final CreateBacsAccountHolder accountHolder, + final CreateCustomerInstrumentRequest customer) { + super(InstrumentType.BACS); + this.account = account; + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + this.customer = customer; + } + + public CreateInstrumentBacsRequest() { + super(InstrumentType.BACS); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java new file mode 100644 index 000000000..c52d5241b --- /dev/null +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBacsResponse.java @@ -0,0 +1,25 @@ +package com.checkout.instruments.create; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Store Bacs Direct Debit account instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class CreateInstrumentBacsResponse extends CreateInstrumentResponse { + + /** + * The type of instrument. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + +} diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java index c9a48df39..e23af6050 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java @@ -5,14 +5,40 @@ import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The shared properties of a stored payment instrument. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class CreateInstrumentResponse extends HttpMetadata { + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ protected String id; + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ protected String fingerprint; + /** + * The customer that the instrument is associated with. + * + *

Only the token and bank_account variants declare this property. The sepa, ach and bacs + * store responses do not, so on those three it is always null and does not belong here. + * [Optional] + * + * @deprecated declared on the wrong level. This will move to + * {@link CreateInstrumentTokenResponse} and {@link CreateInstrumentBankAccountResponse}, the + * only two variants the specification declares it on, in the next major version. Read it from + * the concrete response type rather than from this base class. + */ + @Deprecated protected CustomerResponse customer; } diff --git a/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java new file mode 100644 index 000000000..e149f4d81 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchAccountHolder.java @@ -0,0 +1,43 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a stored ACH instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java new file mode 100644 index 000000000..c297e147e --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchInstrumentData.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.AchInstrumentAccountType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of a stored ACH bank account. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Required] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Required] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Required] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java new file mode 100644 index 000000000..bf9e0ee98 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetAchInstrumentResponse.java @@ -0,0 +1,58 @@ +package com.checkout.instruments.get; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; + +/** + * ACH instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class GetAchInstrumentResponse extends GetInstrumentResponse { + + /** + * The underlying instrument type. + * [Required] + */ + private final InstrumentType type = InstrumentType.ACH; + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant createdOn; + + /** + * The date and time the instrument was last modified. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant modifiedOn; + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ + private String vaultId; + + /** + * The details of the bank account. + * [Required] + */ + private GetAchInstrumentData instrumentData; + + /** + * The account holder details. + * [Required] + */ + private GetAchAccountHolder accountHolder; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java new file mode 100644 index 000000000..4639d25b7 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder's details of a stored Bacs Direct Debit instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Required] + */ + private GetBacsBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java b/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java new file mode 100644 index 000000000..b5b87053e --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsBillingAddress.java @@ -0,0 +1,53 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a stored Bacs Direct Debit instrument. + * + *

The retrieve variant of this address declares no length limits other than on the country, so + * it is deliberately not shared with the store and update variants. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + */ + private String addressLine1; + + /** + * The second line of the address. + * [Optional] + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + */ + private String city; + + /** + * The address ZIP or postal code. + * [Optional] + */ + private String zip; + + /** + * The address country. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java new file mode 100644 index 000000000..21253ce97 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentAccount.java @@ -0,0 +1,29 @@ +package com.checkout.instruments.get; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account configuration for a stored Bacs Direct Debit instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsInstrumentAccount { + + /** + * The ID of the client associated with the instrument. + * [Optional] + */ + private String clientId; + + /** + * The ID of the processing channel associated with the instrument. + * [Optional] + */ + private String processingChannelId; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java new file mode 100644 index 000000000..96eaa8dd8 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentData.java @@ -0,0 +1,93 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of a stored Bacs Direct Debit account. + * + *

The retrieve shape adds four read-back properties that the store and update shapes do not + * declare: status, matchStatus, description and mandateId. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Required] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Required] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. + * [Required] + */ + private BacsPaymentType paymentType; + + /** + * Whether vault accepted a partial match when looking up the Bacs instrument for the supplied + * account details. + * [Optional] + */ + private Boolean allowPartialMatch; + + /** + * The validation status of the account. The specification declares no enum for this property. + * [Optional] + */ + private String status; + + /** + * The result of matching the account holder name against the account owner. The specification + * declares no enum for this property. + * [Optional] + */ + private String matchStatus; + + /** + * A human-readable description of the validation result. + * [Optional] + */ + private String description; + + /** + * The identifier of the Bacs Direct Debit mandate. + * [Optional] + */ + private String mandateId; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java new file mode 100644 index 000000000..3a0529706 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetBacsInstrumentResponse.java @@ -0,0 +1,73 @@ +package com.checkout.instruments.get; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * Bacs Direct Debit instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class GetBacsInstrumentResponse extends GetInstrumentResponse { + + /** + * The underlying instrument type. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ + private Instant createdOn; + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ + private String vaultId; + + /** + * The date and time the instrument was last modified. + * [Optional] + * Format: date-time (RFC 3339) + */ + private Instant modifiedOn; + + /** + * The account configuration for the instrument. + * [Optional] + */ + private GetBacsInstrumentAccount account; + + /** + * The list of validations performed on the instrument. The API publishes no item schema for + * this array, so each entry is exposed as an untyped map. + * [Optional] + */ + private List> validations; + + /** + * The details of the Bacs Direct Debit account. + * [Optional] + */ + private GetBacsInstrumentData instrumentData; + + /** + * The account holder's details. + * [Optional] + */ + private GetBacsAccountHolder accountHolder; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java index 298a57442..a91428ed5 100644 --- a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java @@ -1,5 +1,6 @@ package com.checkout.instruments.get; +import com.checkout.common.AccountHolder; import com.checkout.common.AccountType; import com.checkout.common.BankDetails; import com.checkout.common.CountryCode; @@ -36,4 +37,10 @@ public final class GetBankAccountInstrumentResponse extends GetInstrumentRespons private BankDetails bank; + /** + * The account holder details. + * [Optional] + */ + private AccountHolder accountHolder; + } diff --git a/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java new file mode 100644 index 000000000..149b8529b --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetCardAccountHolder.java @@ -0,0 +1,43 @@ +package com.checkout.instruments.get; + +import com.checkout.common.Address; +import com.checkout.common.Phone; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a stored card instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetCardAccountHolder { + + /** + * The first name of the account holder. + * [Optional] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Optional] + */ + private String lastName; + + /** + * The billing address of the account holder. + * [Optional] + */ + private Address billingAddress; + + /** + * The phone number of the account holder. + * [Optional] + */ + private Phone phone; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java index c4e2f9443..0ac264259 100644 --- a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java @@ -119,4 +119,10 @@ public final class GetCardInstrumentResponse extends GetInstrumentResponse { */ private Boolean regulatedIndicator; + /** + * The account holder details. + * [Optional] + */ + private GetCardAccountHolder accountHolder; + } diff --git a/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java index 5122ee0ae..ce72087a2 100644 --- a/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetInstrumentResponse.java @@ -1,20 +1,39 @@ package com.checkout.instruments.get; import com.checkout.HttpMetadata; -import com.checkout.common.AccountHolder; import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The shared properties of a retrieved payment instrument. + * + *

The account holder is declared by each concrete variant rather than here, because the + * specification gives every variant a different account holder shape: the bank account variant + * refers to the shared AccountHolder, while the card, SEPA and Bacs Direct Debit variants each + * declare their own inline shape. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class GetInstrumentResponse extends HttpMetadata { + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ protected String id; + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ protected String fingerprint; + /** + * The customer that the instrument is associated with. + * [Optional] + */ protected InstrumentCustomerResponse customer; - protected AccountHolder accountHolder; - } diff --git a/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java b/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java new file mode 100644 index 000000000..55d12ce03 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder's details of a stored SEPA instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaAccountHolder { + + /** + * The first name of the account holder. + * [Required] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Required] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Required] + */ + private GetSepaBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java b/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java new file mode 100644 index 000000000..94c4d5c73 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaBillingAddress.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a stored SEPA instrument. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaBillingAddress { + + /** + * The first line of the address. + * [Required] + */ + private String addressLine1; + + /** + * The second line of the address. + * [Required] + */ + private String addressLine2; + + /** + * The address city. + * [Required] + */ + private String city; + + /** + * The address ZIP or postal code. + * [Required] + */ + private String zip; + + /** + * The address country. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java new file mode 100644 index 000000000..62caaef82 --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java @@ -0,0 +1,80 @@ +package com.checkout.instruments.get; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.payments.request.source.apm.MandateType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +/** + * The details of a stored SEPA mandate. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs + * Direct Debit field is capitalized, so the two must not share a type. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class GetSepaInstrumentData { + + /** + * The type of mandate. + * [Optional] + * Enum: "Core" "B2B" + */ + private MandateType type; + + /** + * The International Bank Account Number (IBAN) of the account. + * [Required] + * min 15 characters + * max 34 characters + */ + private String accountNumber; + + /** + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. + * [Required] + */ + private SepaPaymentType paymentType; + + /** + * The mandate ID. If this value was not provided when the instrument was created, it may take + * up to five seconds for the generated mandate ID to be available. + * [Required] + * min 1 characters + * max 35 characters + */ + private String mandateId; + + /** + * The date the mandate was signed. If the mandate ID was not provided when the instrument was + * created, it may take up to five seconds for the generated date of signature to be available. + * [Required] + * Format: yyyy-MM-dd + */ + private LocalDate dateOfSignature; + +} diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java index b7c501a8e..da6638556 100644 --- a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentResponse.java @@ -1,26 +1,58 @@ package com.checkout.instruments.get; import com.checkout.common.InstrumentType; -import com.checkout.instruments.create.InstrumentData; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import java.time.Instant; +/** + * SEPA instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetSepaInstrumentResponse extends GetInstrumentResponse { + /** + * The underlying instrument type. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; - + + /** + * The date and time the instrument was created. + * [Required] + * Format: date-time (RFC 3339) + */ private Instant createdOn; - + + /** + * The date and time the instrument was last modified. + * [Required] + * Format: date-time (RFC 3339) + */ private Instant modifiedOn; - + + /** + * The Vault ID currently attached to the instrument. + * [Required] + */ private String vaultId; - - private InstrumentData instrumentData; + + /** + * The details of the SEPA mandate. + * [Optional] + */ + private GetSepaInstrumentData instrumentData; + + /** + * The account holder's details. + * [Optional] + */ + private GetSepaAccountHolder accountHolder; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java b/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java new file mode 100644 index 000000000..c245257fe --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsAccountHolder.java @@ -0,0 +1,50 @@ +package com.checkout.instruments.update; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of a Bacs Direct Debit instrument being updated. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsAccountHolder { + + /** + * The first name of the account holder. + * [Optional] + */ + private String firstName; + + /** + * The last name of the account holder. + * [Optional] + */ + private String lastName; + + /** + * The legal name of a registered company that holds the account. + * [Optional] + * max 50 characters + */ + private String companyName; + + /** + * The billing address of the account holder. + * [Optional] + */ + private UpdateBacsBillingAddress billingAddress; + + /** + * The type of account holder. + * [Optional] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java b/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java new file mode 100644 index 000000000..480ef12c6 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsBillingAddress.java @@ -0,0 +1,57 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing address of the account holder of a Bacs Direct Debit instrument being updated. + * + *

The city and zip limits are wider here than on the store request, so this type is + * deliberately not shared with it. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsBillingAddress { + + /** + * The first line of the address. + * [Optional] + * max 200 characters + */ + private String addressLine1; + + /** + * The street number. If no number, pass "w/n". + * [Optional] + * max 10 characters + */ + private String addressLine2; + + /** + * The address city. + * [Optional] + * max 50 characters + */ + private String city; + + /** + * The address zip/postal code. + * [Optional] + * max 50 characters + */ + private String zip; + + /** + * The two-letter ISO country code of the address. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java b/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java new file mode 100644 index 000000000..460f017a6 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateBacsInstrumentData.java @@ -0,0 +1,68 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.BacsPaymentType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the Bacs Direct Debit account being updated. + * + *

Every property is optional on update, and allowPartialMatch changes meaning: on the store + * request it instructs the vault, and here it reads back what the vault accepted. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateBacsInstrumentData { + + /** + * The account number of the Bacs Direct Debit account. + * [Optional] + * min 8 characters + * max 8 characters + */ + private String accountNumber; + + /** + * The sort code of the Bacs Direct Debit account. + * [Optional] + * min 6 characters + * max 6 characters + */ + private String bankCode; + + /** + * The country of the account, as an ISO 3166-1 alpha-2 code. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The type of payment. Recurring or Regular. + * [Optional] + */ + private BacsPaymentType paymentType; + + /** + * Whether vault accepted a partial match when looking up the Bacs instrument for the supplied + * account details. + * [Optional] + */ + private Boolean allowPartialMatch; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java new file mode 100644 index 000000000..c3a7a6273 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsRequest.java @@ -0,0 +1,45 @@ +package com.checkout.instruments.update; + +import com.checkout.common.InstrumentType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Update Bacs Direct Debit account details. + * + *

Nothing in this request is required by the specification. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class UpdateInstrumentBacsRequest extends UpdateInstrumentRequest { + + /** + * The details of the Bacs Direct Debit account. + * [Optional] + */ + private UpdateBacsInstrumentData instrumentData; + + /** + * The account holder details. + * [Optional] + */ + private UpdateBacsAccountHolder accountHolder; + + @Builder + private UpdateInstrumentBacsRequest(final UpdateBacsInstrumentData instrumentData, + final UpdateBacsAccountHolder accountHolder) { + super(InstrumentType.BACS); + this.instrumentData = instrumentData; + this.accountHolder = accountHolder; + } + + public UpdateInstrumentBacsRequest() { + super(InstrumentType.BACS); + } + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java new file mode 100644 index 000000000..76fbf0af4 --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java @@ -0,0 +1,33 @@ +package com.checkout.instruments.update; + +import com.checkout.common.InstrumentType; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Update Bacs Direct Debit account instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The id is declared here + * because the shared update response does not carry it. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class UpdateInstrumentBacsResponse extends UpdateInstrumentResponse { + + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ + private final InstrumentType type = InstrumentType.BACS; + + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + +} diff --git a/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java b/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java index c0ea1e845..eb8767b65 100644 --- a/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java +++ b/src/main/java/com/checkout/payments/previous/request/source/apm/RequestSepaSource.java @@ -8,22 +8,33 @@ import lombok.Setter; import lombok.ToString; +/** + * A payment against a stored SEPA mandate on the previous platform. + * + *

The previous platform references the stored mandate through the generic "id" source, so the + * type on the wire is "id" and not "sepa". Use {@link com.checkout.payments.request.source.apm.RequestSepaSource} + * for the current platform, where the type is "sepa". + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestSepaSource extends AbstractRequestSource { + /** + * The ID of the stored SEPA mandate. + * [Required] + */ private String id; @Builder private RequestSepaSource(final String id) { - super(PaymentSourceType.SEPA); + super(PaymentSourceType.ID); this.id = id; } public RequestSepaSource() { - super(PaymentSourceType.SEPA); + super(PaymentSourceType.ID); } } diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java new file mode 100644 index 000000000..34f86f5e2 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBacsSource.java @@ -0,0 +1,37 @@ +package com.checkout.payments.request.source.apm; + +import com.checkout.common.PaymentSourceType; +import com.checkout.payments.request.source.AbstractRequestSource; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Bacs Direct Debit source. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class RequestBacsSource extends AbstractRequestSource { + + /** + * The Bacs Direct Debit instrument ID. + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + @Builder + private RequestBacsSource(final String id) { + super(PaymentSourceType.BACS); + this.id = id; + } + + public RequestBacsSource() { + super(PaymentSourceType.BACS); + } + +} diff --git a/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java b/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java new file mode 100644 index 000000000..78f210349 --- /dev/null +++ b/src/main/java/com/checkout/payments/response/source/BacsResponseSource.java @@ -0,0 +1,24 @@ +package com.checkout.payments.response.source; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import static com.checkout.common.PaymentSourceType.BACS; + +/** + * Bacs Direct Debit source. + * + *

The specification declares a type and an id only, both of which the shared + * {@link AbstractResponseSource} already carries. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BacsResponseSource extends AbstractResponseSource implements ResponseSource { + + public BacsResponseSource() { + this.type = BACS; + } + +} diff --git a/src/test/java/com/checkout/CheckoutApiImplTest.java b/src/test/java/com/checkout/CheckoutApiImplTest.java index 7e9da8faa..f231dc362 100644 --- a/src/test/java/com/checkout/CheckoutApiImplTest.java +++ b/src/test/java/com/checkout/CheckoutApiImplTest.java @@ -40,6 +40,7 @@ void shouldInstantiateAndRetrieveClients() { assertNotNull(checkoutApi.onboardingSimulatorClient()); // APMs assertNotNull(checkoutApi.idealClient()); + assertNotNull(checkoutApi.bacsClient()); } } diff --git a/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java b/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java new file mode 100644 index 000000000..f1354c990 --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsClientImplTest.java @@ -0,0 +1,86 @@ +package com.checkout.apm.bacs; + +import com.checkout.ApiClient; +import com.checkout.CheckoutArgumentException; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class BacsClientImplTest { + + private static final String NOTIFICATIONS_PATH = "apms/bacs/notifications"; + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + @Mock + private BacsNotificationRequest request; + + @Mock + private BacsNotificationResponse response; + + private BacsClient bacsClient; + + @BeforeEach + void setUp() { + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY)).thenReturn(authorization); + lenient().when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + this.bacsClient = new BacsClientImpl(apiClient, configuration); + } + + @Test + void shouldSendNotification() throws ExecutionException, InterruptedException { + + when(apiClient.postAsync(NOTIFICATIONS_PATH, authorization, BacsNotificationResponse.class, request, null)) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = bacsClient.sendNotification(request); + + assertNotNull(future.get()); + assertEquals(response, future.get()); + } + + // Synchronous methods + @Test + void shouldSendNotificationSync() { + + when(apiClient.post(NOTIFICATIONS_PATH, authorization, BacsNotificationResponse.class, request, null)) + .thenReturn(response); + + final BacsNotificationResponse result = bacsClient.sendNotificationSync(request); + + assertNotNull(result); + assertEquals(response, result); + } + + @Test + void shouldFailWhenRequestIsNull() { + assertThrows(CheckoutArgumentException.class, () -> bacsClient.sendNotification(null)); + assertThrows(CheckoutArgumentException.class, () -> bacsClient.sendNotificationSync(null)); + } +} diff --git a/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java b/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java new file mode 100644 index 000000000..d996a815f --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsNotificationTestIT.java @@ -0,0 +1,42 @@ +package com.checkout.apm.bacs; + +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.common.Currency; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class BacsNotificationTestIT extends SandboxTestFixture { + + BacsNotificationTestIT() { + super(PlatformType.DEFAULT); + } + + @Test + @Disabled("Requires a merchant enabled for Bacs Direct Debit and an existing Bacs instrument") + void shouldSendNotification() { + + final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") + .build(); + + final BacsNotificationResponse response = + blocking(() -> checkoutApi.bacsClient().sendNotification(request)); + + assertNotNull(response); + assertNotNull(response.getEventId()); + } +} diff --git a/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java b/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java new file mode 100644 index 000000000..97f83f764 --- /dev/null +++ b/src/test/java/com/checkout/apm/bacs/BacsSerializationTest.java @@ -0,0 +1,158 @@ +package com.checkout.apm.bacs; + +import com.checkout.GsonSerializer; +import com.checkout.common.Currency; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the Bacs Direct Debit pre-notification endpoint. + */ +class BacsSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeAllPropertiesToSnakeCaseKeys() { + final String json = serializer.toJson(fullyPopulatedRequest()); + + assertTrue(json.contains("\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + assertTrue(json.contains("\"notification_type\":\"advance_notice\"")); + assertTrue(json.contains("\"collection_date\":\"2026-07-15\"")); + assertTrue(json.contains("\"amount\":4999")); + assertTrue(json.contains("\"currency\":\"GBP\"")); + assertTrue(json.contains("\"reference\":\"INV-12345\"")); + assertTrue(json.contains("\"customer_email\":\"customer@example.com\"")); + assertTrue(json.contains("\"billing_descriptor\":\"CHECKOUT\"")); + assertTrue(json.contains("\"support_email\":\"support@test.com\"")); + assertTrue(json.contains("\"support_phone\":\"+447700900123\"")); + } + + @Test + void shouldRoundTripAllProperties() { + final BacsNotificationRequest original = fullyPopulatedRequest(); + + final BacsNotificationRequest result = + serializer.fromJson(serializer.toJson(original), BacsNotificationRequest.class); + + assertEquals(original.getSourceId(), result.getSourceId()); + assertEquals(original.getNotificationType(), result.getNotificationType()); + assertEquals(original.getCollectionDate(), result.getCollectionDate()); + assertEquals(original.getAmount(), result.getAmount()); + assertEquals(original.getCurrency(), result.getCurrency()); + assertEquals(original.getReference(), result.getReference()); + assertEquals(original.getCustomerEmail(), result.getCustomerEmail()); + assertEquals(original.getBillingDescriptor(), result.getBillingDescriptor()); + assertEquals(original.getSupportEmail(), result.getSupportEmail()); + assertEquals(original.getSupportPhone(), result.getSupportPhone()); + } + + @Test + void shouldDeserializeSwaggerExample() { + final String json = "{" + + "\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"notification_type\":\"advance_notice\"," + + "\"collection_date\":\"2026-07-15\"," + + "\"amount\":4999," + + "\"currency\":\"GBP\"," + + "\"reference\":\"INV-12345\"," + + "\"customer_email\":\"customer@example.com\"," + + "\"billing_descriptor\":\"CHECKOUT\"," + + "\"support_email\":\"support@test.com\"," + + "\"support_phone\":\"+447700900123\"" + + "}"; + + final BacsNotificationRequest request = serializer.fromJson(json, BacsNotificationRequest.class); + + assertNotNull(request); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", request.getSourceId()); + assertEquals(BacsNotificationType.ADVANCE_NOTICE, request.getNotificationType()); + assertEquals(LocalDate.of(2026, 7, 15), request.getCollectionDate()); + assertEquals(4999L, request.getAmount()); + assertEquals(Currency.GBP, request.getCurrency()); + assertEquals("INV-12345", request.getReference()); + assertEquals("customer@example.com", request.getCustomerEmail()); + assertEquals("CHECKOUT", request.getBillingDescriptor()); + assertEquals("support@test.com", request.getSupportEmail()); + assertEquals("+447700900123", request.getSupportPhone()); + } + + @Test + void shouldOmitOptionalPropertiesWhenNotSet() { + final BacsNotificationRequest request = BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("reference")); + assertFalse(json.contains("support_phone")); + assertTrue(json.contains("\"source_id\"")); + } + + @Test + void shouldHandleAbsentOptionalProperties() { + final String json = "{" + + "\"source_id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"notification_type\":\"advance_notice\"," + + "\"collection_date\":\"2026-07-15\"," + + "\"amount\":1," + + "\"currency\":\"GBP\"," + + "\"customer_email\":\"customer@example.com\"," + + "\"billing_descriptor\":\"CHECKOUT\"," + + "\"support_email\":\"support@test.com\"" + + "}"; + + final BacsNotificationRequest request = serializer.fromJson(json, BacsNotificationRequest.class); + + assertNull(request.getReference()); + assertNull(request.getSupportPhone()); + assertEquals(1L, request.getAmount()); + } + + @Test + void shouldDeserializeNotificationResponse() { + final BacsNotificationResponse response = serializer.fromJson( + "{\"event_id\":\"evt_lzr4csdtddwetactr6phd3kea4\"}", BacsNotificationResponse.class); + + assertNotNull(response); + assertEquals("evt_lzr4csdtddwetactr6phd3kea4", response.getEventId()); + } + + @Test + void shouldSerializeNotificationTypeBothDirections() { + assertEquals(1, BacsNotificationType.values().length); + assertEquals("\"advance_notice\"", serializer.toJson(BacsNotificationType.ADVANCE_NOTICE)); + assertEquals(BacsNotificationType.ADVANCE_NOTICE, + serializer.fromJson("\"advance_notice\"", BacsNotificationType.class)); + } + + private BacsNotificationRequest fullyPopulatedRequest() { + return BacsNotificationRequest.builder() + .sourceId("src_wmlfc3zyhqzehihu7giusaaawu") + .notificationType(BacsNotificationType.ADVANCE_NOTICE) + .collectionDate(LocalDate.of(2026, 7, 15)) + .amount(4999L) + .currency(Currency.GBP) + .reference("INV-12345") + .customerEmail("customer@example.com") + .billingDescriptor("CHECKOUT") + .supportEmail("support@test.com") + .supportPhone("+447700900123") + .build(); + } +} diff --git a/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java b/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java new file mode 100644 index 000000000..5fd625a22 --- /dev/null +++ b/src/test/java/com/checkout/common/InstrumentTypeSerializationTest.java @@ -0,0 +1,36 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link InstrumentType}. + */ +class InstrumentTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeEveryValueBothDirections() { + final Map expected = new LinkedHashMap<>(); + expected.put(InstrumentType.BANK_ACCOUNT, "bank_account"); + expected.put(InstrumentType.TOKEN, "token"); + expected.put(InstrumentType.CARD, "card"); + expected.put(InstrumentType.CARD_TOKEN, "card_token"); + expected.put(InstrumentType.SEPA, "sepa"); + expected.put(InstrumentType.ACH, "ach"); + expected.put(InstrumentType.BACS, "bacs"); + + assertEquals(expected.size(), InstrumentType.values().length); + + expected.forEach((value, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(value)); + assertEquals(value, serializer.fromJson("\"" + wire + "\"", InstrumentType.class)); + }); + } +} diff --git a/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java b/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java new file mode 100644 index 000000000..6b34eaf20 --- /dev/null +++ b/src/test/java/com/checkout/common/PaymentSourceTypeSerializationTest.java @@ -0,0 +1,142 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link PaymentSourceType}. + * + *

The deprecated SEPA constant is asserted deliberately: it stays on the enum for backwards + * compatibility, so its wire value still has to be pinned. + */ +@SuppressWarnings("deprecation") +class PaymentSourceTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + /** + * SEPA and ID deliberately share the wire value "id", because the previous platform references a + * stored SEPA mandate through the generic "id" source. Gson resolves an incoming "id" to the + * constant declared last, so only the serialize direction is asserted for those two. + */ + private static final Map AMBIGUOUS_ON_READ = new LinkedHashMap<>(); + + static { + AMBIGUOUS_ON_READ.put(PaymentSourceType.SEPA, "id"); + AMBIGUOUS_ON_READ.put(PaymentSourceType.ID, "id"); + } + + @Test + void shouldSerializeEveryValueToItsWireString() { + wireValues().forEach((value, wire) -> + assertEquals("\"" + wire + "\"", serializer.toJson(value), value.name())); + } + + @Test + void shouldDeserializeEveryUnambiguousWireStringBackToItsValue() { + wireValues().forEach((value, wire) -> { + if (AMBIGUOUS_ON_READ.containsKey(value)) { + return; + } + assertEquals(value, serializer.fromJson("\"" + wire + "\"", PaymentSourceType.class), wire); + }); + } + + @Test + void shouldResolveTheSharedIdWireValueToTheLastDeclaredConstant() { + // SEPA is declared after ID, so Gson resolves "id" to SEPA. Reordering the two constants + // would silently change this, which is why the enum documents the ordering as load-bearing. + assertEquals(PaymentSourceType.SEPA, serializer.fromJson("\"id\"", PaymentSourceType.class)); + assertEquals("\"id\"", serializer.toJson(PaymentSourceType.ID)); + assertEquals("\"id\"", serializer.toJson(PaymentSourceType.SEPA)); + } + + @Test + void shouldSerializeBacsBothDirections() { + assertEquals("\"bacs\"", serializer.toJson(PaymentSourceType.BACS)); + assertEquals(PaymentSourceType.BACS, serializer.fromJson("\"bacs\"", PaymentSourceType.class)); + } + + @Test + void shouldSerializeCurrentPlatformSepaAsSepa() { + assertEquals("\"sepa\"", serializer.toJson(PaymentSourceType.SEPAV4)); + assertEquals(PaymentSourceType.SEPAV4, serializer.fromJson("\"sepa\"", PaymentSourceType.class)); + } + + private Map wireValues() { + final Map expected = new LinkedHashMap<>(); + expected.put(PaymentSourceType.ACH, "ach"); + expected.put(PaymentSourceType.AFTERPAY, "afterpay"); + expected.put(PaymentSourceType.ALIPAY, "alipay"); + expected.put(PaymentSourceType.ALIPAY_CN, "alipay_cn"); + expected.put(PaymentSourceType.ALIPAY_HK, "alipay_hk"); + expected.put(PaymentSourceType.ALIPAY_PLUS, "alipay_plus"); + expected.put(PaymentSourceType.ALMA, "alma"); + expected.put(PaymentSourceType.APPLEPAY, "applepay"); + expected.put(PaymentSourceType.BACS, "bacs"); + expected.put(PaymentSourceType.BANCONTACT, "bancontact"); + expected.put(PaymentSourceType.BANK_ACCOUNT, "bank_account"); + expected.put(PaymentSourceType.BENEFIT, "benefit"); + expected.put(PaymentSourceType.BENEFITPAY, "benefitpay"); + expected.put(PaymentSourceType.BIZUM, "bizum"); + expected.put(PaymentSourceType.BLIK, "blik"); + expected.put(PaymentSourceType.BOLETO, "boleto"); + expected.put(PaymentSourceType.CARD, "card"); + expected.put(PaymentSourceType.CURRENCY_ACCOUNT, "currency_account"); + expected.put(PaymentSourceType.CUSTOMER, "customer"); + expected.put(PaymentSourceType.CV_CONNECT, "cvconnect"); + expected.put(PaymentSourceType.DANA, "dana"); + expected.put(PaymentSourceType.DLOCAL, "dlocal"); + expected.put(PaymentSourceType.EPS, "eps"); + expected.put(PaymentSourceType.FAWRY, "fawry"); + expected.put(PaymentSourceType.GCASH, "gcash"); + expected.put(PaymentSourceType.GIROPAY, "giropay"); + expected.put(PaymentSourceType.GOOGLEPAY, "googlepay"); + expected.put(PaymentSourceType.ID, "id"); + expected.put(PaymentSourceType.IDEAL, "ideal"); + expected.put(PaymentSourceType.ILLICADO, "illicado"); + expected.put(PaymentSourceType.KAKAOPAY, "kakaopay"); + expected.put(PaymentSourceType.KLARNA, "klarna"); + expected.put(PaymentSourceType.KNET, "knet"); + expected.put(PaymentSourceType.MBWAY, "mbway"); + expected.put(PaymentSourceType.MOBILEPAY, "mobilepay"); + expected.put(PaymentSourceType.MULTIBANCO, "multibanco"); + expected.put(PaymentSourceType.NETWORK_TOKEN, "network_token"); + expected.put(PaymentSourceType.OCTOPUS, "octopus"); + expected.put(PaymentSourceType.OXXO, "oxxo"); + expected.put(PaymentSourceType.P24, "p24"); + expected.put(PaymentSourceType.PAGOFACIL, "pagofacil"); + expected.put(PaymentSourceType.PAYPAL, "paypal"); + expected.put(PaymentSourceType.PLAID, "plaid"); + expected.put(PaymentSourceType.POLI, "poli"); + expected.put(PaymentSourceType.POSTFINANCE, "postfinance"); + expected.put(PaymentSourceType.PROVIDER_TOKEN, "provider_token"); + expected.put(PaymentSourceType.QPAY, "qpay"); + expected.put(PaymentSourceType.RAPIPAGO, "rapipago"); + expected.put(PaymentSourceType.SEPA, "id"); + expected.put(PaymentSourceType.SEPAV4, "sepa"); + expected.put(PaymentSourceType.SEQURA, "sequra"); + expected.put(PaymentSourceType.SOFORT, "sofort"); + expected.put(PaymentSourceType.STCPAY, "stcpay"); + expected.put(PaymentSourceType.SWISH, "swish"); + expected.put(PaymentSourceType.TABBY, "tabby"); + expected.put(PaymentSourceType.TAMARA, "tamara"); + expected.put(PaymentSourceType.TOKEN, "token"); + expected.put(PaymentSourceType.TNG, "tng"); + expected.put(PaymentSourceType.TRUEMONEY, "truemoney"); + expected.put(PaymentSourceType.TRUSTLY, "trustly"); + expected.put(PaymentSourceType.TWINT, "twint"); + expected.put(PaymentSourceType.VIPPS, "vipps"); + expected.put(PaymentSourceType.WECHATPAY, "wechatpay"); + expected.put(PaymentSourceType.PAYNOW, "paynow"); + + assertEquals(expected.size(), PaymentSourceType.values().length, + "every constant must be listed so the enum cannot grow without this test noticing"); + return expected; + } +} diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java new file mode 100644 index 000000000..20122bfa9 --- /dev/null +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -0,0 +1,258 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateAchAccountHolder; +import com.checkout.instruments.create.CreateAchInstrumentData; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.update.AchInstrumentAccountType; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the ACH variants of the instruments endpoints. + */ +class AchInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // StoreAchInstrumentRequest - 13 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryStoreRequestProperty() { + final String json = serializer.toJson(fullyPopulatedStoreRequest()); + + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"account_type\":\"savings\"")); + assertTrue(json.contains("\"account_number\":\"4099999992\"")); + assertTrue(json.contains("\"bank_code\":\"211370545\"")); + assertTrue(json.contains("\"currency\":\"USD\"")); + assertTrue(json.contains("\"country\":\"US\"")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"company_name\":\"Smith Enterprises\"")); + assertTrue(json.contains("\"email\":\"customer@example.com\"")); + assertTrue(json.contains("\"name\":\"John Smith\"")); + assertTrue(json.contains("\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"")); + assertTrue(json.contains("\"default\":true")); + } + + @Test + void shouldRoundTripEveryStoreRequestProperty() { + final CreateInstrumentAchRequest result = serializer.fromJson( + serializer.toJson(fullyPopulatedStoreRequest()), CreateInstrumentAchRequest.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, result.getType()); + assertEquals(AchInstrumentAccountType.SAVINGS, result.getInstrumentData().getAccountType()); + assertEquals("4099999992", result.getInstrumentData().getAccountNumber()); + assertEquals("211370545", result.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, result.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, result.getInstrumentData().getCountry()); + assertEquals("John", result.getAccountHolder().getFirstName()); + assertEquals("Smith", result.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", result.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, result.getAccountHolder().getType()); + assertEquals("customer@example.com", result.getCustomer().getEmail()); + assertEquals("John Smith", result.getCustomer().getName()); + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", result.getCustomer().getId()); + assertTrue(result.getCustomer().isDefaultInstrument()); + } + + @Test + void shouldDeserializeStoreRequestSwaggerExample() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"instrument_data\":{" + + "\"account_type\":\"savings\"," + + "\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\"," + + "\"currency\":\"USD\"," + + "\"country\":\"US\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"company_name\":\"Smith Enterprises\"," + + "\"type\":\"individual\"" + + "}}"; + + final CreateInstrumentAchRequest request = + serializer.fromJson(json, CreateInstrumentAchRequest.class); + + assertNotNull(request); + assertEquals(AchInstrumentAccountType.SAVINGS, request.getInstrumentData().getAccountType()); + assertEquals(InstrumentAccountHolderType.INDIVIDUAL, request.getAccountHolder().getType()); + assertNull(request.getCustomer()); + } + + @Test + void shouldOmitAbsentOptionalStoreRequestProperties() { + final CreateInstrumentAchRequest request = CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.CHECKING) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.INDIVIDUAL) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("\"customer\"")); + assertTrue(json.contains("\"account_type\":\"checking\"")); + } + + // ------------------------------------------------------------------ + // RetrieveAchInstrumentResponse - 15 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeEveryRetrieveResponseProperty() { + final GetAchInstrumentResponse response = + serializer.fromJson(retrieveResponseJson(), GetAchInstrumentResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + assertEquals(Instant.parse("2021-01-01T00:00:00Z"), response.getCreatedOn()); + assertEquals(Instant.parse("2021-01-02T00:00:00Z"), response.getModifiedOn()); + assertEquals("vid_wmlfc3zyhqzehihu7giusaaawu", response.getVaultId()); + + assertEquals(AchInstrumentAccountType.CHECKING, response.getInstrumentData().getAccountType()); + assertEquals("4099999992", response.getInstrumentData().getAccountNumber()); + assertEquals("211370545", response.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, response.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, response.getInstrumentData().getCountry()); + + assertEquals("John", response.getAccountHolder().getFirstName()); + assertEquals("Smith", response.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", response.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, response.getAccountHolder().getType()); + + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", response.getCustomer().getId()); + assertEquals("customer@example.com", response.getCustomer().getEmail()); + assertTrue(response.getCustomer().isDefault()); + } + + @Test + void shouldRoundTripRetrieveResponse() { + final GetAchInstrumentResponse original = + serializer.fromJson(retrieveResponseJson(), GetAchInstrumentResponse.class); + + final GetAchInstrumentResponse result = serializer.fromJson( + serializer.toJson(original), GetAchInstrumentResponse.class); + + assertEquals(original.getVaultId(), result.getVaultId()); + assertEquals(original.getInstrumentData().getAccountType(), + result.getInstrumentData().getAccountType()); + assertEquals(original.getAccountHolder().getCompanyName(), + result.getAccountHolder().getCompanyName()); + } + + // ------------------------------------------------------------------ + // Store response + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeStoreResponse() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final CreateInstrumentAchResponse response = + serializer.fromJson(json, CreateInstrumentAchResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + @Test + void shouldSerializeAchAccountTypeBothDirections() { + assertEquals(2, AchInstrumentAccountType.values().length); + assertEquals("\"savings\"", serializer.toJson(AchInstrumentAccountType.SAVINGS)); + assertEquals("\"checking\"", serializer.toJson(AchInstrumentAccountType.CHECKING)); + assertEquals(AchInstrumentAccountType.SAVINGS, + serializer.fromJson("\"savings\"", AchInstrumentAccountType.class)); + assertEquals(AchInstrumentAccountType.CHECKING, + serializer.fromJson("\"checking\"", AchInstrumentAccountType.class)); + } + + private CreateInstrumentAchRequest fullyPopulatedStoreRequest() { + return CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.CORPORATE) + .build()) + .customer(CreateCustomerInstrumentRequest.builder() + .id("cus_udst2tfldj6upmye2reztkmm4i") + .email("customer@example.com") + .name("John Smith") + .defaultInstrument(true) + .build()) + .build(); + } + + private String retrieveResponseJson() { + return "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"modified_on\":\"2021-01-02T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{" + + "\"account_type\":\"checking\"," + + "\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\"," + + "\"currency\":\"USD\"," + + "\"country\":\"US\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"company_name\":\"Smith Enterprises\"," + + "\"type\":\"corporate\"" + + "}," + + "\"customer\":{" + + "\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"," + + "\"email\":\"customer@example.com\"," + + "\"default\":true" + + "}}"; + } +} diff --git a/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java new file mode 100644 index 000000000..7ec163834 --- /dev/null +++ b/src/test/java/com/checkout/instruments/AchInstrumentsTestIT.java @@ -0,0 +1,54 @@ +package com.checkout.instruments; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateAchAccountHolder; +import com.checkout.instruments.create.CreateAchInstrumentData; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.update.AchInstrumentAccountType; +import com.checkout.payments.AbstractPaymentsTestIT; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class AchInstrumentsTestIT extends AbstractPaymentsTestIT { + + @Test + @Disabled("Requires a merchant enabled for ACH Direct Debit") + void shouldCreateAndGetInstrumentAch() { + + final CreateInstrumentAchRequest request = CreateInstrumentAchRequest.builder() + .instrumentData(CreateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(CreateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.INDIVIDUAL) + .build()) + .build(); + + final CreateInstrumentAchResponse created = + blocking(() -> checkoutApi.instrumentsClient().create(request)); + + assertNotNull(created); + assertNotNull(created.getId()); + assertNotNull(created.getFingerprint()); + + final GetAchInstrumentResponse retrieved = + (GetAchInstrumentResponse) blocking(() -> checkoutApi.instrumentsClient().get(created.getId())); + + assertNotNull(retrieved); + assertNotNull(retrieved.getInstrumentData()); + assertNotNull(retrieved.getAccountHolder()); + assertNotNull(retrieved.getVaultId()); + } +} diff --git a/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java new file mode 100644 index 000000000..ef5cf5779 --- /dev/null +++ b/src/test/java/com/checkout/instruments/BacsInstrumentSerializationTest.java @@ -0,0 +1,446 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.instruments.update.UpdateBacsAccountHolder; +import com.checkout.instruments.update.UpdateBacsBillingAddress; +import com.checkout.instruments.update.UpdateBacsInstrumentData; +import com.checkout.instruments.update.UpdateInstrumentBacsRequest; +import com.checkout.instruments.update.UpdateInstrumentBacsResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the Bacs Direct Debit variants of the instruments endpoints. + */ +class BacsInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // StoreBacsInstrumentRequest - 20 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryStoreRequestProperty() { + final String json = serializer.toJson(fullyPopulatedStoreRequest()); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"processing_channel_id\":\"pc_q4dbxom5jbgudnjzjpz7j2z6uq\"")); + assertTrue(json.contains("\"account_number\":\"86753246\"")); + assertTrue(json.contains("\"bank_code\":\"040004\"")); + assertTrue(json.contains("\"currency\":\"GBP\"")); + assertTrue(json.contains("\"payment_type\":\"Recurring\"")); + assertTrue(json.contains("\"allow_partial_match\":true")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"address_line1\":\"Cloverfield St.\"")); + assertTrue(json.contains("\"address_line2\":\"23A\"")); + assertTrue(json.contains("\"city\":\"London\"")); + assertTrue(json.contains("\"zip\":\"SW1A 1AA\"")); + assertTrue(json.contains("\"country\":\"GB\"")); + assertTrue(json.contains("\"email\":\"customer@example.com\"")); + assertTrue(json.contains("\"name\":\"John Smith\"")); + assertTrue(json.contains("\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"")); + assertTrue(json.contains("\"default\":true")); + } + + @Test + void shouldRoundTripEveryStoreRequestProperty() { + final CreateInstrumentBacsRequest original = fullyPopulatedStoreRequest(); + + final CreateInstrumentBacsRequest result = + serializer.fromJson(serializer.toJson(original), CreateInstrumentBacsRequest.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, result.getType()); + assertEquals("pc_q4dbxom5jbgudnjzjpz7j2z6uq", result.getAccount().getProcessingChannelId()); + assertEquals("86753246", result.getInstrumentData().getAccountNumber()); + assertEquals("040004", result.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, result.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, result.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.RECURRING, result.getInstrumentData().getPaymentType()); + assertTrue(result.getInstrumentData().getAllowPartialMatch()); + assertEquals("John", result.getAccountHolder().getFirstName()); + assertEquals("Smith", result.getAccountHolder().getLastName()); + assertEquals("Cloverfield St.", result.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("23A", result.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", result.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", result.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, result.getAccountHolder().getBillingAddress().getCountry()); + assertEquals("customer@example.com", result.getCustomer().getEmail()); + assertEquals("John Smith", result.getCustomer().getName()); + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", result.getCustomer().getId()); + assertTrue(result.getCustomer().isDefaultInstrument()); + } + + @Test + void shouldDeserializeStoreRequestSwaggerExample() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"account\":{\"processing_channel_id\":\"pc_q4dbxom5jbgudnjzjpz7j2z6uq\"}," + + "\"instrument_data\":{" + + "\"account_number\":\"86753246\"," + + "\"bank_code\":\"040004\"," + + "\"country\":\"GB\"," + + "\"currency\":\"GBP\"," + + "\"payment_type\":\"Recurring\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"John\"," + + "\"last_name\":\"Smith\"," + + "\"billing_address\":{" + + "\"address_line1\":\"Cloverfield St.\"," + + "\"address_line2\":\"23A\"," + + "\"city\":\"London\"," + + "\"zip\":\"SW1A 1AA\"," + + "\"country\":\"GB\"" + + "}}}"; + + final CreateInstrumentBacsRequest request = + serializer.fromJson(json, CreateInstrumentBacsRequest.class); + + assertNotNull(request); + assertEquals(BacsPaymentType.RECURRING, request.getInstrumentData().getPaymentType()); + assertNull(request.getInstrumentData().getAllowPartialMatch()); + assertNull(request.getCustomer()); + } + + @Test + void shouldOmitAbsentOptionalStoreRequestProperties() { + final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.REGULAR) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .country(CountryCode.GB) + .build()) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("allow_partial_match")); + assertFalse(json.contains("\"customer\"")); + assertTrue(json.contains("\"payment_type\":\"Regular\"")); + } + + // ------------------------------------------------------------------ + // UpdateBacsInstrumentRequest - 19 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldSerializeEveryUpdateRequestProperty() { + final String json = serializer.toJson(fullyPopulatedUpdateRequest()); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"account_number\":\"86753246\"")); + assertTrue(json.contains("\"bank_code\":\"040004\"")); + assertTrue(json.contains("\"payment_type\":\"Regular\"")); + assertTrue(json.contains("\"allow_partial_match\":true")); + assertTrue(json.contains("\"first_name\":\"Hannah\"")); + assertTrue(json.contains("\"last_name\":\"Bret\"")); + assertTrue(json.contains("\"company_name\":\"Bret Holdings Ltd\"")); + assertTrue(json.contains("\"address_line1\":\"123 High St.\"")); + assertTrue(json.contains("\"address_line2\":\"Flat 456\"")); + assertTrue(json.contains("\"city\":\"London\"")); + assertTrue(json.contains("\"zip\":\"SW1A 1AA\"")); + assertTrue(json.contains("\"country\":\"GB\"")); + assertTrue(json.contains("\"type\":\"corporate\"")); + } + + @Test + void shouldRoundTripEveryUpdateRequestProperty() { + final UpdateInstrumentBacsRequest result = serializer.fromJson( + serializer.toJson(fullyPopulatedUpdateRequest()), UpdateInstrumentBacsRequest.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, result.getType()); + assertEquals("86753246", result.getInstrumentData().getAccountNumber()); + assertEquals("040004", result.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, result.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, result.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.REGULAR, result.getInstrumentData().getPaymentType()); + assertTrue(result.getInstrumentData().getAllowPartialMatch()); + assertEquals("Hannah", result.getAccountHolder().getFirstName()); + assertEquals("Bret", result.getAccountHolder().getLastName()); + assertEquals("Bret Holdings Ltd", result.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, result.getAccountHolder().getType()); + assertEquals("123 High St.", result.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("Flat 456", result.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", result.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", result.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, result.getAccountHolder().getBillingAddress().getCountry()); + } + + @Test + void shouldSerializeEmptyUpdateRequestWithTypeOnly() { + final String json = serializer.toJson(new UpdateInstrumentBacsRequest()); + + assertEquals("{\"type\":\"bacs\"}", json); + } + + // ------------------------------------------------------------------ + // RetrieveBacsInstrumentResponse - 31 properties including nested + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeEveryRetrieveResponseProperty() { + final GetBacsInstrumentResponse response = + serializer.fromJson(retrieveResponseJson(), GetBacsInstrumentResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + assertEquals(Instant.parse("2021-01-01T00:00:00Z"), response.getCreatedOn()); + assertEquals(Instant.parse("2021-01-02T00:00:00Z"), response.getModifiedOn()); + assertEquals("vid_wmlfc3zyhqzehihu7giusaaawu", response.getVaultId()); + + assertEquals("cli_memowvltf7aulpb3poehtiffei", response.getAccount().getClientId()); + assertEquals("pc_jcs4ufa6hrgepcrvhic4bfspay", response.getAccount().getProcessingChannelId()); + + assertEquals(1, response.getValidations().size()); + assertEquals("account_number", response.getValidations().get(0).get("field")); + + assertEquals("86753246", response.getInstrumentData().getAccountNumber()); + assertEquals("040004", response.getInstrumentData().getBankCode()); + assertEquals(CountryCode.GB, response.getInstrumentData().getCountry()); + assertEquals(Currency.GBP, response.getInstrumentData().getCurrency()); + assertEquals(BacsPaymentType.RECURRING, response.getInstrumentData().getPaymentType()); + assertTrue(response.getInstrumentData().getAllowPartialMatch()); + assertEquals("INVALID", response.getInstrumentData().getStatus()); + assertEquals("no match", response.getInstrumentData().getMatchStatus()); + assertEquals("The name did not match with the account owner.", + response.getInstrumentData().getDescription()); + assertEquals("6PZ6KFI3KW3UFHAM3J", response.getInstrumentData().getMandateId()); + + assertEquals("Hannah", response.getAccountHolder().getFirstName()); + assertEquals("Bret", response.getAccountHolder().getLastName()); + assertEquals("Bret Holdings Ltd", response.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, response.getAccountHolder().getType()); + assertEquals("123 High St.", response.getAccountHolder().getBillingAddress().getAddressLine1()); + assertEquals("Flat 456", response.getAccountHolder().getBillingAddress().getAddressLine2()); + assertEquals("London", response.getAccountHolder().getBillingAddress().getCity()); + assertEquals("SW1A 1AA", response.getAccountHolder().getBillingAddress().getZip()); + assertEquals(CountryCode.GB, response.getAccountHolder().getBillingAddress().getCountry()); + + assertEquals("cus_udst2tfldj6upmye2reztkmm4i", response.getCustomer().getId()); + assertEquals("customer@example.com", response.getCustomer().getEmail()); + assertTrue(response.getCustomer().isDefault()); + } + + @Test + void shouldRoundTripRetrieveResponseValidations() { + final GetBacsInstrumentResponse original = + serializer.fromJson(retrieveResponseJson(), GetBacsInstrumentResponse.class); + + final GetBacsInstrumentResponse result = serializer.fromJson( + serializer.toJson(original), GetBacsInstrumentResponse.class); + + assertEquals(1, result.getValidations().size()); + assertEquals("no match", result.getValidations().get(0).get("result")); + } + + // ------------------------------------------------------------------ + // Store and update responses + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeStoreResponse() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final CreateInstrumentBacsResponse response = + serializer.fromJson(json, CreateInstrumentBacsResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentBacsResponse response = + serializer.fromJson(json, UpdateInstrumentBacsResponse.class); + + assertEquals(com.checkout.common.InstrumentType.BACS, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + + // ------------------------------------------------------------------ + // BacsPaymentType - the casing regression guard for T1 + // ------------------------------------------------------------------ + + @Test + void shouldSerializeBacsPaymentTypeCapitalized() { + assertEquals("\"Recurring\"", serializer.toJson(BacsPaymentType.RECURRING)); + assertEquals("\"Regular\"", serializer.toJson(BacsPaymentType.REGULAR)); + assertEquals(BacsPaymentType.RECURRING, + serializer.fromJson("\"Recurring\"", BacsPaymentType.class)); + assertEquals(BacsPaymentType.REGULAR, + serializer.fromJson("\"Regular\"", BacsPaymentType.class)); + } + + @Test + void shouldKeepSepaPaymentTypeLowercase() { + assertEquals("\"recurring\"", serializer.toJson(SepaPaymentType.RECURRING)); + assertEquals("\"regular\"", serializer.toJson(SepaPaymentType.REGULAR)); + assertEquals(SepaPaymentType.RECURRING, + serializer.fromJson("\"recurring\"", SepaPaymentType.class)); + assertEquals(SepaPaymentType.REGULAR, + serializer.fromJson("\"regular\"", SepaPaymentType.class)); + } + + @Test + void shouldSerializeInstrumentAccountHolderTypeBothDirections() { + assertEquals("\"individual\"", serializer.toJson(InstrumentAccountHolderType.INDIVIDUAL)); + assertEquals("\"corporate\"", serializer.toJson(InstrumentAccountHolderType.CORPORATE)); + assertEquals(InstrumentAccountHolderType.INDIVIDUAL, + serializer.fromJson("\"individual\"", InstrumentAccountHolderType.class)); + assertEquals(InstrumentAccountHolderType.CORPORATE, + serializer.fromJson("\"corporate\"", InstrumentAccountHolderType.class)); + } + + private CreateInstrumentBacsRequest fullyPopulatedStoreRequest() { + return CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .allowPartialMatch(true) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .addressLine1("Cloverfield St.") + .addressLine2("23A") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .customer(CreateCustomerInstrumentRequest.builder() + .id("cus_udst2tfldj6upmye2reztkmm4i") + .email("customer@example.com") + .name("John Smith") + .defaultInstrument(true) + .build()) + .build(); + } + + private UpdateInstrumentBacsRequest fullyPopulatedUpdateRequest() { + return UpdateInstrumentBacsRequest.builder() + .instrumentData(UpdateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.REGULAR) + .allowPartialMatch(true) + .build()) + .accountHolder(UpdateBacsAccountHolder.builder() + .firstName("Hannah") + .lastName("Bret") + .companyName("Bret Holdings Ltd") + .type(InstrumentAccountHolderType.CORPORATE) + .billingAddress(UpdateBacsBillingAddress.builder() + .addressLine1("123 High St.") + .addressLine2("Flat 456") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .build(); + } + + private String retrieveResponseJson() { + return "{" + + "\"type\":\"bacs\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"modified_on\":\"2021-01-02T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"account\":{" + + "\"client_id\":\"cli_memowvltf7aulpb3poehtiffei\"," + + "\"processing_channel_id\":\"pc_jcs4ufa6hrgepcrvhic4bfspay\"" + + "}," + + "\"validations\":[{\"field\":\"account_number\",\"result\":\"no match\"}]," + + "\"instrument_data\":{" + + "\"account_number\":\"86753246\"," + + "\"bank_code\":\"040004\"," + + "\"country\":\"GB\"," + + "\"currency\":\"GBP\"," + + "\"payment_type\":\"Recurring\"," + + "\"allow_partial_match\":true," + + "\"status\":\"INVALID\"," + + "\"match_status\":\"no match\"," + + "\"description\":\"The name did not match with the account owner.\"," + + "\"mandate_id\":\"6PZ6KFI3KW3UFHAM3J\"" + + "}," + + "\"account_holder\":{" + + "\"first_name\":\"Hannah\"," + + "\"last_name\":\"Bret\"," + + "\"company_name\":\"Bret Holdings Ltd\"," + + "\"type\":\"corporate\"," + + "\"billing_address\":{" + + "\"address_line1\":\"123 High St.\"," + + "\"address_line2\":\"Flat 456\"," + + "\"city\":\"London\"," + + "\"zip\":\"SW1A 1AA\"," + + "\"country\":\"GB\"" + + "}}," + + "\"customer\":{" + + "\"id\":\"cus_udst2tfldj6upmye2reztkmm4i\"," + + "\"email\":\"customer@example.com\"," + + "\"default\":true" + + "}}"; + } +} diff --git a/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java new file mode 100644 index 000000000..ef960a301 --- /dev/null +++ b/src/test/java/com/checkout/instruments/BacsInstrumentsTestIT.java @@ -0,0 +1,54 @@ +package com.checkout.instruments; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import com.checkout.instruments.create.CreateBacsAccountHolder; +import com.checkout.instruments.create.CreateBacsBillingAddress; +import com.checkout.instruments.create.CreateBacsInstrumentAccount; +import com.checkout.instruments.create.CreateBacsInstrumentData; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.payments.AbstractPaymentsTestIT; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class BacsInstrumentsTestIT extends AbstractPaymentsTestIT { + + @Test + @Disabled("Requires a merchant enabled for Bacs Direct Debit") + void shouldCreateInstrumentBacs() { + + final CreateInstrumentBacsRequest request = CreateInstrumentBacsRequest.builder() + .account(CreateBacsInstrumentAccount.builder() + .processingChannelId("pc_q4dbxom5jbgudnjzjpz7j2z6uq") + .build()) + .instrumentData(CreateBacsInstrumentData.builder() + .accountNumber("86753246") + .bankCode("040004") + .country(CountryCode.GB) + .currency(Currency.GBP) + .paymentType(BacsPaymentType.RECURRING) + .build()) + .accountHolder(CreateBacsAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .billingAddress(CreateBacsBillingAddress.builder() + .addressLine1("Cloverfield St.") + .addressLine2("23A") + .city("London") + .zip("SW1A 1AA") + .country(CountryCode.GB) + .build()) + .build()) + .build(); + + final CreateInstrumentBacsResponse response = + blocking(() -> checkoutApi.instrumentsClient().create(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getFingerprint()); + } +} diff --git a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java new file mode 100644 index 000000000..7102e0d5c --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java @@ -0,0 +1,96 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.instruments.create.CreateInstrumentAchResponse; +import com.checkout.instruments.create.CreateInstrumentBacsResponse; +import com.checkout.instruments.create.CreateInstrumentResponse; +import com.checkout.instruments.get.GetAchInstrumentResponse; +import com.checkout.instruments.get.GetBacsInstrumentResponse; +import com.checkout.instruments.get.GetInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentBacsResponse; +import com.checkout.instruments.update.UpdateInstrumentResponse; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * Polymorphic dispatch tests for the Bacs Direct Debit and ACH instrument responses. + * + *

Before these subtypes were registered on the three instrument factories, every assertion here + * failed with a JsonParseException, because none of the factories declares a default subtype. ACH + * was already registered on the update factory but not on create or get. + */ +class InstrumentResponseDispatchTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldDispatchCreateResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + + final CreateInstrumentBacsResponse bacs = assertInstanceOf(CreateInstrumentBacsResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); + } + + @Test + void shouldDispatchUpdateResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentBacsResponse bacs = assertInstanceOf(UpdateInstrumentBacsResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", bacs.getFingerprint()); + } + + @Test + void shouldDispatchCreateResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + + final CreateInstrumentAchResponse ach = assertInstanceOf(CreateInstrumentAchResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", ach.getId()); + } + + @Test + void shouldDispatchGetResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{\"account_type\":\"savings\",\"account_number\":\"4099999992\"," + + "\"bank_code\":\"211370545\",\"currency\":\"USD\",\"country\":\"US\"}}"; + + final GetInstrumentResponse response = serializer.fromJson(json, GetInstrumentResponse.class); + + final GetAchInstrumentResponse ach = assertInstanceOf(GetAchInstrumentResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals(com.checkout.instruments.update.AchInstrumentAccountType.SAVINGS, + ach.getInstrumentData().getAccountType()); + } + + @Test + void shouldDispatchGetResponseToBacsSubtype() { + final String json = "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"," + + "\"created_on\":\"2021-01-01T00:00:00Z\"," + + "\"vault_id\":\"vid_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"instrument_data\":{\"account_number\":\"86753246\",\"bank_code\":\"040004\"," + + "\"country\":\"GB\",\"currency\":\"GBP\",\"payment_type\":\"Recurring\"}}"; + + final GetInstrumentResponse response = serializer.fromJson(json, GetInstrumentResponse.class); + + final GetBacsInstrumentResponse bacs = assertInstanceOf(GetBacsInstrumentResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals(BacsPaymentType.RECURRING, bacs.getInstrumentData().getPaymentType()); + } +} diff --git a/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java b/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java new file mode 100644 index 000000000..2c116a289 --- /dev/null +++ b/src/test/java/com/checkout/payments/previous/request/source/apm/RequestSepaSourceSerializationTest.java @@ -0,0 +1,54 @@ +package com.checkout.payments.previous.request.source.apm; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Serialization tests for the previous-platform SEPA source. + * + *

These pin the wire contract that the previous platform references a stored SEPA mandate + * through the generic "id" source. The source class was switched from the deprecated + * PaymentSourceType.SEPA to PaymentSourceType.ID, which both map to "id", so these assertions must + * hold identically before and after that change. + */ +class RequestSepaSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeTypeAsIdAndNotSepa() { + final RequestSepaSource source = RequestSepaSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final String json = serializer.toJson(source); + + assertTrue(json.contains("\"type\":\"id\"")); + assertFalse(json.contains("\"sepa\"")); + assertTrue(json.contains("\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + } + + @Test + void shouldUseTheIdSourceTypeConstant() { + assertEquals(PaymentSourceType.ID, new RequestSepaSource().getType()); + assertEquals(PaymentSourceType.ID, + RequestSepaSource.builder().id("src_wmlfc3zyhqzehihu7giusaaawu").build().getType()); + } + + @Test + void shouldMatchTheCurrentPlatformSourceOnEverythingButTheType() { + final String previousJson = serializer.toJson(RequestSepaSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build()); + final String currentJson = serializer.toJson( + com.checkout.payments.request.source.apm.RequestSepaSource.builder().build()); + + assertTrue(previousJson.contains("\"type\":\"id\"")); + assertTrue(currentJson.contains("\"type\":\"sepa\"")); + } +} diff --git a/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java b/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java new file mode 100644 index 000000000..cfaa692c0 --- /dev/null +++ b/src/test/java/com/checkout/payments/request/source/apm/RequestBacsSourceSerializationTest.java @@ -0,0 +1,48 @@ +package com.checkout.payments.request.source.apm; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for {@link RequestBacsSource}. + */ +class RequestBacsSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeTypeAndId() { + final RequestBacsSource source = RequestBacsSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final String json = serializer.toJson(source); + + assertTrue(json.contains("\"type\":\"bacs\"")); + assertTrue(json.contains("\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"")); + } + + @Test + void shouldRoundTripTypeAndId() { + final RequestBacsSource original = RequestBacsSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + final RequestBacsSource result = + serializer.fromJson(serializer.toJson(original), RequestBacsSource.class); + + assertNotNull(result); + assertEquals(PaymentSourceType.BACS, result.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", result.getId()); + } + + @Test + void shouldDefaultTypeOnNoArgsConstructor() { + assertEquals(PaymentSourceType.BACS, new RequestBacsSource().getType()); + } +} diff --git a/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java b/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java new file mode 100644 index 000000000..3c1a1ef3f --- /dev/null +++ b/src/test/java/com/checkout/payments/response/source/BacsResponseSourceSerializationTest.java @@ -0,0 +1,40 @@ +package com.checkout.payments.response.source; + +import com.checkout.GsonSerializer; +import com.checkout.common.PaymentSourceType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Schema validation tests for {@link BacsResponseSource}, including the polymorphic dispatch that + * selects it instead of the alternative payment source fallback. + */ +class BacsResponseSourceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldDeserializeTypeAndId() { + final BacsResponseSource source = serializer.fromJson( + "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"}", + BacsResponseSource.class); + + assertNotNull(source); + assertEquals(PaymentSourceType.BACS, source.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", source.getId()); + } + + @Test + void shouldDispatchToTypedSourceAndNotTheFallback() { + final ResponseSource source = serializer.fromJson( + "{\"type\":\"bacs\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"}", + ResponseSource.class); + + final BacsResponseSource bacs = assertInstanceOf(BacsResponseSource.class, source); + assertEquals(PaymentSourceType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); + } +} From 2a5a525ba36c692fddd8265ea75bbc33342cee01 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 09:57:27 +0200 Subject: [PATCH 11/18] Id field fixes, test fixes and some other serialization fixes --- .../java/com/checkout/GsonSerializer.java | 3 + .../checkout/common/PaymentMethodType.java | 20 ++- .../payments/common/source/SourceType.java | 9 ++ .../common/source/achsource/AchSource.java | 43 +++++++ .../source/alipaycnsource/AlipayCnSource.java | 16 +++ .../common/source/bacssource/BacsSource.java | 43 +++++++ .../bankaccountsource/BankAccountSource.java | 43 +++++++ .../instruments/create/InstrumentData.java | 50 ++++++-- .../update/UpdateInstrumentAchResponse.java | 15 +++ .../update/UpdateInstrumentBacsResponse.java | 3 - .../update/UpdateInstrumentResponse.java | 13 ++ .../update/UpdateInstrumentSepaResponse.java | 15 +++ .../RuntimeTypeAdapterFactory.java | 7 +- .../checkout/ScratchDiscriminatorProbe.java | 85 +++++++++++++ .../PaymentMethodTypeSerializationTest.java | 118 ++++++++++++++++++ ...ayoutResponseCreatedSerializationTest.java | 66 ++++++++++ .../AchInstrumentSerializationTest.java | 20 ++- .../InstrumentResponseDispatchTest.java | 35 +++++- ...a => SepaInstrumentSerializationTest.java} | 86 ++++++++++++- .../instruments/SepaInstrumentsTestIT.java | 4 +- .../schema/LocalDateFieldsRegressionTest.java | 6 +- 21 files changed, 667 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java create mode 100644 src/test/java/com/checkout/ScratchDiscriminatorProbe.java create mode 100644 src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java rename src/test/java/com/checkout/instruments/{CreateInstrumentSepaRequestSerializationTest.java => SepaInstrumentSerializationTest.java} (67%) diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index ed0c0e0eb..5e09d4b75 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -115,6 +115,9 @@ public final class GsonSerializer implements Serializer { CheckoutUtils.TYPE ) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CARD)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ACH)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource.BacsSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BACS)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource.BankAccountSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.BANK_ACCOUNT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.afterpaysource.AfterpaySource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.AFTERPAY)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.alipaycnsource.AlipayCnSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ALIPAY_CN)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.alipayhksource.AlipayHkSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ALIPAY_HK)) diff --git a/src/main/java/com/checkout/common/PaymentMethodType.java b/src/main/java/com/checkout/common/PaymentMethodType.java index 95a14ebf2..f18e1baaf 100644 --- a/src/main/java/com/checkout/common/PaymentMethodType.java +++ b/src/main/java/com/checkout/common/PaymentMethodType.java @@ -3,14 +3,19 @@ import com.google.gson.annotations.SerializedName; /** - * Comprehensive enum for all payment method types across different APIs - * Consolidates values from payment methods, flow APIs, and other payment contexts + * The type of payment method. + * + *

This is a consolidated enum covering every payment method type the SDK sees, across + * GET /payment-methods, the flow API and the payment session entities. The specification's + * PaymentMethod.type enum is a subset of it, so the constants declared here that the current + * specification does not list are deliberate rather than invented, and must not be removed by a + * reverse comparison against a single endpoint. */ public enum PaymentMethodType { @SerializedName("accel") ACCEL, - @SerializedName("ach") + @SerializedName("ach") ACH, @SerializedName("alipay_cn") ALIPAY_CN, @@ -24,6 +29,8 @@ public enum PaymentMethodType { AMEX, @SerializedName("applepay") APPLEPAY, + @SerializedName("bacs") + BACS, @SerializedName("bancontact") BANCONTACT, @SerializedName("benefit") @@ -100,6 +107,10 @@ public enum PaymentMethodType { QPAY, @SerializedName("rabbit_line_pay") RABBIT_LINE_PAY, + /** + * Fully supported by the API but deliberately unlisted in the public specification, so that + * merchants do not disable Remember Me en masse. Do not remove it as an unspecified value. + */ @SerializedName("remember_me") REMEMBER_ME, @SerializedName("sepa") @@ -137,7 +148,8 @@ public enum PaymentMethodType { @SerializedName("wechatpay") WECHATPAY, - // Payment method categories + // Payment method categories. These are grouping values used by the flow and payment session + // entities, not values of the specification's PaymentMethod.type enum. @SerializedName("card_scheme") CARD_SCHEME, @SerializedName("bank_redirects") diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java index 2c78a26f9..0e73cf763 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/SourceType.java @@ -7,6 +7,9 @@ public enum SourceType { @SerializedName("card") CARD, + @SerializedName("ach") + ACH, + @SerializedName("afterpay") AFTERPAY, @@ -22,9 +25,15 @@ public enum SourceType { @SerializedName("alma") ALMA, + @SerializedName("bacs") + BACS, + @SerializedName("bancontact") BANCONTACT, + @SerializedName("bank_account") + BANK_ACCOUNT, + @SerializedName("benefit") BENEFIT, diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java new file mode 100644 index 000000000..e75338af3 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/achsource/AchSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.achsource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * ach source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class AchSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the AchSource class. + */ + @Builder + private AchSource( + final String id + ) { + super(SourceType.ACH); + this.id = id; + } + + public AchSource() { + super(SourceType.ACH); + } + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java index 196ada801..328cd939e 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/alipaycnsource/AlipayCnSource.java @@ -2,6 +2,7 @@ import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -17,9 +18,24 @@ @ToString(callSuper = true) public final class AlipayCnSource extends AbstractSource { + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + /** * Initializes a new instance of the AlipayCnSource class. */ + @Builder + private AlipayCnSource( + final String id + ) { + super(SourceType.ALIPAY_CN); + this.id = id; + } + public AlipayCnSource() { super(SourceType.ALIPAY_CN); } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java new file mode 100644 index 000000000..7097b2187 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bacssource/BacsSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * bacs source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BacsSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the BacsSource class. + */ + @Builder + private BacsSource( + final String id + ) { + super(SourceType.BACS); + this.id = id; + } + + public BacsSource() { + super(SourceType.BACS); + } + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java new file mode 100644 index 000000000..327fce082 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/payments/common/source/bankaccountsource/BankAccountSource.java @@ -0,0 +1,43 @@ +package com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource; + +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * bank_account source Class + * The source of the payment + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class BankAccountSource extends AbstractSource { + + /** + * The payment instrument identifier + * [Required] + * ^(src)_(\w{26})$ + */ + private String id; + + /** + * Initializes a new instance of the BankAccountSource class. + */ + @Builder + private BankAccountSource( + final String id + ) { + super(SourceType.BANK_ACCOUNT); + this.id = id; + } + + public BankAccountSource() { + super(SourceType.BANK_ACCOUNT); + } + +} diff --git a/src/main/java/com/checkout/instruments/create/InstrumentData.java b/src/main/java/com/checkout/instruments/create/InstrumentData.java index 0e530e18e..8bde35dd1 100644 --- a/src/main/java/com/checkout/instruments/create/InstrumentData.java +++ b/src/main/java/com/checkout/instruments/create/InstrumentData.java @@ -2,7 +2,8 @@ import com.checkout.common.CountryCode; import com.checkout.common.Currency; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.payments.request.source.apm.MandateType; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; @@ -11,6 +12,13 @@ import java.time.LocalDate; +/** + * The details of the SEPA mandate being stored. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs Direct + * Debit field is capitalized, so the two must not share a type: sending + * {@link com.checkout.instruments.BacsPaymentType} values here produces a request the API rejects. + */ @Data @Builder @AllArgsConstructor @@ -18,38 +26,58 @@ public final class InstrumentData { /** - * The SEPA account number. + * The type of mandate. * [Optional] + * Enum: "Core" "B2B" + */ + private MandateType type; + + /** + * The International Bank Account Number (IBAN) of the account. + * [Required] + * min 15 characters + * max 34 characters + * + *

The Java field name carries a typo. The wire name is correct because of the explicit + * annotation, so this is a source-only defect; renaming the field is breaking and is deferred + * to the next major version. */ @SerializedName("account_number") private String accoountNumber; /** - * The country of the SEPA account. - * [Optional] + * The country of the account. + * [Required] + * min 2 characters + * max 2 characters */ private CountryCode country; /** - * The currency of the SEPA account. - * [Optional] + * The currency of the account. + * [Required] + * min 3 characters + * max 3 characters */ private Currency currency; /** - * The payment type for this instrument. - * [Optional] + * The type of payment. recurring or regular. + * [Required] */ - private PaymentType paymentType; + private SepaPaymentType paymentType; /** - * The unique identifier of the SEPA mandate. + * The mandate ID. If a mandate ID is not provided, a new, random mandate ID will be generated. * [Optional] + * min 1 characters + * max 35 characters */ private String mandateId; /** - * The date the mandate was signed. + * The date on which the mandate was signed. Required if mandateId is provided. Ignored and set + * as the current date if mandateId is not provided. * [Optional] * Format: yyyy-MM-dd */ diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java index cb825724e..56b8546a1 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchResponse.java @@ -5,11 +5,26 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update ACH bank account instrument response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentAchResponse extends UpdateInstrumentResponse { + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ private final InstrumentType type = InstrumentType.ACH; + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java index 76fbf0af4..e5c0be2de 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java @@ -7,9 +7,6 @@ /** * Update Bacs Direct Debit account instrument response. - * - *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The id is declared here - * because the shared update response does not carry it. */ @Data @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java index 72483baa2..b2aa84b82 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentResponse.java @@ -4,10 +4,23 @@ import lombok.Data; import lombok.EqualsAndHashCode; +/** + * The response for the type of instrument updated. + * + *

The id is deliberately not declared here. The specification declares it on the sepa, ach and + * bacs update responses only, so each of those three variants carries its own. The bank_account and + * card update responses return a type and a fingerprint but no id, and would expose an always-null + * property if the id lived on this base class. + */ @Data @EqualsAndHashCode(callSuper = true) public abstract class UpdateInstrumentResponse extends HttpMetadata { + /** + * A token that can uniquely identify this instrument across all customers. + * [Required] + * ^([a-z0-9]{26})$ + */ private String fingerprint; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java index 87f433785..90c379c55 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaResponse.java @@ -5,11 +5,26 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update SEPA account instrument response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentSepaResponse extends UpdateInstrumentResponse { + /** + * The underlying instrument type. For instruments created from Checkout.com tokens, this will + * reflect the type of instrument that was tokenized. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; + /** + * The unique identifier of the payment source or destination that can be used later for + * payments. + * [Required] + */ + private String id; + } diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 716164053..799577e00 100644 --- a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -270,13 +270,12 @@ public void write(final JsonWriter out, final R value) throws IOException { final JsonObject clone = new JsonObject(); - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } clone.add(typeFieldName, new JsonPrimitive(label)); for (final Map.Entry e : jsonObject.entrySet()) { + if (typeFieldName.equals(e.getKey())) { + continue; + } clone.add(e.getKey(), e.getValue()); } Streams.write(clone, out); diff --git a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java new file mode 100644 index 000000000..f20fac506 --- /dev/null +++ b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java @@ -0,0 +1,85 @@ +package com.checkout; + +import com.checkout.issuing.controls.requests.VelocityLimit; +import com.checkout.issuing.controls.requests.VelocityWindow; +import com.checkout.issuing.controls.requests.VelocityWindowType; +import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; +import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +class ScratchDiscriminatorProbe { + + private final GsonSerializer serializer = new GsonSerializer(); + + private void probe(final String label, final Object value) { + try { + System.out.println("PROBE " + label + " => OK: " + serializer.toJson(value)); + } catch (final RuntimeException e) { + System.out.println("PROBE " + label + " => THROWS: " + e.getMessage()); + } + } + + private void probeRoundTrip(final String label, final String json, final Class type) { + try { + final Object parsed = serializer.fromJson(json, type); + probe(label, parsed); + } catch (final RuntimeException e) { + System.out.println("PROBE " + label + " => READ-THROWS: " + e.getMessage()); + } + } + + @Test + void probeAll() { + // 1. REQUEST path - issuing control groups + probe("CreateControlGroupRequest(request!)", CreateControlGroupRequest.builder() + .description("g") + .controls(Collections.singletonList(VelocityControlGroupControl.builder() + .description("c") + .velocityLimit(VelocityLimit.builder() + .amountLimit(1000) + .velocityWindow(VelocityWindow.builder().type(VelocityWindowType.DAILY).build()) + .build()) + .build())) + .build()); + + // 2. customers - List + probeRoundTrip("CustomerResponse.instruments", + "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\",\"fingerprint\":\"f\"}]}", + com.checkout.customers.CustomerResponse.class); + + // 3. workflows - List + List + probeRoundTrip("GetWorkflowResponse.actions+conditions", + "{\"id\":\"wf_1\",\"name\":\"n\"," + + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," + + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://x.test\"}]}", + com.checkout.workflows.GetWorkflowResponse.class); + + // 4. accounts payout schedule - ScheduleResponse recurrence + probeRoundTrip("CurrencySchedule.recurrence", + "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"daily\"}}", + com.checkout.accounts.payout.schedule.response.CurrencySchedule.class); + + // 5. issuing cardholder cards - List + probeRoundTrip("CardholderCardsResponse.cards", + "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", + com.checkout.issuing.cardholders.CardholderCardsResponse.class); + + // 6. issuing card controls query - List + probeRoundTrip("CardControlsQueryResponse.controls", + "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", + com.checkout.issuing.controls.responses.query.CardControlsQueryResponse.class); + + // 7. issuing control group response - List + probeRoundTrip("ControlGroupResponse.controls", + "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\",\"description\":\"d\"}]}", + com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse.class); + + // 8. the payments source hierarchy touched by this branch + probeRoundTrip("RequestAPaymentOrPayoutResponseCreated.source", + "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", + com.checkout.handlepaymentsandpayouts.payments.postpayments.responses + .requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated.class); + } +} diff --git a/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java b/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java new file mode 100644 index 000000000..e50ea3d4e --- /dev/null +++ b/src/test/java/com/checkout/common/PaymentMethodTypeSerializationTest.java @@ -0,0 +1,118 @@ +package com.checkout.common; + +import com.checkout.GsonSerializer; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Value-by-value serialization test for {@link PaymentMethodType}. + * + *

This enum is a consolidated union: it carries every payment method type the SDK sees across + * GET /payment-methods, the flow API and the payment session entities. The specification's + * PaymentMethod.type enum is therefore a subset of it, and the values this enum adds on top + * (applepay, card, googlepay, stored_card, wallet, bnpl, bank_redirects, and the APM values the + * flow API returns) are deliberate rather than invented. remember_me is supported by the API but + * deliberately unlisted in the public specification. + * + *

The full map below is the guard: GET /payment-methods maps a value it does not recognise to + * null rather than failing, so a missing constant is silent. bacs went missing that way. + */ +class PaymentMethodTypeSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeEveryValueBothDirections() { + wireValues().forEach((value, wire) -> { + assertEquals("\"" + wire + "\"", serializer.toJson(value), value.name()); + assertEquals(value, serializer.fromJson("\"" + wire + "\"", PaymentMethodType.class), wire); + }); + } + + @Test + void shouldSerializeBacsBothDirections() { + assertEquals("\"bacs\"", serializer.toJson(PaymentMethodType.BACS)); + assertEquals(PaymentMethodType.BACS, serializer.fromJson("\"bacs\"", PaymentMethodType.class)); + } + + private Map wireValues() { + final Map expected = new LinkedHashMap<>(); + expected.put(PaymentMethodType.ACCEL, "accel"); + expected.put(PaymentMethodType.ACH, "ach"); + expected.put(PaymentMethodType.ALIPAY_CN, "alipay_cn"); + expected.put(PaymentMethodType.ALIPAY_HK, "alipay_hk"); + expected.put(PaymentMethodType.ALIPAY_PLUS, "alipay_plus"); + expected.put(PaymentMethodType.ALMA, "alma"); + expected.put(PaymentMethodType.AMEX, "amex"); + expected.put(PaymentMethodType.APPLEPAY, "applepay"); + expected.put(PaymentMethodType.BACS, "bacs"); + expected.put(PaymentMethodType.BANCONTACT, "bancontact"); + expected.put(PaymentMethodType.BANK_REDIRECTS, "bank_redirects"); + expected.put(PaymentMethodType.BENEFIT, "benefit"); + expected.put(PaymentMethodType.BIZUM, "bizum"); + expected.put(PaymentMethodType.BLIK, "blik"); + expected.put(PaymentMethodType.BNPL, "bnpl"); + expected.put(PaymentMethodType.BOOST, "boost"); + expected.put(PaymentMethodType.BPI, "bpi"); + expected.put(PaymentMethodType.CARD, "card"); + expected.put(PaymentMethodType.CARD_SCHEME, "card_scheme"); + expected.put(PaymentMethodType.CARTES_BANCAIRES, "cartes_bancaires"); + expected.put(PaymentMethodType.CHINA_UNION_PAY, "china_union_pay"); + expected.put(PaymentMethodType.CONNECT_WALLET, "connect_wallet"); + expected.put(PaymentMethodType.DANA, "dana"); + expected.put(PaymentMethodType.DCI, "dci"); + expected.put(PaymentMethodType.DINERS, "diners"); + expected.put(PaymentMethodType.DISCOVER, "discover"); + expected.put(PaymentMethodType.EPS, "eps"); + expected.put(PaymentMethodType.GCASH, "gcash"); + expected.put(PaymentMethodType.GOOGLEPAY, "googlepay"); + expected.put(PaymentMethodType.IDEAL, "ideal"); + expected.put(PaymentMethodType.JCB, "jcb"); + expected.put(PaymentMethodType.KAKAOPAY, "kakaopay"); + expected.put(PaymentMethodType.KLARNA, "klarna"); + expected.put(PaymentMethodType.KNET, "knet"); + expected.put(PaymentMethodType.MADA, "mada"); + expected.put(PaymentMethodType.MASTERCARD, "mastercard"); + expected.put(PaymentMethodType.MBWAY, "mbway"); + expected.put(PaymentMethodType.MOBILEPAY, "mobilepay"); + expected.put(PaymentMethodType.MULTIBANCO, "multibanco"); + expected.put(PaymentMethodType.NYCE, "nyce"); + expected.put(PaymentMethodType.OCTOPUS, "octopus"); + expected.put(PaymentMethodType.OMANNET, "omannet"); + expected.put(PaymentMethodType.P24, "p24"); + expected.put(PaymentMethodType.PAYNOW, "paynow"); + expected.put(PaymentMethodType.PAYPAL, "paypal"); + expected.put(PaymentMethodType.PAYPAY, "paypay"); + expected.put(PaymentMethodType.PLAID, "plaid"); + expected.put(PaymentMethodType.PULSE, "pulse"); + expected.put(PaymentMethodType.QPAY, "qpay"); + expected.put(PaymentMethodType.RABBIT_LINE_PAY, "rabbit_line_pay"); + expected.put(PaymentMethodType.REMEMBER_ME, "remember_me"); + expected.put(PaymentMethodType.SEPA, "sepa"); + expected.put(PaymentMethodType.SEQURA, "sequra"); + expected.put(PaymentMethodType.SHAZAM, "shazam"); + expected.put(PaymentMethodType.SOFORT, "sofort"); + expected.put(PaymentMethodType.STAR, "star"); + expected.put(PaymentMethodType.STCPAY, "stcpay"); + expected.put(PaymentMethodType.STORED_CARD, "stored_card"); + expected.put(PaymentMethodType.SWISH, "swish"); + expected.put(PaymentMethodType.TABBY, "tabby"); + expected.put(PaymentMethodType.TAMARA, "tamara"); + expected.put(PaymentMethodType.TNG, "tng"); + expected.put(PaymentMethodType.TRUEMONEY, "truemoney"); + expected.put(PaymentMethodType.TWINT, "twint"); + expected.put(PaymentMethodType.UPI, "upi"); + expected.put(PaymentMethodType.VIPPS, "vipps"); + expected.put(PaymentMethodType.VISA, "visa"); + expected.put(PaymentMethodType.WALLET, "wallet"); + expected.put(PaymentMethodType.WECHATPAY, "wechatpay"); + + assertEquals(expected.size(), PaymentMethodType.values().length, + "every constant must be listed so the enum cannot grow without this test noticing"); + return expected; + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java index b2c6fb42b..dd3fe6796 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/payments/postpayments/responses/requestapaymentorpayoutresponsecreated/RequestAPaymentOrPayoutResponseCreatedSerializationTest.java @@ -7,6 +7,11 @@ import org.junit.jupiter.api.Test; import com.checkout.GsonSerializer; +import com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.alipaycnsource.AlipayCnSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.bacssource.BacsSource; +import com.checkout.handlepaymentsandpayouts.payments.common.source.bankaccountsource.BankAccountSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.currencyaccountsource.CurrencyAccountSource; import com.checkout.handlepaymentsandpayouts.payments.common.source.klarnasource.KlarnaSource; @@ -150,6 +155,67 @@ void shouldDeserializeSepaSource() { assertEquals("src_sepa_123", sepaSource.getId()); } + // ------------------------------------------------------------------ + // PaymentDeclinedSourceResponse - the ach, alipay_cn, bank_account, sepa + // and bacs branches of the PaymentResponseSource discriminator. The schema + // declares a type and an id, and requires both. + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeAlipayCnSourceWithId() { + AbstractSource source = declinedSourceOfType("alipay_cn", "src_alipay_cn_123"); + + assertInstanceOf(AlipayCnSource.class, source); + assertEquals("src_alipay_cn_123", ((AlipayCnSource) source).getId()); + } + + @Test + void shouldDeserializeAchSourceWithId() { + AbstractSource source = declinedSourceOfType("ach", "src_ach_123"); + + assertInstanceOf(AchSource.class, source); + assertEquals("src_ach_123", ((AchSource) source).getId()); + } + + @Test + void shouldDeserializeBacsSourceWithId() { + AbstractSource source = declinedSourceOfType("bacs", "src_bacs_123"); + + assertInstanceOf(BacsSource.class, source); + assertEquals("src_bacs_123", ((BacsSource) source).getId()); + } + + @Test + void shouldDeserializeBankAccountSourceWithId() { + AbstractSource source = declinedSourceOfType("bank_account", "src_bank_account_123"); + + assertInstanceOf(BankAccountSource.class, source); + assertEquals("src_bank_account_123", ((BankAccountSource) source).getId()); + } + + private AbstractSource declinedSourceOfType(final String type, final String sourceId) { + String json = "{\n" + + " \"id\": \"pay_123\",\n" + + " \"amount\": 1000,\n" + + " \"currency\": \"USD\",\n" + + " \"approved\": false,\n" + + " \"status\": \"Declined\",\n" + + " \"response_code\": \"20005\",\n" + + " \"processed_on\": \"2021-06-08T12:25:01Z\",\n" + + " \"source\": {\n" + + " \"type\": \"" + type + "\",\n" + + " \"id\": \"" + sourceId + "\"\n" + + " }\n" + + "}"; + + RequestAPaymentOrPayoutResponseCreated response = + serializer.fromJson(json, RequestAPaymentOrPayoutResponseCreated.class); + + assertNotNull(response); + assertNotNull(response.getSource()); + return response.getSource(); + } + @Test void shouldDeserializeProcessingSchemeTransactionLinkId() { String json = "{\n" + diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java index 20122bfa9..3ab15a4cb 100644 --- a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -10,6 +10,7 @@ import com.checkout.instruments.create.CreateInstrumentAchResponse; import com.checkout.instruments.get.GetAchInstrumentResponse; import com.checkout.instruments.update.AchInstrumentAccountType; +import com.checkout.instruments.update.UpdateInstrumentAchResponse; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -173,7 +174,7 @@ void shouldRoundTripRetrieveResponse() { } // ------------------------------------------------------------------ - // Store response + // Store and update responses // ------------------------------------------------------------------ @Test @@ -193,6 +194,23 @@ void shouldDeserializeStoreResponse() { assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); } + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"ach\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentAchResponse response = + serializer.fromJson(json, UpdateInstrumentAchResponse.class); + + assertEquals(com.checkout.common.InstrumentType.ACH, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } + @Test void shouldSerializeAchAccountTypeBothDirections() { assertEquals(2, AchInstrumentAccountType.values().length); diff --git a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java index 7102e0d5c..bf3bdbd7c 100644 --- a/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java +++ b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java @@ -7,19 +7,25 @@ import com.checkout.instruments.get.GetAchInstrumentResponse; import com.checkout.instruments.get.GetBacsInstrumentResponse; import com.checkout.instruments.get.GetInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentAchResponse; import com.checkout.instruments.update.UpdateInstrumentBacsResponse; import com.checkout.instruments.update.UpdateInstrumentResponse; +import com.checkout.instruments.update.UpdateInstrumentSepaResponse; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; /** - * Polymorphic dispatch tests for the Bacs Direct Debit and ACH instrument responses. + * Polymorphic dispatch tests for the Bacs Direct Debit, ACH and SEPA instrument responses. * *

Before these subtypes were registered on the three instrument factories, every assertion here * failed with a JsonParseException, because none of the factories declares a default subtype. ACH * was already registered on the update factory but not on create or get. + * + *

The update assertions also guard the id, which the specification declares on the sepa, ach and + * bacs update responses. It was once declared on the bacs variant only, and was silently dropped on + * the other two. */ class InstrumentResponseDispatchTest { @@ -46,9 +52,36 @@ void shouldDispatchUpdateResponseToBacsSubtype() { final UpdateInstrumentBacsResponse bacs = assertInstanceOf(UpdateInstrumentBacsResponse.class, response); assertEquals(com.checkout.common.InstrumentType.BACS, bacs.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", bacs.getId()); assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", bacs.getFingerprint()); } + @Test + void shouldDispatchUpdateResponseToSepaSubtype() { + final String json = "{\"type\":\"sepa\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentSepaResponse sepa = assertInstanceOf(UpdateInstrumentSepaResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.SEPA, sepa.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", sepa.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", sepa.getFingerprint()); + } + + @Test + void shouldDispatchUpdateResponseToAchSubtype() { + final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}"; + + final UpdateInstrumentResponse response = serializer.fromJson(json, UpdateInstrumentResponse.class); + + final UpdateInstrumentAchResponse ach = assertInstanceOf(UpdateInstrumentAchResponse.class, response); + assertEquals(com.checkout.common.InstrumentType.ACH, ach.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", ach.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", ach.getFingerprint()); + } + @Test void shouldDispatchCreateResponseToAchSubtype() { final String json = "{\"type\":\"ach\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," diff --git a/src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java b/src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java similarity index 67% rename from src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java rename to src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java index 019418593..c61f54bc6 100644 --- a/src/test/java/com/checkout/instruments/CreateInstrumentSepaRequestSerializationTest.java +++ b/src/test/java/com/checkout/instruments/SepaInstrumentSerializationTest.java @@ -8,7 +8,9 @@ import com.checkout.instruments.create.CreateCustomerInstrumentRequest; import com.checkout.instruments.create.CreateInstrumentSepaRequest; import com.checkout.instruments.create.InstrumentData; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; +import com.checkout.instruments.update.UpdateInstrumentSepaResponse; +import com.checkout.payments.request.source.apm.MandateType; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -18,10 +20,19 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -class CreateInstrumentSepaRequestSerializationTest { +/** + * Schema validation tests for the SEPA variants of the instruments endpoints. + */ +class SepaInstrumentSerializationTest { + + private static final String FINGERPRINT_PATTERN = "^([a-z0-9]{26})$"; private final GsonSerializer serializer = new GsonSerializer(); + // ------------------------------------------------------------------ + // StoreSepaInstrumentRequest + // ------------------------------------------------------------------ + @Test void shouldSerializeTypeAsSepa() { final CreateInstrumentSepaRequest request = CreateInstrumentSepaRequest.builder() @@ -49,7 +60,7 @@ void shouldSerializeInstrumentData() { .accoountNumber("DE89370400440532013000") .country(CountryCode.DE) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .firstName("Hans") @@ -63,6 +74,52 @@ void shouldSerializeInstrumentData() { assertTrue(json.contains("\"account_number\":\"DE89370400440532013000\"")); assertTrue(json.contains("\"country\":\"DE\"")); assertTrue(json.contains("\"currency\":\"EUR\"")); + assertTrue(json.contains("\"payment_type\":\"recurring\"")); + } + + /** + * The store request once typed paymentType as the generic payments payment type, whose + * constants serialize capitalized, so a SEPA instrument could not be created at all: the + * specification pins this field to recurring or regular in lowercase. No test asserted the wire + * value, which is why the defect survived. + */ + @Test + void shouldSerializePaymentTypeLowercaseOnTheStoreRequest() { + assertTrue(serializer.toJson(storeRequestWithPaymentType(SepaPaymentType.RECURRING)) + .contains("\"payment_type\":\"recurring\"")); + assertTrue(serializer.toJson(storeRequestWithPaymentType(SepaPaymentType.REGULAR)) + .contains("\"payment_type\":\"regular\"")); + } + + @Test + void shouldSerializeMandateType() { + final CreateInstrumentSepaRequest request = CreateInstrumentSepaRequest.builder() + .instrumentData(InstrumentData.builder() + .type(MandateType.B2B) + .accoountNumber("DE89370400440532013000") + .country(CountryCode.DE) + .currency(Currency.EUR) + .paymentType(SepaPaymentType.REGULAR) + .build()) + .build(); + + final String json = serializer.toJson(request); + final CreateInstrumentSepaRequest result = + serializer.fromJson(json, CreateInstrumentSepaRequest.class); + + assertTrue(json.contains("\"type\":\"B2B\"")); + assertEquals(MandateType.B2B, result.getInstrumentData().getType()); + } + + private CreateInstrumentSepaRequest storeRequestWithPaymentType(final SepaPaymentType paymentType) { + return CreateInstrumentSepaRequest.builder() + .instrumentData(InstrumentData.builder() + .accoountNumber("DE89370400440532013000") + .country(CountryCode.DE) + .currency(Currency.EUR) + .paymentType(paymentType) + .build()) + .build(); } @Test @@ -140,7 +197,7 @@ void shouldRoundTripSerialize() { .accoountNumber("ES9121000418450200051332") .country(CountryCode.ES) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .firstName("Carlos") @@ -191,4 +248,25 @@ void shouldHandleAbsentOptionalCustomer() { assertDoesNotThrow(() -> serializer.toJson(request)); assertNull(request.getCustomer()); } + + // ------------------------------------------------------------------ + // Update response + // ------------------------------------------------------------------ + + @Test + void shouldDeserializeUpdateResponse() { + final String json = "{" + + "\"type\":\"sepa\"," + + "\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"" + + "}"; + + final UpdateInstrumentSepaResponse response = + serializer.fromJson(json, UpdateInstrumentSepaResponse.class); + + assertEquals(com.checkout.common.InstrumentType.SEPA, response.getType()); + assertEquals("src_wmlfc3zyhqzehihu7giusaaawu", response.getId()); + assertEquals("vnsdrvikkvre3dtrjjvlm5du4q", response.getFingerprint()); + assertTrue(response.getFingerprint().matches(FINGERPRINT_PATTERN)); + } } diff --git a/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java b/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java index 0dc057fba..1bc684146 100644 --- a/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java +++ b/src/test/java/com/checkout/instruments/SepaInstrumentsTestIT.java @@ -9,8 +9,8 @@ import com.checkout.instruments.create.CreateInstrumentSepaRequest; import com.checkout.instruments.create.CreateInstrumentSepaResponse; import com.checkout.instruments.create.InstrumentData; +import com.checkout.instruments.update.SepaPaymentType; import com.checkout.payments.AbstractPaymentsTestIT; -import com.checkout.payments.PaymentType; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -25,7 +25,7 @@ void shouldCreateInstrumentSepa() { .accoountNumber("FR7630006000011234567890189") .country(CountryCode.FR) .currency(Currency.EUR) - .paymentType(PaymentType.RECURRING) + .paymentType(SepaPaymentType.RECURRING) .build()) .accountHolder(AccountHolder.builder() .type(AccountHolderType.INDIVIDUAL) diff --git a/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java index f7de6548c..20743951e 100644 --- a/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java +++ b/src/test/java/com/checkout/schema/LocalDateFieldsRegressionTest.java @@ -6,7 +6,7 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.customer.MerchantAccount; import com.checkout.handlepaymentsandpayouts.setups.entities.order.OrderSubMerchant; import com.checkout.instruments.create.InstrumentData; -import com.checkout.payments.PaymentType; +import com.checkout.instruments.update.SepaPaymentType; import com.checkout.payments.ProductRequest; import com.checkout.payments.ProductResponse; import com.checkout.payments.request.ItemSubType; @@ -205,7 +205,7 @@ void instrumentData_allProperties_roundTrip() { .accoountNumber("DE89370400440532013000") .country(CountryCode.DE) .currency(Currency.EUR) - .paymentType(PaymentType.REGULAR) + .paymentType(SepaPaymentType.REGULAR) .mandateId("MANDATE-XYZ-999") .dateOfSignature(LocalDate.of(2021, 3, 15)) .build(); @@ -217,7 +217,7 @@ void instrumentData_allProperties_roundTrip() { assertEquals("DE89370400440532013000", deserialized.getAccoountNumber()); assertEquals(CountryCode.DE, deserialized.getCountry()); assertEquals(Currency.EUR, deserialized.getCurrency()); - assertEquals(PaymentType.REGULAR, deserialized.getPaymentType()); + assertEquals(SepaPaymentType.REGULAR, deserialized.getPaymentType()); assertEquals("MANDATE-XYZ-999", deserialized.getMandateId()); assertEquals(LocalDate.of(2021, 3, 15), deserialized.getDateOfSignature()); } From 6319804b7ad60e9f385084159178ff988f2f9e3c Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 10:12:13 +0200 Subject: [PATCH 12/18] Java doc + clean up + serialization test --- .../CreateCustomerInstrumentRequest.java | 17 ++ .../create/CreateInstrumentRequest.java | 10 + .../create/CreateInstrumentSepaRequest.java | 3 + .../create/CreateInstrumentSepaResponse.java | 10 + .../get/GetCardInstrumentResponse.java | 9 + .../get/GetSepaInstrumentData.java | 2 + .../get/InstrumentCustomerResponse.java | 11 ++ .../update/AchInstrumentAccountType.java | 7 + .../instruments/update/SepaPaymentType.java | 10 + .../update/UpdateInstrumentAchRequest.java | 48 +++++ .../UpdateInstrumentBankAccountResponse.java | 10 + .../update/UpdateInstrumentCardResponse.java | 10 + .../update/UpdateInstrumentRequest.java | 11 ++ .../update/UpdateInstrumentSepaRequest.java | 11 ++ .../request/source/apm/MandateType.java | 5 + .../checkout/ScratchDiscriminatorProbe.java | 85 --------- ...morphicDiscriminatorSerializationTest.java | 179 ++++++++++++++++++ 17 files changed, 353 insertions(+), 85 deletions(-) delete mode 100644 src/test/java/com/checkout/ScratchDiscriminatorProbe.java create mode 100644 src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java diff --git a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java index 19d2ba6f6..c3a74006b 100644 --- a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java @@ -9,14 +9,31 @@ import lombok.NoArgsConstructor; import lombok.ToString; +/** + * The customer's details. Associates the instrument with an existing or new customer. + * + *

The email, name and phone number are inherited from {@link CustomerRequest}. The email is + * limited to max 255 characters and must be a valid email address, and the name to max 255 + * characters; the name and phone number are only applied when a new customer is created. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @NoArgsConstructor public final class CreateCustomerInstrumentRequest extends CustomerRequest { + /** + * The identifier of an existing customer. + * [Optional] + * ^(cus)_(\w{26})$ + */ private String id; + /** + * If true, this instrument will become the default for the customer. If a new customer is + * created as a result of this request, the instrument will automatically be the default. + * [Optional] + */ @SerializedName("default") private boolean defaultInstrument; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java index 76837d643..80bff3ed1 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentRequest.java @@ -3,9 +3,19 @@ import com.checkout.common.InstrumentType; import lombok.Data; +/** + * The shared properties of a payment instrument being stored. + * + *

Each concrete variant fixes the type in its constructor, which is what selects the schema the + * API validates the request against. + */ @Data public abstract class CreateInstrumentRequest { + /** + * The type of instrument. + * [Required] + */ protected final InstrumentType type; protected CreateInstrumentRequest(final InstrumentType type) { diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java index b8811d86f..e8c56a321 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaRequest.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Store SEPA mandate details. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java index 88279e9b9..f450ad1fe 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentSepaResponse.java @@ -11,11 +11,21 @@ import java.time.Instant; +/** + * Store SEPA mandate instrument response. + * + *

The id and the fingerprint are inherited from {@link CreateInstrumentResponse}. The + * fingerprint is required for this variant and matches the pattern {@code ^([a-z0-9]{26})$}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentSepaResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.SEPA; } diff --git a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java index 0ac264259..e6cd1e929 100644 --- a/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetCardInstrumentResponse.java @@ -9,11 +9,20 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Card instrument response. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetCardInstrumentResponse extends GetInstrumentResponse { + /** + * The underlying instrument type. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; /** diff --git a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java index 62caaef82..ac4e50b4f 100644 --- a/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java @@ -74,6 +74,8 @@ public final class GetSepaInstrumentData { * created, it may take up to five seconds for the generated date of signature to be available. * [Required] * Format: yyyy-MM-dd + * min 10 characters + * max 10 characters */ private LocalDate dateOfSignature; diff --git a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java index e752a3d0c..20e729704 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java @@ -6,11 +6,22 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * The customer that a retrieved payment instrument is associated with. + * + *

The id, email and name are inherited from {@link CustomerResponse}. That base class also + * carries a phone number, which the retrieve instrument customer schema does not declare, so it is + * always null here. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class InstrumentCustomerResponse extends CustomerResponse { + /** + * This will be true if this instrument is set as the default for the customer. + * [Optional] + */ @SerializedName("default") private boolean isDefault; diff --git a/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java b/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java index d52c76e9e..9456b487a 100644 --- a/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java +++ b/src/main/java/com/checkout/instruments/update/AchInstrumentAccountType.java @@ -2,6 +2,13 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of Direct Debit account of an ACH instrument. + * + *

Shared by the store, update and retrieve ACH instrument variants, which all declare the same + * two values. The package is historical: the enum was introduced with the update request and is now + * used by the create and get variants as well. + */ public enum AchInstrumentAccountType { @SerializedName("savings") diff --git a/src/main/java/com/checkout/instruments/update/SepaPaymentType.java b/src/main/java/com/checkout/instruments/update/SepaPaymentType.java index 97b55fd67..8689f561d 100644 --- a/src/main/java/com/checkout/instruments/update/SepaPaymentType.java +++ b/src/main/java/com/checkout/instruments/update/SepaPaymentType.java @@ -2,6 +2,16 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of payment for a SEPA instrument. + * + *

The wire values are lowercase, and the specification allows these two values only. The + * equivalent Bacs Direct Debit field is capitalized, so do not share one type between the two: + * reusing {@link com.checkout.instruments.BacsPaymentType} sends a value the API rejects. Do not + * replace this enum with {@link com.checkout.payments.PaymentType} either, whose constants + * serialize capitalized and which also accepts MOTO, Installment, PayLater and Unscheduled, none of + * which SEPA allows. + */ public enum SepaPaymentType { @SerializedName("recurring") diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java index d9f2148bd..8aa56e6a2 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java @@ -13,14 +13,27 @@ import lombok.Setter; import lombok.ToString; +/** + * Update ACH bank account details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentAchRequest extends UpdateInstrumentRequest { + /** + * The details of the bank account. + * [Optional] + */ private AchInstrumentData instrumentData; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; @Builder @@ -35,20 +48,55 @@ public UpdateInstrumentAchRequest() { super(InstrumentType.ACH); } + /** + * The details of the ACH account being updated. + * + *

Every property is optional on update. The shape is identical to the store and retrieve + * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per + * operation. + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public static final class AchInstrumentData { + /** + * The type of Direct Debit account. + * [Optional] + */ private AchInstrumentAccountType accountType; + /** + * The account number of the Direct Debit account. + * [Optional] + * min 4 characters + * max 17 characters + */ private String accountNumber; + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Optional] + * min 8 characters + * max 9 characters + */ private String bankCode; + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ private Currency currency; + /** + * The country of the account. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode country; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java index e85e737ca..3210312ac 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountResponse.java @@ -5,11 +5,21 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update bank account instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The specification gives + * this variant a type and a fingerprint only, with no id. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentBankAccountResponse extends UpdateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java index 61d77f8e3..4696ec0eb 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardResponse.java @@ -5,11 +5,21 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Update card instrument response. + * + *

The fingerprint is inherited from {@link UpdateInstrumentResponse}. The specification gives + * this variant a type and a fingerprint only, with no id. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentCardResponse extends UpdateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java index 96f6ff3d0..1ad9dc645 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentRequest.java @@ -3,9 +3,20 @@ import com.checkout.common.InstrumentType; import lombok.Data; +/** + * The shared properties of a payment instrument being updated. + * + *

Each concrete variant fixes the type in its constructor. The specification declares no + * required list on the update variants, but the type is the discriminator that selects which + * variant the API validates against, so it is always sent. + */ @Data public abstract class UpdateInstrumentRequest { + /** + * The type of instrument to be updated. + * [Required] + */ protected final InstrumentType type; protected UpdateInstrumentRequest(final InstrumentType type) { diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java index a3ac4c472..141eea27d 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentSepaRequest.java @@ -16,6 +16,11 @@ import java.time.LocalDate; +/** + * Update SEPA mandate details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @@ -46,6 +51,12 @@ public UpdateInstrumentSepaRequest() { super(InstrumentType.SEPA); } + /** + * The details of the SEPA mandate being updated. + * + *

The payment type is the SEPA enum, whose wire values are lowercase. The equivalent Bacs + * Direct Debit field is capitalized, so the two must not share a type. + */ @Data @Builder @NoArgsConstructor diff --git a/src/main/java/com/checkout/payments/request/source/apm/MandateType.java b/src/main/java/com/checkout/payments/request/source/apm/MandateType.java index 08e893098..07f4f3048 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/MandateType.java +++ b/src/main/java/com/checkout/payments/request/source/apm/MandateType.java @@ -2,6 +2,11 @@ import com.google.gson.annotations.SerializedName; +/** + * The type of SEPA mandate. + * + *

The wire values are capitalized, and the specification allows these two values only. + */ public enum MandateType { @SerializedName("Core") diff --git a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java b/src/test/java/com/checkout/ScratchDiscriminatorProbe.java deleted file mode 100644 index f20fac506..000000000 --- a/src/test/java/com/checkout/ScratchDiscriminatorProbe.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.checkout; - -import com.checkout.issuing.controls.requests.VelocityLimit; -import com.checkout.issuing.controls.requests.VelocityWindow; -import com.checkout.issuing.controls.requests.VelocityWindowType; -import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; -import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -class ScratchDiscriminatorProbe { - - private final GsonSerializer serializer = new GsonSerializer(); - - private void probe(final String label, final Object value) { - try { - System.out.println("PROBE " + label + " => OK: " + serializer.toJson(value)); - } catch (final RuntimeException e) { - System.out.println("PROBE " + label + " => THROWS: " + e.getMessage()); - } - } - - private void probeRoundTrip(final String label, final String json, final Class type) { - try { - final Object parsed = serializer.fromJson(json, type); - probe(label, parsed); - } catch (final RuntimeException e) { - System.out.println("PROBE " + label + " => READ-THROWS: " + e.getMessage()); - } - } - - @Test - void probeAll() { - // 1. REQUEST path - issuing control groups - probe("CreateControlGroupRequest(request!)", CreateControlGroupRequest.builder() - .description("g") - .controls(Collections.singletonList(VelocityControlGroupControl.builder() - .description("c") - .velocityLimit(VelocityLimit.builder() - .amountLimit(1000) - .velocityWindow(VelocityWindow.builder().type(VelocityWindowType.DAILY).build()) - .build()) - .build())) - .build()); - - // 2. customers - List - probeRoundTrip("CustomerResponse.instruments", - "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\",\"fingerprint\":\"f\"}]}", - com.checkout.customers.CustomerResponse.class); - - // 3. workflows - List + List - probeRoundTrip("GetWorkflowResponse.actions+conditions", - "{\"id\":\"wf_1\",\"name\":\"n\"," - + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," - + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://x.test\"}]}", - com.checkout.workflows.GetWorkflowResponse.class); - - // 4. accounts payout schedule - ScheduleResponse recurrence - probeRoundTrip("CurrencySchedule.recurrence", - "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"daily\"}}", - com.checkout.accounts.payout.schedule.response.CurrencySchedule.class); - - // 5. issuing cardholder cards - List - probeRoundTrip("CardholderCardsResponse.cards", - "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", - com.checkout.issuing.cardholders.CardholderCardsResponse.class); - - // 6. issuing card controls query - List - probeRoundTrip("CardControlsQueryResponse.controls", - "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", - com.checkout.issuing.controls.responses.query.CardControlsQueryResponse.class); - - // 7. issuing control group response - List - probeRoundTrip("ControlGroupResponse.controls", - "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\",\"description\":\"d\"}]}", - com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse.class); - - // 8. the payments source hierarchy touched by this branch - probeRoundTrip("RequestAPaymentOrPayoutResponseCreated.source", - "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", - com.checkout.handlepaymentsandpayouts.payments.postpayments.responses - .requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated.class); - } -} diff --git a/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java b/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java new file mode 100644 index 000000000..8309fedad --- /dev/null +++ b/src/test/java/com/checkout/schema/PolymorphicDiscriminatorSerializationTest.java @@ -0,0 +1,179 @@ +package com.checkout.schema; + +import com.checkout.GsonSerializer; +import com.checkout.accounts.payout.schedule.response.CurrencySchedule; +import com.checkout.customers.CustomerResponse; +import com.checkout.handlepaymentsandpayouts.payments.postpayments.responses.requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated; +import com.checkout.issuing.cardholders.CardholderCardsResponse; +import com.checkout.issuing.controls.requests.VelocityLimit; +import com.checkout.issuing.controls.requests.VelocityWindow; +import com.checkout.issuing.controls.requests.VelocityWindowType; +import com.checkout.issuing.controls.requests.controlgroup.CreateControlGroupRequest; +import com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl; +import com.checkout.issuing.controls.responses.controlgroup.ControlGroupResponse; +import com.checkout.issuing.controls.responses.query.CardControlsQueryResponse; +import com.checkout.workflows.GetWorkflowResponse; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression tests for polymorphic types nested inside a field or collection whose declared type is + * the abstract base. + * + *

Every polymorphic base in this SDK declares its discriminator as a real field, so that callers + * get a typed getter. The factories for these hierarchies are registered without + * {@code maintainType}, which means the factory owns the discriminator on the wire: it strips the + * field on read and re-injects it from the registered label on write. Upstream Gson refuses to + * serialize a subtype that already declares that field, so before the local modification in + * {@link com.google.gson.typeadapters.RuntimeTypeAdapterFactory} every case below failed with + * + *

   {@code
+ *   JsonParseException: cannot serialize  because it already defines a field named type
+ * }
+ * + *

Deserialization was never affected, which is why the SDK's response handling hid the defect. + * The write path is reached only when the declared type is the base, so serializing a subtype + * directly always worked and the failure surfaced only through the containers exercised here. + * {@link CreateControlGroupRequest} is the case that mattered most: it is an outbound request body, + * so the exception was thrown before the HTTP call and made the endpoint unusable. + * + *

Each test asserts the discriminator survives exactly once, with the value of the registered + * label, and that no other property is lost in the round trip. + */ +class PolymorphicDiscriminatorSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ------------------------------------------------------------------ + // Outbound request bodies + // ------------------------------------------------------------------ + + @Test + void shouldSerializeControlGroupRequestWithPolymorphicControls() { + final CreateControlGroupRequest request = CreateControlGroupRequest.builder() + .description("Velocity control group") + .controls(Collections.singletonList(VelocityControlGroupControl.builder() + .description("Daily spend cap") + .velocityLimit(VelocityLimit.builder() + .amountLimit(1000) + .velocityWindow(VelocityWindow.builder() + .type(VelocityWindowType.DAILY) + .build()) + .build()) + .build())) + .build(); + + final String json = serializer.toJson(request); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"description\":\"Velocity control group\"")); + assertTrue(json.contains("\"description\":\"Daily spend cap\"")); + assertTrue(json.contains("\"amount_limit\":1000")); + assertTrue(json.contains("\"type\":\"daily\"")); + } + + // ------------------------------------------------------------------ + // Response bodies, round-tripped + // ------------------------------------------------------------------ + + @Test + void shouldRoundTripPaymentResponseWithPolymorphicSource() { + final String json = roundTrip( + "{\"id\":\"pay_1\",\"source\":{\"type\":\"ach\",\"id\":\"src_ach_1\"}}", + RequestAPaymentOrPayoutResponseCreated.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"id\":\"src_ach_1\"")); + } + + @Test + void shouldRoundTripCustomerResponseWithPolymorphicInstruments() { + final String json = roundTrip( + "{\"id\":\"cus_1\",\"instruments\":[{\"type\":\"card\",\"id\":\"src_1\"," + + "\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"}]}", + CustomerResponse.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"card\"")); + assertTrue(json.contains("\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\"")); + } + + @Test + void shouldRoundTripWorkflowResponseWithPolymorphicActionsAndConditions() { + final String json = roundTrip( + "{\"id\":\"wf_1\",\"name\":\"n\"," + + "\"conditions\":[{\"type\":\"event\",\"events\":{}}]," + + "\"actions\":[{\"type\":\"webhook\",\"url\":\"https://example.test\"}]}", + GetWorkflowResponse.class); + + assertEquals(2, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"event\"")); + assertTrue(json.contains("\"type\":\"webhook\"")); + assertTrue(json.contains("\"url\":\"https://example.test\"")); + } + + @Test + void shouldRoundTripCurrencyScheduleWithPolymorphicRecurrence() { + final String json = roundTrip( + "{\"enabled\":true,\"threshold\":100,\"recurrence\":{\"frequency\":\"Daily\"}}", + CurrencySchedule.class); + + assertEquals(1, occurrences(json, "\"frequency\"")); + assertTrue(json.contains("\"frequency\":\"Daily\"")); + assertTrue(json.contains("\"threshold\":100")); + } + + @Test + void shouldRoundTripCardholderCardsWithPolymorphicCardDetails() { + final String json = roundTrip( + "{\"cards\":[{\"type\":\"virtual\",\"id\":\"crd_1\"}]}", + CardholderCardsResponse.class); + + assertEquals(1, occurrences(json, "\"type\"")); + assertTrue(json.contains("\"type\":\"virtual\"")); + assertTrue(json.contains("\"id\":\"crd_1\"")); + } + + @Test + void shouldRoundTripCardControlsQueryWithPolymorphicControls() { + final String json = roundTrip( + "{\"controls\":[{\"control_type\":\"velocity_limit\",\"id\":\"ctr_1\"}]}", + CardControlsQueryResponse.class); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"id\":\"ctr_1\"")); + } + + @Test + void shouldRoundTripControlGroupResponseWithPolymorphicControls() { + final String json = roundTrip( + "{\"id\":\"cg_1\",\"controls\":[{\"control_type\":\"velocity_limit\"," + + "\"description\":\"Daily spend cap\"}]}", + ControlGroupResponse.class); + + assertEquals(1, occurrences(json, "\"control_type\"")); + assertTrue(json.contains("\"control_type\":\"velocity_limit\"")); + assertTrue(json.contains("\"description\":\"Daily spend cap\"")); + } + + private String roundTrip(final String json, final Class type) { + return serializer.toJson(serializer.fromJson(json, type)); + } + + private int occurrences(final String json, final String needle) { + int count = 0; + int from = json.indexOf(needle); + while (from != -1) { + count++; + from = json.indexOf(needle, from + needle.length()); + } + return count; + } +} From 35c64848fae0c760cfd10bfbe2e60d3b1eb52068 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 11:00:13 +0200 Subject: [PATCH 13/18] Java doc + some fixes --- .../java/com/checkout/GsonSerializer.java | 21 +-- .../instruments/InstrumentsClient.java | 82 +++++++++++ .../instruments/InstrumentsClientImpl.java | 3 + .../CreateInstrumentBankAccountRequest.java | 58 ++++++++ .../CreateInstrumentBankAccountResponse.java | 31 ++++ .../create/CreateInstrumentTokenRequest.java | 16 +++ .../create/CreateInstrumentTokenResponse.java | 70 +++++++++ .../instruments/get/BankAccountField.java | 51 +++++++ .../get/BankAccountFieldAllowedOption.java | 11 ++ .../get/BankAccountFieldDependency.java | 11 ++ .../get/BankAccountFieldQuery.java | 13 ++ .../get/BankAccountFieldResponse.java | 7 + .../instruments/get/BankAccountSection.java | 11 ++ .../get/GetBankAccountInstrumentResponse.java | 52 +++++++ .../get/InstrumentNetworkToken.java | 3 + .../instruments/get/NetworkTokenState.java | 3 + .../instruments/get/PaymentNetwork.java | 3 + .../UpdateInstrumentBankAccountRequest.java | 62 +++++++- .../update/UpdateInstrumentCardRequest.java | 30 ++++ .../update/UpdateInstrumentTokenRequest.java | 11 ++ .../request/source/apm/RequestAchSource.java | 3 + .../source/apm/RequestAfterPaySource.java | 6 + .../source/apm/RequestAlipayCnSource.java | 3 + .../source/apm/RequestAlipayHkSource.java | 3 + .../source/apm/RequestAlipayPlusSource.java | 3 + .../request/source/apm/RequestAlmaSource.java | 3 + .../source/apm/RequestBancontactSource.java | 3 + .../source/apm/RequestBenefitSource.java | 6 + .../source/apm/RequestBizumSource.java | 3 + .../source/apm/RequestCvConnectSource.java | 6 + .../request/source/apm/RequestDanaSource.java | 3 + .../request/source/apm/RequestEpsSource.java | 3 + .../source/apm/RequestFawrySource.java | 3 + .../source/apm/RequestGcashSource.java | 3 + .../source/apm/RequestGiropaySource.java | 6 + .../source/apm/RequestIdealSource.java | 3 + .../source/apm/RequestIllicadoSource.java | 6 + .../source/apm/RequestKakaopaySource.java | 3 + .../source/apm/RequestKlarnaSource.java | 6 + .../request/source/apm/RequestKnetSource.java | 3 + .../source/apm/RequestMbwaySource.java | 3 + .../source/apm/RequestMobilePaySource.java | 3 + .../source/apm/RequestMultiBancoSource.java | 3 + .../source/apm/RequestOctopusSource.java | 5 + .../request/source/apm/RequestP24Source.java | 3 + .../source/apm/RequestPayPalSource.java | 6 + .../source/apm/RequestPlaidSource.java | 3 + .../source/apm/RequestPostFinanceSource.java | 6 + .../request/source/apm/RequestQPaySource.java | 3 + .../request/source/apm/RequestSepaSource.java | 7 + .../source/apm/RequestSequraSource.java | 3 + .../source/apm/RequestStcPaySource.java | 3 + .../source/apm/RequestSwishSource.java | 3 + .../source/apm/RequestTamaraSource.java | 3 + .../request/source/apm/RequestTngSource.java | 3 + .../source/apm/RequestTruemoneySource.java | 3 + .../source/apm/RequestTwintSource.java | 3 + .../source/apm/RequestVippsSource.java | 3 + .../source/apm/RequestWeChatPaySource.java | 3 + .../source/AbstractResponseSource.java | 12 ++ .../AlternativePaymentSourceResponse.java | 9 ++ .../response/source/CardResponseSource.java | 135 +++++++++++++++++- .../source/CurrencyAccountResponseSource.java | 15 ++ .../response/source/PayPalResponseSource.java | 8 ++ .../response/source/ResponseSource.java | 11 ++ .../RuntimeTypeAdapterFactory.java | 7 +- .../InstrumentRequestTypeTest.java | 62 ++++++++ 67 files changed, 933 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 5e09d4b75..42ee1603c 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -112,7 +112,8 @@ public final class GsonSerializer implements Serializer { .registerTypeAdapterFactory( RuntimeTypeAdapterFactory.of( com.checkout.handlepaymentsandpayouts.payments.common.source.AbstractSource.class, - CheckoutUtils.TYPE + CheckoutUtils.TYPE, + true ) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.cardsource.CardSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.CARD)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.achsource.AchSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.ACH)) @@ -187,47 +188,47 @@ public final class GsonSerializer implements Serializer { .registerSubtype(com.checkout.payments.sender.PaymentIndividualSender.class, identifier(SenderType.INDIVIDUAL)) .registerSubtype(com.checkout.payments.sender.PaymentInstrumentSender.class, identifier(SenderType.INSTRUMENT))) // Instruments CS2 - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.create.CreateInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentTokenResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentBacsResponse.class, identifier(InstrumentType.BACS)) .registerSubtype(com.checkout.instruments.create.CreateInstrumentAchResponse.class, identifier(InstrumentType.ACH))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.get.GetBankAccountInstrumentResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.get.GetCardInstrumentResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.get.GetSepaInstrumentResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.get.GetBacsInstrumentResponse.class, identifier(InstrumentType.BACS)) .registerSubtype(com.checkout.instruments.get.GetAchInstrumentResponse.class, identifier(InstrumentType.ACH))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBankAccountResponse.class, identifier(InstrumentType.BANK_ACCOUNT)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentCardResponse.class, identifier(InstrumentType.CARD)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentSepaResponse.class, identifier(InstrumentType.SEPA)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentAchResponse.class, identifier(InstrumentType.ACH)) .registerSubtype(com.checkout.instruments.update.UpdateInstrumentBacsResponse.class, identifier(InstrumentType.BACS))) // Workflows CS2 - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.actions.response.WorkflowActionResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.workflows.actions.response.WebhookWorkflowActionResponse.class, identifier(WorkflowActionType.WEBHOOK))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.conditions.response.WorkflowConditionResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.workflows.conditions.response.WorkflowConditionResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(com.checkout.workflows.conditions.response.EventWorkflowConditionResponse.class, identifier(WorkflowConditionType.EVENT)) .registerSubtype(com.checkout.workflows.conditions.response.EntityWorkflowConditionResponse.class, identifier(WorkflowConditionType.ENTITY)) .registerSubtype(com.checkout.workflows.conditions.response.ProcessingChannelWorkflowConditionResponse.class, identifier(WorkflowConditionType.PROCESSING_CHANNEL))) // Accounts CS2 - PayoutSchedules .registerTypeAdapter(GetScheduleResponse.class, getScheduleResponseDeserializer()) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.accounts.payout.schedule.response.ScheduleResponse.class, CheckoutUtils.FREQUENCY) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.accounts.payout.schedule.response.ScheduleResponse.class, CheckoutUtils.FREQUENCY, true) .registerSubtype(com.checkout.accounts.payout.schedule.response.ScheduleFrequencyDailyResponse.class, CheckoutUtils.DAILY) .registerSubtype(com.checkout.accounts.payout.schedule.response.ScheduleFrequencyWeeklyResponse.class, CheckoutUtils.WEEKLY) .registerSubtype(ScheduleFrequencyMonthlyResponse.class, CheckoutUtils.MONTHLY)) // Issuing CS2 - CardDetailsResponse - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardDetailsResponse.class, CheckoutUtils.TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardDetailsResponse.class, CheckoutUtils.TYPE, true) .registerSubtype(PhysicalCardDetailsResponse.class, identifier(CardType.PHYSICAL)) .registerSubtype(VirtualCardDetailsResponse.class, identifier(CardType.VIRTUAL))) // Issuing CS2 - CardControlsResponse - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardControlResponse.class, CheckoutUtils.CONTROL_TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(CardControlResponse.class, CheckoutUtils.CONTROL_TYPE, true) .registerSubtype(VelocityCardControlResponse.class, identifier(ControlType.VELOCITY_LIMIT)) .registerSubtype(MccCardControlResponse.class, identifier(ControlType.MCC_LIMIT))) // Issuing CS2 - ControlGroupControl - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.issuing.controls.requests.controlgroup.ControlGroupControl.class, CheckoutUtils.CONTROL_TYPE) + .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.issuing.controls.requests.controlgroup.ControlGroupControl.class, CheckoutUtils.CONTROL_TYPE, true) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.VelocityControlGroupControl.class, identifier(ControlType.VELOCITY_LIMIT)) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.MccControlGroupControl.class, identifier(ControlType.MCC_LIMIT)) .registerSubtype(com.checkout.issuing.controls.requests.controlgroup.MidControlGroupControl.class, identifier(ControlType.MID_LIMIT))) diff --git a/src/main/java/com/checkout/instruments/InstrumentsClient.java b/src/main/java/com/checkout/instruments/InstrumentsClient.java index 7d89fdd7e..338841152 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClient.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClient.java @@ -13,14 +13,51 @@ import java.util.concurrent.CompletableFuture; +/** + * The payment instruments client. + * + *

The create, update and retrieve operations are polymorphic: the concrete request type selects + * the instrument variant, and the response deserializes to the matching concrete type. Cast or + * parameterize to the variant you sent. + */ public interface InstrumentsClient { + /** + * Stores a payment instrument. + * + * @param createInstrumentRequest the instrument details, as one of the concrete variants of + * {@link CreateInstrumentRequest}. + * @param the concrete response variant matching the request type. + * @return a {@link CompletableFuture} that resolves to the stored instrument. + */ CompletableFuture create(CreateInstrumentRequest createInstrumentRequest); + /** + * Retrieves a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @return a {@link CompletableFuture} that resolves to the instrument, as one of the concrete + * variants of {@link GetInstrumentResponse}. + */ CompletableFuture get(String instrumentId); + /** + * Updates a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @param updateInstrumentRequest the properties to update, as one of the concrete variants of + * {@link UpdateInstrumentRequest}. + * @param the concrete response variant matching the request type. + * @return a {@link CompletableFuture} that resolves to the updated instrument. + */ CompletableFuture update(String instrumentId, UpdateInstrumentRequest updateInstrumentRequest); + /** + * Deletes a payment instrument. + * + * @param instrumentId the payment instrument ID. + * @return a {@link CompletableFuture} that resolves to an empty response on success. + */ CompletableFuture delete(String instrumentId); /** @@ -32,15 +69,51 @@ public interface InstrumentsClient { */ CompletableFuture revoke(String instrumentId); + /** + * Retrieves the bank account field formatting requirements for a country and currency. + * + * @param country the two-letter ISO country code. + * @param currency the three-letter ISO currency code. + * @param query the optional filters on account holder type and payment network. + * @return a {@link CompletableFuture} that resolves to the required field sections. + */ CompletableFuture getBankAccountFieldFormatting(CountryCode country, Currency currency, BankAccountFieldQuery query); // Synchronous methods + + /** + * Synchronous variant of {@link #create(CreateInstrumentRequest)}. + * + * @param createInstrumentRequest the instrument details. + * @param the concrete response variant matching the request type. + * @return the stored instrument. + */ T createSync(CreateInstrumentRequest createInstrumentRequest); + /** + * Synchronous variant of {@link #get(String)}. + * + * @param instrumentId the payment instrument ID. + * @return the instrument. + */ GetInstrumentResponse getSync(String instrumentId); + /** + * Synchronous variant of {@link #update(String, UpdateInstrumentRequest)}. + * + * @param instrumentId the payment instrument ID. + * @param updateInstrumentRequest the properties to update. + * @param the concrete response variant matching the request type. + * @return the updated instrument. + */ T updateSync(String instrumentId, UpdateInstrumentRequest updateInstrumentRequest); + /** + * Synchronous variant of {@link #delete(String)}. + * + * @param instrumentId the payment instrument ID. + * @return an empty response on success. + */ EmptyResponse deleteSync(String instrumentId); /** @@ -51,5 +124,14 @@ public interface InstrumentsClient { */ EmptyResponse revokeSync(String instrumentId); + /** + * Synchronous variant of + * {@link #getBankAccountFieldFormatting(CountryCode, Currency, BankAccountFieldQuery)}. + * + * @param country the two-letter ISO country code. + * @param currency the three-letter ISO currency code. + * @param query the optional filters on account holder type and payment network. + * @return the required field sections. + */ BankAccountFieldResponse getBankAccountFieldFormattingSync(CountryCode country, Currency currency, BankAccountFieldQuery query); } diff --git a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java index 54934fb31..48684137d 100644 --- a/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java +++ b/src/main/java/com/checkout/instruments/InstrumentsClientImpl.java @@ -20,6 +20,9 @@ import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; +/** + * The default {@link InstrumentsClient} implementation. + */ public class InstrumentsClientImpl extends AbstractClient implements InstrumentsClient { private static final String INSTRUMENTS_PATH = "instruments"; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java index d038b3fbc..23c34edbe 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountRequest.java @@ -12,36 +12,94 @@ import lombok.Setter; import lombok.ToString; +/** + * Store bank account details. + * + *

The bank_account instrument type only supports payouts. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentBankAccountRequest extends CreateInstrumentRequest { + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Required] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Required] + */ private CountryCode country; + /** + * The ID of the primary processing channel this instrument is intended to be used for. + * [Optional] + */ private String processingChannelId; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ private CreateCustomerInstrumentRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java index b6e6bd706..ce59414b2 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java @@ -6,21 +6,52 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Store bank account instrument response. + * + *

The id, the fingerprint and the customer are inherited from + * {@link CreateInstrumentResponse}. This is one of only two store responses the specification + * declares a customer on, the other being {@link CreateInstrumentTokenResponse}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentBankAccountResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java index bc3b8b628..7372905e5 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenRequest.java @@ -8,16 +8,32 @@ import lombok.Setter; import lombok.ToString; +/** + * Store token details. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentTokenRequest extends CreateInstrumentRequest { + /** + * The Checkout.com token. + * [Required] + * ^(tok)_(\w{26})$|^(card_tok)_(\w{12})$ + */ private String token; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * The customer's details. Associates the instrument with an existing or new customer. + * [Optional] + */ private CreateCustomerInstrumentRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java index 90127b0ce..0318860c0 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java @@ -8,35 +8,105 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Card instrument response. + * + *

The id, the fingerprint and the customer are inherited from + * {@link CreateInstrumentResponse}. The instrument type is card rather than token: the + * specification maps the token store request onto a card store response. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CreateInstrumentTokenResponse extends CreateInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.CARD; + /** + * The expiry month. + * [Required] + * max 2 characters + * min 1 + */ private Integer expiryMonth; + /** + * The expiry year. + * [Required] + * min 4 characters + * max 4 characters + */ private Integer expiryYear; + /** + * The card scheme. + * [Optional] + */ private String scheme; + /** + * The local co-branded card scheme. + * [Optional] + * Enum: "cartes_bancaires" + */ private String schemeLocal; + /** + * The last four digits of the card number. + * [Required] + * min 4 characters + * max 4 characters + */ private String last4; + /** + * The card issuer's bank identification number (BIN). + * [Required] + */ private String bin; + /** + * The card type. + * [Optional] + * Enum: "CREDIT" "DEBIT" "PREPAID" "CHARGE" + */ private CardType cardType; + /** + * The card category. + * [Optional] + * Enum: "CONSUMER" "COMMERCIAL" + */ private CardCategory cardCategory; + /** + * The name of the card issuer. + * [Optional] + */ private String issuer; + /** + * The card issuer's country, as a two-letter ISO code. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode issuerCountry; + /** + * The issuer or card scheme product identifier. + * [Optional] + */ private String productId; + /** + * The issuer or card scheme product type. + * [Optional] + */ private String productType; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountField.java b/src/main/java/com/checkout/instruments/get/BankAccountField.java index beb0a4b73..62b42919b 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountField.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountField.java @@ -5,30 +5,81 @@ import java.util.List; +/** + * A bank account field to collect. + */ @Data public final class BankAccountField { + /** + * The field identifier. + * [Required] + */ private String id; + /** + * The section to display the field in. + * [Optional] + */ private String section; + /** + * The field's display name. + * [Required] + */ private String display; + /** + * The help text that explains the purpose of the field. + * [Optional] + */ private String helpText; + /** + * The type of field. + * [Required] + */ private String type; + /** + * Whether the field is required. + * [Required] + */ private boolean required; + /** + * A regular expression that can be used to validate the input of the field. + * [Optional] + */ private String validationRegex; + /** + * The minimum length of the field. + * [Optional] + */ private int minLength; + /** + * The maximum length of the field. + * [Optional] + * + *

The Java field name is lowercase where the neighbouring minLength is camel case. The wire + * name is correct because of the explicit annotation, so this is a source-only defect; + * renaming the field is breaking and is deferred to the next major version. + */ @SerializedName("max_length") private int maxlength; + /** + * The allowed options for the field. + * [Optional] + */ private List allowedOptions; + /** + * The field's dependencies. + * [Optional] + */ private List dependencies; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java index 49ede5ac6..52283fa6f 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldAllowedOption.java @@ -2,11 +2,22 @@ import lombok.Data; +/** + * An allowed option for a bank account field. + */ @Data public final class BankAccountFieldAllowedOption { + /** + * The option identifier. + * [Optional] + */ private String id; + /** + * The option display value. + * [Optional] + */ private String display; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java index 3d3daa532..b3a6ced1c 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldDependency.java @@ -2,11 +2,22 @@ import lombok.Data; +/** + * A dependency that controls whether a bank account field is displayed. + */ @Data public final class BankAccountFieldDependency { + /** + * The field identifier. + * [Optional] + */ private String fieldId; + /** + * The value of the dependent field that must match in order for this field to be displayed. + * [Optional] + */ private String value; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java index fbcb79ad7..73414039d 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldQuery.java @@ -5,13 +5,26 @@ import lombok.Builder; import lombok.Data; +/** + * The optional query parameters for the bank account field formatting request. + */ @Data @Builder public final class BankAccountFieldQuery { + /** + * The type of account holder that will be used to filter the fields returned. + * [Optional] + * Enum: "individual" "corporate" "government" + */ @SerializedName("account-holder-type") private AccountHolderType accountHolderType; + /** + * The banking network that will be used to filter the fields returned. + * [Optional] + * Enum: "local" "sepa" "fps" "ach" "fedwire" "swift" + */ @SerializedName("payment-network") private PaymentNetwork paymentNetwork; diff --git a/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java b/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java index fda7168c7..1bd5505ec 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountFieldResponse.java @@ -6,10 +6,17 @@ import java.util.List; +/** + * The bank account field formatting requirements for a country and currency. + */ @Data @EqualsAndHashCode(callSuper = true) public final class BankAccountFieldResponse extends HttpMetadata { + /** + * The sections of fields to collect. + * [Optional] + */ private List sections; } diff --git a/src/main/java/com/checkout/instruments/get/BankAccountSection.java b/src/main/java/com/checkout/instruments/get/BankAccountSection.java index 570020880..21d28c4bc 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountSection.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountSection.java @@ -4,11 +4,22 @@ import java.util.List; +/** + * A section of bank account fields to collect. + */ @Data public final class BankAccountSection { + /** + * The name of the section. + * [Required] + */ private String name; + /** + * The fields to collect in this section. + * [Optional] + */ private List fields; } diff --git a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java index a91428ed5..9d38893a5 100644 --- a/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/get/GetBankAccountInstrumentResponse.java @@ -10,31 +10,83 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Bank account details. + * + *

The id and the fingerprint are inherited from {@link GetInstrumentResponse}. This is the only + * retrieve variant whose account holder is the shared {@link AccountHolder} type; the card, SEPA, + * ACH and Bacs Direct Debit variants each declare their own shape. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class GetBankAccountInstrumentResponse extends GetInstrumentResponse { + /** + * The type of instrument. + * [Required] + */ private final InstrumentType type = InstrumentType.BANK_ACCOUNT; + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Required] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Required] + */ private CountryCode country; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; /** diff --git a/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java b/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java index 740fccc54..471c9e6c3 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentNetworkToken.java @@ -2,6 +2,9 @@ import lombok.Data; +/** + * The network token provisioned for a stored card instrument. + */ @Data public final class InstrumentNetworkToken { diff --git a/src/main/java/com/checkout/instruments/get/NetworkTokenState.java b/src/main/java/com/checkout/instruments/get/NetworkTokenState.java index 5871e9bd8..168a47c55 100644 --- a/src/main/java/com/checkout/instruments/get/NetworkTokenState.java +++ b/src/main/java/com/checkout/instruments/get/NetworkTokenState.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The state of a network token. + */ public enum NetworkTokenState { @SerializedName("active") diff --git a/src/main/java/com/checkout/instruments/get/PaymentNetwork.java b/src/main/java/com/checkout/instruments/get/PaymentNetwork.java index 39707e17e..5070f04d8 100644 --- a/src/main/java/com/checkout/instruments/get/PaymentNetwork.java +++ b/src/main/java/com/checkout/instruments/get/PaymentNetwork.java @@ -2,6 +2,9 @@ import com.google.gson.annotations.SerializedName; +/** + * The banking network used to filter the bank account fields returned. + */ public enum PaymentNetwork { @SerializedName("local") diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java index d72a6ff50..f026d6ff5 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBankAccountRequest.java @@ -13,36 +13,94 @@ import lombok.Setter; import lombok.ToString; +/** + * Update bank account details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentBankAccountRequest extends UpdateInstrumentRequest { + /** + * The type of account. + * [Optional] + * Enum: "savings" "current" "cash" + */ private AccountType accountType; + /** + * Number (which can contain letters) that identifies the account. + * [Optional] + */ private String accountNumber; + /** + * Code that identifies the bank. + * [Optional] + */ private String bankCode; + /** + * Code that identifies the bank branch. + * [Optional] + */ private String branchCode; + /** + * Internationally agreed standard for identifying bank account. + * [Optional] + */ private String iban; + /** + * The combination of bank code and/or branch code and account number. + * [Optional] + */ private String bban; + /** + * 8 or 11 character code which identifies the bank or bank branch. + * [Optional] + */ private String swiftBic; + /** + * The three-letter ISO currency code of the account's currency. + * [Optional] + */ private Currency currency; + /** + * The two-letter ISO country code of where the account is based. + * [Optional] + */ private CountryCode country; + /** + * The ID of the primary processing channel this instrument is intended to be used for. + * [Optional] + */ private String processingChannelId; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * Details of the bank. + * [Optional] + */ private BankDetails bank; + /** + * The customer's details. + * [Optional] + */ private UpdateCustomerRequest customer; @Builder @@ -59,7 +117,7 @@ private UpdateInstrumentBankAccountRequest(final AccountType accountType, final AccountHolder accountHolder, final BankDetails bank, final UpdateCustomerRequest customer) { - super(InstrumentType.TOKEN); + super(InstrumentType.BANK_ACCOUNT); this.accountType = accountType; this.accountNumber = accountNumber; this.bankCode = bankCode; @@ -76,7 +134,7 @@ private UpdateInstrumentBankAccountRequest(final AccountType accountType, } public UpdateInstrumentBankAccountRequest() { - super(InstrumentType.TOKEN); + super(InstrumentType.BANK_ACCOUNT); } } diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java index c23723cc7..ed61b3e47 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentCardRequest.java @@ -9,20 +9,50 @@ import lombok.Setter; import lombok.ToString; +/** + * Update card details. + * + *

Nothing in this request is required by the specification. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentCardRequest extends UpdateInstrumentRequest { + /** + * The expiry month of the card. + * [Optional] + * min 1 characters + * max 2 characters + * min 1 + */ private Integer expiryMonth; + /** + * The expiry year of the card. + * [Optional] + * min 4 characters + * max 4 characters + */ private Integer expiryYear; + /** + * Name of the cardholder. + * [Optional] + */ private String name; + /** + * The account holder details. + * [Optional] + */ private AccountHolder accountHolder; + /** + * The customer's details. + * [Optional] + */ private UpdateCustomerRequest customer; @Builder diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java index 9d69af90f..6fed07a0d 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentTokenRequest.java @@ -7,12 +7,23 @@ import lombok.Setter; import lombok.ToString; +/** + * Update an instrument from a Checkout.com token. + * + *

The current specification's update discriminator declares card, bank_account, sepa, ach and + * bacs only, so there is no token schema to align this request against. It is retained for + * backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class UpdateInstrumentTokenRequest extends UpdateInstrumentRequest { + /** + * The Checkout.com token. + * [Optional] + */ private String token; @Builder diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java index 10a68a156..e053baecc 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java @@ -11,6 +11,9 @@ import lombok.Setter; import lombok.ToString; +/** + * An ach payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java index 9edea45c4..217a9d87d 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAfterPaySource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * Afterpay source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java index ff8ce7380..a85d24de7 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayCnSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay CN source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestAlipayCnSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java index 342f6d6e7..ebc895a54 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayHkSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay HK source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestAlipayHkSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java index aae882d06..7943334c9 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlipayPlusSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Alipay Plus source. + */ @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) public final class RequestAlipayPlusSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java index 06b6b728d..51d7b8893 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAlmaSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Alma payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java index 4e09d30c2..885d7f016 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBancontactSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Bancontact source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java index aebf95644..6c7edf1b5 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBenefitSource.java @@ -5,6 +5,12 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Benefit source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestBenefitSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java index 180f0052e..935461066 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestBizumSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Bizum source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java index c2481d806..59990e724 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestCvConnectSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * CVConnect source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java index 9be03296f..4b09c5aa0 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestDanaSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Dana source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestDanaSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java index 69fb81c6c..250604479 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestEpsSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * EPS source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java index 5a9e44c10..00f3e3a82 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestFawrySource.java @@ -15,6 +15,9 @@ import java.time.Instant; import java.util.List; +/** + * Fawry source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java index 15ec2bc47..d659044f4 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestGcashSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * GCash source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestGcashSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java index aabaeaec8..253924219 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestGiropaySource.java @@ -14,6 +14,12 @@ import java.util.List; +/** + * giropay source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java index 6427c50b7..606dcc8b8 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestIdealSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * iDEAL source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java index c41cbde15..094b999fd 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestIllicadoSource.java @@ -5,6 +5,12 @@ import com.checkout.payments.request.source.AbstractRequestSource; import lombok.*; +/** + * Illicado source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java index b59bf7f6f..e4842ac95 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKakaopaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Kakaopay source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestKakaopaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java index 2ffd7f97c..c47c2d0a1 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKlarnaSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * Klarna source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java index b0e068137..40f097cfa 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestKnetSource.java @@ -10,6 +10,9 @@ import lombok.Setter; import lombok.ToString; +/** + * KNet source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java index 0adc4d229..9e003f842 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMbwaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * MBWay payment request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestMbwaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java index 6f016a9d6..135d01c50 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMobilePaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * MobilePay request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestMobilePaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java index 498ac7ab9..898da4d7a 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestMultiBancoSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Multibanco source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java index 654423dc8..7120acbfc 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestOctopusSource.java @@ -5,6 +5,11 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Octopus Pay source. + * + *

The specification calls this schema PaymentRequestOctopusPaySource. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestOctopusSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java b/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java index 1feadf0b8..6e4826dfe 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestP24Source.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * P24 source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java index ceec52c76..3c2daec7f 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPayPalSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * PayPal source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java index 4c03c49c4..3232180c0 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPlaidSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A Plaid source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java index eb4be737b..168f9d3c9 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestPostFinanceSource.java @@ -9,6 +9,12 @@ import lombok.Setter; import lombok.ToString; +/** + * PostFinance source. + * + *

The current specification's payment request source + * list does not declare this source. It is retained for backwards compatibility. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java index 78ecf1d0a..9989b5a71 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestQPaySource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * QPay source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java index ec86cf4fb..b933856ec 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java @@ -11,6 +11,13 @@ import lombok.Setter; import lombok.ToString; +/** + * SEPA Direct Debit source. + * + *

This is the current-platform source, whose type on the wire is "sepa". The previous platform + * references a stored mandate through the generic "id" source instead; use + * {@link com.checkout.payments.previous.request.source.apm.RequestSepaSource} for that. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java index 295d0e275..372b7afb7 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSequraSource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A seQura payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java index 51b972b75..6a28b6df5 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestStcPaySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * STC Pay request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestStcPaySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java index 46813b623..791042133 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java @@ -10,6 +10,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Swish source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java index a73029ab9..5ada85f7e 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTamaraSource.java @@ -8,6 +8,9 @@ import lombok.Setter; import lombok.ToString; +/** + * A Tamara payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java index 70f939016..bdac6efcf 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTngSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * TNG source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTngSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java index 976ccb353..9df913186 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTruemoneySource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * TrueMoney source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTruemoneySource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java index e2ad48bff..9c48d5aad 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestTwintSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Twint request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestTwintSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java index 2456a6aae..51fcfba97 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestVippsSource.java @@ -5,6 +5,9 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Vipps request source. + */ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class RequestVippsSource extends AbstractRequestSource { diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java index 245496be0..3e7c66c25 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestWeChatPaySource.java @@ -9,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * WeChat Pay source. + */ @Getter @Setter @ToString(callSuper = true) diff --git a/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java b/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java index 4778a555f..44139e36f 100644 --- a/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/AbstractResponseSource.java @@ -3,11 +3,23 @@ import com.checkout.common.PaymentSourceType; import lombok.Data; +/** + * The properties every typed payment response source shares. + */ @Data public abstract class AbstractResponseSource { + /** + * The payment source type. + * [Required] + */ public PaymentSourceType type; + /** + * The payment source identifier that can be used for subsequent payments. For new sources, + * this is only returned if the payment was approved. + * [Optional] + */ public String id; } diff --git a/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java b/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java index d896e650a..855c1c2eb 100644 --- a/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java +++ b/src/main/java/com/checkout/payments/response/source/AlternativePaymentSourceResponse.java @@ -9,11 +9,20 @@ import java.util.HashMap; +/** + * The fallback payment response source, used for any source type the SDK does not model as a + * dedicated class. The raw JSON is exposed as map entries. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class AlternativePaymentSourceResponse extends HashMap implements ResponseSource { + /** + * Returns the payment source type resolved from the raw type entry. + * + * @return the payment source type, or null if the type entry is absent or unrecognised. + */ @Override public PaymentSourceType getType() { return EnumUtils.getEnumIgnoreCase(PaymentSourceType.class, (String) get(CheckoutUtils.TYPE)); diff --git a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java index 2267f12a2..a6b765ed3 100644 --- a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java @@ -1,6 +1,5 @@ package com.checkout.payments.response.source; -import com.checkout.common.AccountHolder; import com.checkout.common.AccountHolderResponse; import com.checkout.common.Address; import com.checkout.common.CardCategory; @@ -15,66 +14,188 @@ import java.util.List; +/** + * A card payment source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CardResponseSource extends AbstractResponseSource implements ResponseSource { + /** + * The payment source owner's billing address. + * [Optional] + */ private Address billingAddress; + /** + * The payment source owner's phone number. + * [Optional] + */ private Phone phone; - // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization - // issues if it is set to Instant + /** + * The expiry month. + * [Required] + * min 1 characters + * max 2 characters + * min 1 + * + *

The specification types this as an integer, but the API masks the value with asterisks on + * some responses, so it is exposed as a string to keep deserialization working. + */ private String expiryMonth; - // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization - // issues if it is set to Instant + /** + * The expiry year. + * [Required] + * min 4 characters + * max 4 characters + * + *

The specification types this as an integer, but the API masks the value with asterisks on + * some responses, so it is exposed as a string to keep deserialization working. + */ private String expiryYear; + /** + * The cardholder's name. + * [Optional] + */ private String name; + /** + * The card scheme. + * [Optional] + */ private String scheme; /** - * @deprecated This property will be removed in the future, and should be used - * {@link CardResponseSource#localSchemes} instead + * The local co-branded card scheme. + * [Optional] + * + * @deprecated replaced by {@link CardResponseSource#localSchemes}. This property will be + * removed in a future version. */ @Deprecated private String schemeLocal; + /** + * The local co-branded card schemes. + * [Optional] + */ private List localSchemes; + /** + * The last four digits of the card number. + * [Required] + */ private String last4; + /** + * Uniquely identifies this particular card number. You can use this to compare cards across + * customers. + * [Required] + */ private String fingerprint; + /** + * The card issuer's Bank Identification Number (BIN). + * [Required] + * max 8 characters + */ private String bin; + /** + * The card type. + * [Optional] + * Enum: "CREDIT" "DEBIT" "PREPAID" "CHARGE" "DEFERRED DEBIT" + */ private CardType cardType; + /** + * The card category. + * [Optional] + * Enum: "CONSUMER" "COMMERCIAL" + */ private CardCategory cardCategory; + /** + * The card wallet type. + * [Optional] + * Enum: "applepay" "googlepay" + */ private CardWalletType cardWalletType; + /** + * The name of the card issuer. + * [Optional] + */ private String issuer; + /** + * The card issuer's country, as a two-letter ISO code. + * [Optional] + * min 2 characters + * max 2 characters + */ private CountryCode issuerCountry; + /** + * The issuer or card scheme product identifier. + * [Optional] + */ private String productId; + /** + * The issuer or card scheme product type. + * [Optional] + */ private String productType; + /** + * The Address Verification System check result. + * [Optional] + */ private String avsCheck; + /** + * The card verification value (CVV) check result. + * [Optional] + */ private String cvvCheck; + /** + * A unique reference to the underlying card for network tokens, such as Apple Pay or Google + * Pay. + * [Optional] + */ private String paymentAccountReference; + /** + * The JWE encrypted full card number that has been updated by the real-time account updater. + * [Optional] + */ private String encryptedCardNumber; + /** + * Specifies what card information was updated by the real-time account updater. + * [Optional] + * Enum: "card_updated" "card_expiry_updated" "card_closed" "contact_cardholder" + */ private AccountUpdateStatusType accountUpdateStatus; + /** + * Provides the failure code if the real-time account update failed. + * [Optional] + */ + private String accountUpdateFailureCode; + + /** + * Information about the account holder of the card. + * [Optional] + */ private AccountHolderResponse accountHolder; } diff --git a/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java b/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java index 3e7296bac..06f2d7d55 100644 --- a/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/CurrencyAccountResponseSource.java @@ -4,13 +4,28 @@ import lombok.EqualsAndHashCode; import lombok.ToString; +/** + * Currency account source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. For this variant the + * specification declares the id required, with the pattern {@code ^(ca)_(\w{26})$}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public final class CurrencyAccountResponseSource extends AbstractResponseSource implements ResponseSource { + /** + * If specified, indicates the amount in the source currency to be paid out. If omitted, the + * root amount in the destination currency is used. + * [Optional] + */ private Long amount; + /** + * The currency of the currency account. + * [Optional] + */ private String currency; } diff --git a/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java b/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java index cc6a4e13a..0690f0369 100644 --- a/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/PayPalResponseSource.java @@ -6,6 +6,11 @@ import static com.checkout.common.PaymentSourceType.PAYPAL; +/** + * PayPal source. + * + *

The type and the id are inherited from {@link AbstractResponseSource}. + */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -21,6 +26,9 @@ public PayPalResponseSource() { this.type = PAYPAL; } + /** + * The PayPal account holder details. + */ @Data public static final class AccountHolder { diff --git a/src/main/java/com/checkout/payments/response/source/ResponseSource.java b/src/main/java/com/checkout/payments/response/source/ResponseSource.java index bdff48539..af7a63cd2 100644 --- a/src/main/java/com/checkout/payments/response/source/ResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/ResponseSource.java @@ -2,8 +2,19 @@ import com.checkout.common.PaymentSourceType; +/** + * The payment source returned on a payment response. + * + *

Implemented by the typed variants the specification declares, and by + * {@link AlternativePaymentSourceResponse} for any source type the SDK does not model. + */ public interface ResponseSource { + /** + * Returns the payment source type. + * + * @return the payment source type. + */ PaymentSourceType getType(); } diff --git a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java index 799577e00..716164053 100644 --- a/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ b/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java @@ -270,12 +270,13 @@ public void write(final JsonWriter out, final R value) throws IOException { final JsonObject clone = new JsonObject(); + if (jsonObject.has(typeFieldName)) { + throw new JsonParseException("cannot serialize " + srcType.getName() + + " because it already defines a field named " + typeFieldName); + } clone.add(typeFieldName, new JsonPrimitive(label)); for (final Map.Entry e : jsonObject.entrySet()) { - if (typeFieldName.equals(e.getKey())) { - continue; - } clone.add(e.getKey(), e.getValue()); } Streams.write(clone, out); diff --git a/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java b/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java new file mode 100644 index 000000000..657ac90a7 --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentRequestTypeTest.java @@ -0,0 +1,62 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.InstrumentType; +import com.checkout.instruments.create.CreateInstrumentAchRequest; +import com.checkout.instruments.create.CreateInstrumentBacsRequest; +import com.checkout.instruments.create.CreateInstrumentBankAccountRequest; +import com.checkout.instruments.create.CreateInstrumentSepaRequest; +import com.checkout.instruments.create.CreateInstrumentTokenRequest; +import com.checkout.instruments.update.UpdateInstrumentAchRequest; +import com.checkout.instruments.update.UpdateInstrumentBacsRequest; +import com.checkout.instruments.update.UpdateInstrumentBankAccountRequest; +import com.checkout.instruments.update.UpdateInstrumentCardRequest; +import com.checkout.instruments.update.UpdateInstrumentSepaRequest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Pins the type discriminator every instrument request sends. + * + *

The type selects the schema the API validates the request against, so a wrong value makes the + * whole request fail regardless of its other properties. UpdateInstrumentBankAccountRequest sent + * "token" until this test was added, and no test covered that class at all. + */ +class InstrumentRequestTypeTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSendTheCorrectTypeOnEveryCreateRequest() { + assertType("bank_account", new CreateInstrumentBankAccountRequest()); + assertType("token", new CreateInstrumentTokenRequest()); + assertType("sepa", new CreateInstrumentSepaRequest()); + assertType("ach", new CreateInstrumentAchRequest()); + assertType("bacs", new CreateInstrumentBacsRequest()); + } + + @Test + void shouldSendTheCorrectTypeOnEveryUpdateRequest() { + assertType("bank_account", new UpdateInstrumentBankAccountRequest()); + assertType("sepa", new UpdateInstrumentSepaRequest()); + assertType("ach", new UpdateInstrumentAchRequest()); + assertType("bacs", new UpdateInstrumentBacsRequest()); + } + + @Test + void shouldSendCardOnTheCardUpdateRequest() { + final UpdateInstrumentCardRequest request = UpdateInstrumentCardRequest.builder().build(); + + assertEquals(InstrumentType.CARD, request.getType()); + assertTrue(serializer.toJson(request).contains("\"type\":\"card\"")); + } + + private void assertType(final String expectedWireValue, final Object request) { + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"type\":\"" + expectedWireValue + "\""), + "expected type " + expectedWireValue + " but serialized " + json); + } +} From 1033b86d611665ad330e5efefb4289e811b23296 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 28 Aug 2026 11:17:05 +0200 Subject: [PATCH 14/18] Gson fix --- src/main/java/com/checkout/GsonSerializer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 42ee1603c..0bea1e310 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -156,10 +156,10 @@ public final class GsonSerializer implements Serializer { .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.twintsource.TwintSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.TWINT)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.vippssource.VippsSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.VIPPS)) .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.wechatpaysource.WechatpaySource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.WECHATPAY)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsegiropaysourcesource.PaymentGetResponseGiropaySourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_GIROPAY_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponseklarnasourcesource.PaymentGetResponseKlarnaSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_KLARNA_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsesepavfoursourcesource.PaymentGetResponseSEPAVFourSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_SEPAV4_SOURCE)) - .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentresponsesourcesource.PaymentResponseSourceSource.class, identifier(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_RESPONSE_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsegiropaysourcesource.PaymentGetResponseGiropaySourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_GIROPAY_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponseklarnasourcesource.PaymentGetResponseKlarnaSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_KLARNA_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentgetresponsesepavfoursourcesource.PaymentGetResponseSEPAVFourSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_GET_RESPONSE_SEPAV4_SOURCE)) + .registerSubtype(com.checkout.handlepaymentsandpayouts.payments.common.source.paymentresponsesourcesource.PaymentResponseSourceSource.class, serializedName(com.checkout.handlepaymentsandpayouts.payments.common.source.SourceType.PAYMENT_RESPONSE_SOURCE)) ) // Payments PREVIOUS - source .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.payments.previous.response.source.ResponseSource.class, CheckoutUtils.TYPE, true, com.checkout.payments.previous.response.source.AlternativePaymentSourceResponse.class) From 4bbbc181d1f26f6d7b98506e5a7106e0717b5950 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 31 Aug 2026 11:23:22 +0200 Subject: [PATCH 15/18] Some fixes --- .../CreateCustomerInstrumentRequest.java | 12 +++- .../CreateInstrumentBankAccountResponse.java | 13 +++- .../create/CreateInstrumentResponse.java | 16 ----- .../create/CreateInstrumentTokenResponse.java | 27 +++++++- .../instruments/create/InstrumentData.java | 33 +++++++-- .../instruments/get/BankAccountField.java | 13 +--- .../get/InstrumentCustomerResponse.java | 6 +- .../source/apm/RequestSwishAccountHolder.java | 30 +++++++++ .../apm/RequestSwishBillingDescriptor.java | 23 +++++++ .../source/apm/RequestSwishSource.java | 20 +++--- .../InstrumentSchemaRegressionTest.java | 67 +++++++++++++++++++ 11 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java create mode 100644 src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java create mode 100644 src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java diff --git a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java index c3a74006b..ad8178677 100644 --- a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java @@ -35,14 +35,22 @@ public final class CreateCustomerInstrumentRequest extends CustomerRequest { * [Optional] */ @SerializedName("default") - private boolean defaultInstrument; + private Boolean defaultInstrument; + + /** + * @deprecated Use {@link #getDefaultInstrument()}. + */ + @Deprecated + public Boolean isDefaultInstrument() { + return defaultInstrument; + } @Builder private CreateCustomerInstrumentRequest(final String email, final String name, final Phone phone, final String id, - final boolean defaultInstrument) { + final Boolean defaultInstrument) { super(email, name, phone); this.id = id; this.defaultInstrument = defaultInstrument; diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java index ce59414b2..d9585c9b5 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java @@ -1,6 +1,7 @@ package com.checkout.instruments.create; import com.checkout.common.BankDetails; +import com.checkout.common.CustomerResponse; import com.checkout.common.InstrumentType; import lombok.Data; import lombok.EqualsAndHashCode; @@ -9,9 +10,9 @@ /** * Store bank account instrument response. * - *

The id, the fingerprint and the customer are inherited from - * {@link CreateInstrumentResponse}. This is one of only two store responses the specification - * declares a customer on, the other being {@link CreateInstrumentTokenResponse}. + *

The id and fingerprint are inherited from {@link CreateInstrumentResponse}. This is one of + * only two store responses the specification declares a customer on, the other being + * {@link CreateInstrumentTokenResponse}. */ @Data @EqualsAndHashCode(callSuper = true) @@ -54,4 +55,10 @@ public final class CreateInstrumentBankAccountResponse extends CreateInstrumentR */ private String iban; + /** + * The customer that the instrument is associated with. + * [Optional] + */ + private CustomerResponse customer; + } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java index e23af6050..cdf55cd88 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java @@ -1,7 +1,6 @@ package com.checkout.instruments.create; import com.checkout.HttpMetadata; -import com.checkout.common.CustomerResponse; import lombok.Data; import lombok.EqualsAndHashCode; @@ -26,19 +25,4 @@ public abstract class CreateInstrumentResponse extends HttpMetadata { */ protected String fingerprint; - /** - * The customer that the instrument is associated with. - * - *

Only the token and bank_account variants declare this property. The sepa, ach and bacs - * store responses do not, so on those three it is always null and does not belong here. - * [Optional] - * - * @deprecated declared on the wrong level. This will move to - * {@link CreateInstrumentTokenResponse} and {@link CreateInstrumentBankAccountResponse}, the - * only two variants the specification declares it on, in the next major version. Read it from - * the concrete response type rather than from this base class. - */ - @Deprecated - protected CustomerResponse customer; - } diff --git a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java index 0318860c0..877d33441 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java @@ -3,7 +3,10 @@ import com.checkout.common.CardCategory; import com.checkout.common.CardType; import com.checkout.common.CountryCode; +import com.checkout.common.AccountHolderResponse; +import com.checkout.common.CustomerResponse; import com.checkout.common.InstrumentType; +import com.checkout.instruments.get.InstrumentNetworkToken; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -11,9 +14,9 @@ /** * Card instrument response. * - *

The id, the fingerprint and the customer are inherited from - * {@link CreateInstrumentResponse}. The instrument type is card rather than token: the - * specification maps the token store request onto a card store response. + *

The id and fingerprint are inherited from {@link CreateInstrumentResponse}. The instrument + * type is card rather than token: the specification maps the token store request onto a card + * store response. */ @Data @EqualsAndHashCode(callSuper = true) @@ -109,4 +112,22 @@ public final class CreateInstrumentTokenResponse extends CreateInstrumentRespons */ private String productType; + /** + * The account holder details. + * [Optional] + */ + private AccountHolderResponse accountHolder; + + /** + * The customer that the instrument is associated with. + * [Optional] + */ + private CustomerResponse customer; + + /** + * The network token provisioned for this instrument. + * [Optional] + */ + private InstrumentNetworkToken networkToken; + } diff --git a/src/main/java/com/checkout/instruments/create/InstrumentData.java b/src/main/java/com/checkout/instruments/create/InstrumentData.java index 8bde35dd1..9705d84bc 100644 --- a/src/main/java/com/checkout/instruments/create/InstrumentData.java +++ b/src/main/java/com/checkout/instruments/create/InstrumentData.java @@ -37,13 +37,36 @@ public final class InstrumentData { * [Required] * min 15 characters * max 34 characters - * - *

The Java field name carries a typo. The wire name is correct because of the explicit - * annotation, so this is a source-only defect; renaming the field is breaking and is deferred - * to the next major version. */ @SerializedName("account_number") - private String accoountNumber; + private String accountNumber; + + /** + * @deprecated Use {@link #getAccountNumber()}. + */ + @Deprecated + public String getAccoountNumber() { + return accountNumber; + } + + /** + * @deprecated Use {@link #setAccountNumber(String)}. + */ + @Deprecated + public void setAccoountNumber(final String accountNumber) { + this.accountNumber = accountNumber; + } + + public static class InstrumentDataBuilder { + + /** + * @deprecated Use {@link #accountNumber(String)}. + */ + @Deprecated + public InstrumentDataBuilder accoountNumber(final String accountNumber) { + return accountNumber(accountNumber); + } + } /** * The country of the account. diff --git a/src/main/java/com/checkout/instruments/get/BankAccountField.java b/src/main/java/com/checkout/instruments/get/BankAccountField.java index 62b42919b..66534082e 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountField.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountField.java @@ -1,6 +1,4 @@ package com.checkout.instruments.get; - -import com.google.gson.annotations.SerializedName; import lombok.Data; import java.util.List; @@ -45,7 +43,7 @@ public final class BankAccountField { * Whether the field is required. * [Required] */ - private boolean required; + private Boolean required; /** * A regular expression that can be used to validate the input of the field. @@ -57,18 +55,13 @@ public final class BankAccountField { * The minimum length of the field. * [Optional] */ - private int minLength; + private Integer minLength; /** * The maximum length of the field. * [Optional] - * - *

The Java field name is lowercase where the neighbouring minLength is camel case. The wire - * name is correct because of the explicit annotation, so this is a source-only defect; - * renaming the field is breaking and is deferred to the next major version. */ - @SerializedName("max_length") - private int maxlength; + private Integer maxLength; /** * The allowed options for the field. diff --git a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java index 20e729704..1658c307a 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java @@ -23,6 +23,10 @@ public final class InstrumentCustomerResponse extends CustomerResponse { * [Optional] */ @SerializedName("default") - private boolean isDefault; + private Boolean isDefault; + + public Boolean isDefault() { + return isDefault; + } } diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java new file mode 100644 index 000000000..f4f81e148 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishAccountHolder.java @@ -0,0 +1,30 @@ +package com.checkout.payments.request.source.apm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details for a Swish payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestSwishAccountHolder { + + /** + * The account holder's first name. + * [Required] + * max 50 characters + */ + private String firstName; + + /** + * The account holder's last name. + * [Required] + * max 50 characters + */ + private String lastName; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java new file mode 100644 index 000000000..3c826f085 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishBillingDescriptor.java @@ -0,0 +1,23 @@ +package com.checkout.payments.request.source.apm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The billing descriptor for a Swish payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestSwishBillingDescriptor { + + /** + * A description for the payment, which displays on the customer's statement. + * [Required] + * max 120 characters + */ + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java index 791042133..4f3a53e05 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSwishSource.java @@ -1,8 +1,7 @@ package com.checkout.payments.request.source.apm; -import com.checkout.common.AccountHolder; +import com.checkout.common.CountryCode; import com.checkout.common.PaymentSourceType; -import com.checkout.payments.BillingDescriptor; import com.checkout.payments.request.source.AbstractRequestSource; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -21,26 +20,27 @@ public final class RequestSwishSource extends AbstractRequestSource { /** * The two-letter ISO country code of the payment. - * [Optional] + * [Required] + * Enum: "SE" */ - private String paymentCountry; + private CountryCode paymentCountry; /** * The account holder's details. - * [Optional] + * [Required] */ - private AccountHolder accountHolder; + private RequestSwishAccountHolder accountHolder; /** * A description of the purchase shown on the customer's statement. * [Optional] */ - private BillingDescriptor billingDescriptor; + private RequestSwishBillingDescriptor billingDescriptor; @Builder - private RequestSwishSource(final String paymentCountry, - final AccountHolder accountHolder, - final BillingDescriptor billingDescriptor) { + private RequestSwishSource(final CountryCode paymentCountry, + final RequestSwishAccountHolder accountHolder, + final RequestSwishBillingDescriptor billingDescriptor) { super(PaymentSourceType.SWISH); this.paymentCountry = paymentCountry; this.accountHolder = accountHolder; diff --git a/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java b/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java new file mode 100644 index 000000000..e55286406 --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentSchemaRegressionTest.java @@ -0,0 +1,67 @@ +package com.checkout.instruments; + +import com.checkout.GsonSerializer; +import com.checkout.common.CountryCode; +import com.checkout.instruments.create.CreateCustomerInstrumentRequest; +import com.checkout.instruments.create.CreateInstrumentResponse; +import com.checkout.instruments.create.CreateInstrumentTokenResponse; +import com.checkout.instruments.get.BankAccountField; +import com.checkout.instruments.get.InstrumentCustomerResponse; +import com.checkout.payments.request.source.apm.RequestSwishAccountHolder; +import com.checkout.payments.request.source.apm.RequestSwishBillingDescriptor; +import com.checkout.payments.request.source.apm.RequestSwishSource; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class InstrumentSchemaRegressionTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldOmitUnsetInstrumentCustomerDefaultsAndBankAccountFieldValues() { + final String customerJson = serializer.toJson(CreateCustomerInstrumentRequest.builder().build()); + final BankAccountField field = serializer.fromJson("{\"id\":\"iban\",\"display\":\"IBAN\",\"type\":\"string\"}", BankAccountField.class); + final InstrumentCustomerResponse customer = serializer.fromJson("{}", InstrumentCustomerResponse.class); + + assertFalse(customerJson.contains("\"default\"")); + assertNull(field.getRequired()); + assertNull(field.getMinLength()); + assertNull(field.getMaxLength()); + assertNull(customer.isDefault()); + } + + @Test + void shouldDeserializeAllTokenStoreResponseProperties() { + final String json = "{\"type\":\"card\",\"id\":\"src_wmlfc3zyhqzehihu7giusaaawu\",\"fingerprint\":\"vnsdrvikkvre3dtrjjvlm5du4q\",\"expiry_month\":6,\"expiry_year\":2025,\"last4\":\"9996\",\"bin\":\"454347\",\"account_holder\":{\"first_name\":\"Hannah\",\"last_name\":\"Bret\"},\"network_token\":{\"id\":\"nt_y3oqhf46pyzuxjbcn2giaqnb44\",\"state\":\"active\"}}"; + + final CreateInstrumentResponse response = serializer.fromJson(json, CreateInstrumentResponse.class); + final CreateInstrumentTokenResponse token = assertInstanceOf(CreateInstrumentTokenResponse.class, response); + + assertEquals("Hannah", token.getAccountHolder().getFirstName()); + assertEquals("nt_y3oqhf46pyzuxjbcn2giaqnb44", token.getNetworkToken().getId()); + } + + @Test + void shouldSerializeAndDeserializeOnlySwishSchemaFields() { + final RequestSwishSource source = RequestSwishSource.builder() + .paymentCountry(CountryCode.SE) + .accountHolder(RequestSwishAccountHolder.builder().firstName("Bruce").lastName("Wayne").build()) + .billingDescriptor(RequestSwishBillingDescriptor.builder().name("CKO Store").build()) + .build(); + + final String json = serializer.toJson(source); + final RequestSwishSource deserialized = serializer.fromJson(json, RequestSwishSource.class); + + assertTrue(json.contains("\"payment_country\":\"SE\"")); + assertTrue(json.contains("\"account_holder\":{\"first_name\":\"Bruce\",\"last_name\":\"Wayne\"}")); + assertTrue(json.contains("\"billing_descriptor\":{\"name\":\"CKO Store\"}")); + assertEquals(CountryCode.SE, deserialized.getPaymentCountry()); + assertEquals("Bruce", deserialized.getAccountHolder().getFirstName()); + assertEquals("CKO Store", deserialized.getBillingDescriptor().getName()); + } +} \ No newline at end of file From e5e47c88ea9d605419358b504cfc59c9aa1a5874 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 1 Sep 2026 10:03:41 +0200 Subject: [PATCH 16/18] Arch instrument data detailed --- .../update/UpdateAchAccountHolder.java | 51 +++++++++++ .../update/UpdateAchInstrumentData.java | 60 +++++++++++++ .../update/UpdateInstrumentAchRequest.java | 67 +-------------- .../AchInstrumentSerializationTest.java | 85 +++++++++++++++++++ 4 files changed, 200 insertions(+), 63 deletions(-) create mode 100644 src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java create mode 100644 src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java diff --git a/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java b/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java new file mode 100644 index 000000000..dea75a4ed --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateAchAccountHolder.java @@ -0,0 +1,51 @@ +package com.checkout.instruments.update; + +import com.checkout.instruments.InstrumentAccountHolderType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The account holder details of an ACH instrument being updated. + * + *

The specification marks all four properties as required, but the descriptions qualify that: + * the names apply to an individual account holder and the company name to a corporate one. That is a + * conditional requirement the specification cannot express, so it is not enforced here. + * + *

This deliberately does not use {@code com.checkout.common.AccountHolder}, which is a superset + * carrying a phone number, identification, a date of birth and a tax ID that the ACH instrument + * schema does not declare. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateAchAccountHolder { + + /** + * First name. Required for individual account holder type. + * [Required] + */ + private String firstName; + + /** + * Last name. Required for individual account holder type. + * [Required] + */ + private String lastName; + + /** + * Company name. Required for corporate account holder type. + * [Required] + */ + private String companyName; + + /** + * Account holder type. + * [Required] + * Enum: "individual" "corporate" + */ + private InstrumentAccountHolderType type; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java b/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java new file mode 100644 index 000000000..fc3d4911d --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateAchInstrumentData.java @@ -0,0 +1,60 @@ +package com.checkout.instruments.update; + +import com.checkout.common.CountryCode; +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the ACH account being updated. + * + *

Every property is optional on update. The shape is identical to the store and retrieve + * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per operation. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class UpdateAchInstrumentData { + + /** + * The type of Direct Debit account. + * [Optional] + */ + private AchInstrumentAccountType accountType; + + /** + * The account number of the Direct Debit account. + * [Optional] + * min 4 characters + * max 17 characters + */ + private String accountNumber; + + /** + * The bank code of the Direct Debit account, also known as the routing number. + * [Optional] + * min 8 characters + * max 9 characters + */ + private String bankCode; + + /** + * The currency of the account. + * [Optional] + * min 3 characters + * max 3 characters + */ + private Currency currency; + + /** + * The country of the account. + * [Optional] + * min 2 characters + * max 2 characters + */ + private CountryCode country; + +} diff --git a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java index 8aa56e6a2..da4bc228c 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java @@ -1,15 +1,9 @@ package com.checkout.instruments.update; -import com.checkout.common.AccountHolder; -import com.checkout.common.CountryCode; -import com.checkout.common.Currency; import com.checkout.common.InstrumentType; -import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @@ -28,17 +22,17 @@ public final class UpdateInstrumentAchRequest extends UpdateInstrumentRequest { * The details of the bank account. * [Optional] */ - private AchInstrumentData instrumentData; + private UpdateAchInstrumentData instrumentData; /** * The account holder details. * [Optional] */ - private AccountHolder accountHolder; + private UpdateAchAccountHolder accountHolder; @Builder - private UpdateInstrumentAchRequest(final AchInstrumentData instrumentData, - final AccountHolder accountHolder) { + private UpdateInstrumentAchRequest(final UpdateAchInstrumentData instrumentData, + final UpdateAchAccountHolder accountHolder) { super(InstrumentType.ACH); this.instrumentData = instrumentData; this.accountHolder = accountHolder; @@ -48,57 +42,4 @@ public UpdateInstrumentAchRequest() { super(InstrumentType.ACH); } - /** - * The details of the ACH account being updated. - * - *

Every property is optional on update. The shape is identical to the store and retrieve - * variants, unlike the Bacs Direct Debit instrument data, whose length limits differ per - * operation. - */ - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static final class AchInstrumentData { - - /** - * The type of Direct Debit account. - * [Optional] - */ - private AchInstrumentAccountType accountType; - - /** - * The account number of the Direct Debit account. - * [Optional] - * min 4 characters - * max 17 characters - */ - private String accountNumber; - - /** - * The bank code of the Direct Debit account, also known as the routing number. - * [Optional] - * min 8 characters - * max 9 characters - */ - private String bankCode; - - /** - * The currency of the account. - * [Optional] - * min 3 characters - * max 3 characters - */ - private Currency currency; - - /** - * The country of the account. - * [Optional] - * min 2 characters - * max 2 characters - */ - private CountryCode country; - - } - } diff --git a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java index 3ab15a4cb..1ec7c3927 100644 --- a/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -20,6 +20,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.checkout.instruments.update.UpdateAchAccountHolder; +import com.checkout.instruments.update.UpdateAchInstrumentData; +import com.checkout.instruments.update.UpdateInstrumentAchRequest; +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Schema validation tests for the ACH variants of the instruments endpoints. @@ -273,4 +281,81 @@ private String retrieveResponseJson() { + "\"default\":true" + "}}"; } + // ------------------------------------------------------------------ + // UpdateAchInstrumentRequest - A2c + // The update variant predated this work. Its instrument data was a nested class inside the + // request and its account holder reused the shared com.checkout.common.AccountHolder, a superset + // of the four properties the schema declares. + // ------------------------------------------------------------------ + + @Test + void shouldRoundTripEveryUpdateRequestProperty() { + final UpdateInstrumentAchRequest request = UpdateInstrumentAchRequest.builder() + .instrumentData(UpdateAchInstrumentData.builder() + .accountType(AchInstrumentAccountType.SAVINGS) + .accountNumber("4099999992") + .bankCode("211370545") + .currency(Currency.USD) + .country(CountryCode.US) + .build()) + .accountHolder(UpdateAchAccountHolder.builder() + .firstName("John") + .lastName("Smith") + .companyName("Smith Enterprises") + .type(InstrumentAccountHolderType.CORPORATE) + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"account_type\":\"savings\"")); + assertTrue(json.contains("\"account_number\":\"4099999992\"")); + assertTrue(json.contains("\"bank_code\":\"211370545\"")); + assertTrue(json.contains("\"currency\":\"USD\"")); + assertTrue(json.contains("\"country\":\"US\"")); + assertTrue(json.contains("\"first_name\":\"John\"")); + assertTrue(json.contains("\"last_name\":\"Smith\"")); + assertTrue(json.contains("\"company_name\":\"Smith Enterprises\"")); + assertTrue(json.contains("\"type\":\"corporate\"")); + + final UpdateInstrumentAchRequest back = + serializer.fromJson(json, UpdateInstrumentAchRequest.class); + + assertEquals(AchInstrumentAccountType.SAVINGS, back.getInstrumentData().getAccountType()); + assertEquals("4099999992", back.getInstrumentData().getAccountNumber()); + assertEquals("211370545", back.getInstrumentData().getBankCode()); + assertEquals(Currency.USD, back.getInstrumentData().getCurrency()); + assertEquals(CountryCode.US, back.getInstrumentData().getCountry()); + assertEquals("John", back.getAccountHolder().getFirstName()); + assertEquals("Smith", back.getAccountHolder().getLastName()); + assertEquals("Smith Enterprises", back.getAccountHolder().getCompanyName()); + assertEquals(InstrumentAccountHolderType.CORPORATE, back.getAccountHolder().getType()); + } + + @Test + void shouldNotExposeSharedAccountHolderFieldsOnTheAchUpdateVariant() { + // UpdateAchInstrumentRequest.account_holder declares exactly four properties. The shared + // com.checkout.common.AccountHolder carries a phone number, identification, a date of birth + // and a tax ID that this schema does not declare. + final Field[] fields = UpdateAchAccountHolder.class.getDeclaredFields(); + final Set names = new HashSet<>(); + for (final Field field : fields) { + if (!field.isSynthetic()) { + names.add(field.getName()); + } + } + + assertEquals(new HashSet<>(asList("firstName", "lastName", "companyName", "type")), names); + } + + @Test + void shouldModelTheAchUpdateInstrumentDataInItsOwnFile() { + // The instrument data used to be a nested class inside UpdateInstrumentAchRequest, which + // broke the one-type-per-file rule. + assertNull(UpdateAchInstrumentData.class.getEnclosingClass()); + assertThrows(ClassNotFoundException.class, () -> Class.forName( + "com.checkout.instruments.update.UpdateInstrumentAchRequest$AchInstrumentData")); + } + } From 7b8035596671588e0ac9530dd247cd477f7d6a43 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 1 Sep 2026 17:51:42 +0200 Subject: [PATCH 17/18] Bankcode comment --- .../payments/request/source/apm/RequestSepaSource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java index b933856ec..a45d9fc20 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestSepaSource.java @@ -37,8 +37,9 @@ public final class RequestSepaSource extends AbstractRequestSource { private String accountNumber; /** - * The BIC/SWIFT code of the bank. - * [Optional] + * Not declared by PaymentRequestSEPAV4Source. No SEPA schema in the specification declares a + * bank code, and the SEPA source is identified by IBAN through accountNumber. Retained + * for retro-compatibility purposes only. Possibly an obsoleted field. */ private String bankCode; From 98e2c4d1e0f350f2927f8be6701f7637814cb63c Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 2 Sep 2026 15:54:08 +0200 Subject: [PATCH 18/18] AchSourceAccountType fix --- .../source/apm/AchSourceAccountType.java | 29 ++++++ .../request/source/apm/RequestAchSource.java | 7 +- .../payments/RequestApmPaymentsIT.java | 4 +- .../source/apm/AchSourceAccountTypeTest.java | 93 +++++++++++++++++++ 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/checkout/payments/request/source/apm/AchSourceAccountType.java create mode 100644 src/test/java/com/checkout/payments/request/source/apm/AchSourceAccountTypeTest.java diff --git a/src/main/java/com/checkout/payments/request/source/apm/AchSourceAccountType.java b/src/main/java/com/checkout/payments/request/source/apm/AchSourceAccountType.java new file mode 100644 index 000000000..7773269a9 --- /dev/null +++ b/src/main/java/com/checkout/payments/request/source/apm/AchSourceAccountType.java @@ -0,0 +1,29 @@ +package com.checkout.payments.request.source.apm; + +import com.google.gson.annotations.SerializedName; + +/** + * The type of Direct Debit account on an ACH payment source. + * + *

PaymentRequestAchSource is the only schema declaring this set of values. Two neighbouring + * enums are deliberately different and are not interchangeable: + * + *

    + *
  • {@link com.checkout.common.AccountType} is savings / current / cash and serves the + * bank-account instrument and destination positions, so it cannot express "checking". + *
  • {@link com.checkout.instruments.update.AchInstrumentAccountType} is savings / checking and + * serves the stored ACH instrument positions, so it does not declare "cash". + *
+ */ +public enum AchSourceAccountType { + + @SerializedName("savings") + SAVINGS, + + @SerializedName("checking") + CHECKING, + + @SerializedName("cash") + CASH + +} diff --git a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java index e053baecc..c80322139 100644 --- a/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java +++ b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java @@ -1,7 +1,6 @@ package com.checkout.payments.request.source.apm; import com.checkout.common.AccountHolder; -import com.checkout.common.AccountType; import com.checkout.common.CountryCode; import com.checkout.common.PaymentSourceType; import com.checkout.payments.request.source.AbstractRequestSource; @@ -24,8 +23,10 @@ public final class RequestAchSource extends AbstractRequestSource { * The type of Direct Debit account. * [Required] * Enum: "savings" "checking" "cash" + *

Deliberately not {@link com.checkout.common.AccountType}, which declares "current" + * instead of "checking" and is rejected at this position. */ - private AccountType accountType; + private AchSourceAccountType accountType; /** * The source country as an ISO 3166-1 alpha-2 code. @@ -57,7 +58,7 @@ public final class RequestAchSource extends AbstractRequestSource { @Builder private RequestAchSource( - final AccountType accountType, + final AchSourceAccountType accountType, final CountryCode country, final String accountNumber, final String bankCode, diff --git a/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java b/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java index df26a9bde..3cf617d9f 100644 --- a/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java +++ b/src/test/java/com/checkout/payments/RequestApmPaymentsIT.java @@ -27,7 +27,6 @@ import com.checkout.TestHelper; import com.checkout.common.AccountHolder; import com.checkout.common.AccountHolderType; -import com.checkout.common.AccountType; import com.checkout.common.Address; import com.checkout.common.CountryCode; import com.checkout.common.Currency; @@ -37,6 +36,7 @@ import com.checkout.payments.request.PaymentCustomerRequest; import com.checkout.payments.request.PaymentRequest; import com.checkout.payments.request.source.AbstractRequestSource; +import com.checkout.payments.request.source.apm.AchSourceAccountType; import com.checkout.payments.request.source.apm.RequestAchSource; import com.checkout.payments.request.source.apm.RequestAfterPaySource; import com.checkout.payments.request.source.apm.RequestAlipayPlusSource; @@ -815,7 +815,7 @@ private RequestSepaSource createSepaSource() { private RequestAchSource createAchSource() { return RequestAchSource.builder() - .accountType(AccountType.SAVINGS) + .accountType(AchSourceAccountType.SAVINGS) .country(CountryCode.GB) .accountNumber("8784738748973829") .bankCode("BANK") diff --git a/src/test/java/com/checkout/payments/request/source/apm/AchSourceAccountTypeTest.java b/src/test/java/com/checkout/payments/request/source/apm/AchSourceAccountTypeTest.java new file mode 100644 index 000000000..8f4c1269d --- /dev/null +++ b/src/test/java/com/checkout/payments/request/source/apm/AchSourceAccountTypeTest.java @@ -0,0 +1,93 @@ +package com.checkout.payments.request.source.apm; + +import com.checkout.GsonSerializer; +import com.checkout.common.AccountType; +import com.checkout.common.CountryCode; +import com.checkout.instruments.update.AchInstrumentAccountType; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Schema validation tests for the ACH payment source account type. + * + *

PaymentRequestAchSource is the only schema declaring savings / checking / cash. + * RequestAchSource previously typed this field as com.checkout.common.AccountType, which declares + * "current" instead of "checking", so a valid account type could not be sent and an invalid one + * was offered. + */ +class AchSourceAccountTypeTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeEachAccountTypeToItsWireValue() { + assertTrue(serializer.toJson(sourceWith(AchSourceAccountType.SAVINGS)) + .contains("\"account_type\":\"savings\"")); + assertTrue(serializer.toJson(sourceWith(AchSourceAccountType.CHECKING)) + .contains("\"account_type\":\"checking\"")); + assertTrue(serializer.toJson(sourceWith(AchSourceAccountType.CASH)) + .contains("\"account_type\":\"cash\"")); + } + + @Test + void shouldDeclareExactlyTheThreeValuesTheSchemaDeclares() { + assertEquals(3, AchSourceAccountType.values().length); + + final String names = Arrays.stream(AchSourceAccountType.values()) + .map(Enum::name) + .collect(Collectors.joining(",")); + + assertEquals("SAVINGS,CHECKING,CASH", names); + } + + @Test + void shouldDifferFromTheSharedAndInstrumentAccountTypes() { + final String shared = Arrays.stream(AccountType.values()) + .map(Enum::name).collect(Collectors.joining(",")); + final String instrument = Arrays.stream(AchInstrumentAccountType.values()) + .map(Enum::name).collect(Collectors.joining(",")); + + // The shared enum offers CURRENT, which this position rejects, and cannot express + // CHECKING. If these are ever unified, this test fails. + assertTrue(shared.contains("CURRENT")); + assertFalse(shared.contains("CHECKING")); + assertFalse(instrument.contains("CASH")); + } + + @Test + void shouldRoundTripAnAchSourceWithCheckingAccountType() { + final RequestAchSource original = RequestAchSource.builder() + .accountType(AchSourceAccountType.CHECKING) + .country(CountryCode.US) + .accountNumber("136549956") + .bankCode("021000021") + .build(); + + final String json = serializer.toJson(original); + + assertTrue(json.contains("\"type\":\"ach\"")); + assertTrue(json.contains("\"account_type\":\"checking\"")); + assertTrue(json.contains("\"account_number\":\"136549956\"")); + assertTrue(json.contains("\"bank_code\":\"021000021\"")); + + final RequestAchSource result = serializer.fromJson(json, RequestAchSource.class); + + assertEquals(AchSourceAccountType.CHECKING, result.getAccountType()); + assertEquals("136549956", result.getAccountNumber()); + } + + private RequestAchSource sourceWith(final AchSourceAccountType accountType) { + return RequestAchSource.builder() + .accountType(accountType) + .country(CountryCode.US) + .accountNumber("136549956") + .bankCode("021000021") + .build(); + } +}