diff --git a/README.md b/README.md index 1c58463ef..2ce185db1 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(); @@ -159,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. @@ -399,7 +478,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 +735,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/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 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/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/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/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/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index d192843ca..0bea1e310 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -112,9 +112,13 @@ 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)) + .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)) @@ -152,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) @@ -167,7 +171,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)) @@ -183,42 +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))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.get.GetInstrumentResponse.class, CheckoutUtils.TYPE) + .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, 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))) - .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(com.checkout.instruments.update.UpdateInstrumentResponse.class, CheckoutUtils.TYPE) + .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, 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.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/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/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/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/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/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/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/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/CreateCustomerInstrumentRequest.java b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java index 19d2ba6f6..ad8178677 100644 --- a/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java +++ b/src/main/java/com/checkout/instruments/create/CreateCustomerInstrumentRequest.java @@ -9,23 +9,48 @@ 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; + 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/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/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..d9585c9b5 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentBankAccountResponse.java @@ -1,26 +1,64 @@ 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; import lombok.ToString; +/** + * Store bank account instrument response. + * + *

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) @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; + /** + * The customer that the instrument is associated with. + * [Optional] + */ + private CustomerResponse customer; + } 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/CreateInstrumentResponse.java b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java index c9a48df39..cdf55cd88 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentResponse.java @@ -1,18 +1,28 @@ package com.checkout.instruments.create; import com.checkout.HttpMetadata; -import com.checkout.common.CustomerResponse; 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; - protected CustomerResponse customer; - } 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/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..877d33441 100644 --- a/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java +++ b/src/main/java/com/checkout/instruments/create/CreateInstrumentTokenResponse.java @@ -3,40 +3,131 @@ 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; +/** + * Card instrument 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) @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; + /** + * 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 0e530e18e..9705d84bc 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,81 @@ 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 */ @SerializedName("account_number") - private String accoountNumber; + private String accountNumber; /** - * The country of the SEPA account. - * [Optional] + * @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. + * [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/get/BankAccountField.java b/src/main/java/com/checkout/instruments/get/BankAccountField.java index beb0a4b73..66534082e 100644 --- a/src/main/java/com/checkout/instruments/get/BankAccountField.java +++ b/src/main/java/com/checkout/instruments/get/BankAccountField.java @@ -1,34 +1,78 @@ package com.checkout.instruments.get; - -import com.google.gson.annotations.SerializedName; import lombok.Data; 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; - private boolean required; + /** + * 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; - private int minLength; + /** + * The minimum length of the field. + * [Optional] + */ + private Integer minLength; - @SerializedName("max_length") - private int maxlength; + /** + * The maximum length of the field. + * [Optional] + */ + private Integer 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/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..9d38893a5 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; @@ -9,31 +10,89 @@ 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; + /** + * 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..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; /** @@ -119,4 +128,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..ac4e50b4f --- /dev/null +++ b/src/main/java/com/checkout/instruments/get/GetSepaInstrumentData.java @@ -0,0 +1,82 @@ +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 + * min 10 characters + * max 10 characters + */ + 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/get/InstrumentCustomerResponse.java b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java index e752a3d0c..1658c307a 100644 --- a/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java +++ b/src/main/java/com/checkout/instruments/get/InstrumentCustomerResponse.java @@ -6,12 +6,27 @@ 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; + private Boolean isDefault; + + public Boolean isDefault() { + return isDefault; + } } 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/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/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/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/UpdateInstrumentAchRequest.java b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java index d9f2148bd..da4bc228c 100644 --- a/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentAchRequest.java @@ -1,31 +1,38 @@ 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; +/** + * 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 { - private AchInstrumentData instrumentData; + /** + * The details of the bank account. + * [Optional] + */ + private UpdateAchInstrumentData instrumentData; - private AccountHolder accountHolder; + /** + * The account holder details. + * [Optional] + */ + 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; @@ -35,22 +42,4 @@ public UpdateInstrumentAchRequest() { super(InstrumentType.ACH); } - @Data - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static final class AchInstrumentData { - - private AchInstrumentAccountType accountType; - - private String accountNumber; - - private String bankCode; - - private Currency currency; - - private CountryCode country; - - } - } 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/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..e5c0be2de --- /dev/null +++ b/src/main/java/com/checkout/instruments/update/UpdateInstrumentBacsResponse.java @@ -0,0 +1,30 @@ +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. + */ +@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/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/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/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/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/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/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/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/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/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/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/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/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java b/src/main/java/com/checkout/payments/request/source/apm/RequestAchSource.java index 10a68a156..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; @@ -11,6 +10,9 @@ import lombok.Setter; import lombok.ToString; +/** + * An ach payment source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @@ -21,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. @@ -54,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/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/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/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..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 @@ -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) @@ -30,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; 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/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 46813b623..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; @@ -10,6 +9,9 @@ import lombok.Setter; import lombok.ToString; +/** + * Swish source. + */ @Getter @Setter @EqualsAndHashCode(callSuper = true) @@ -18,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/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/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/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/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/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/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/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/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/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 new file mode 100644 index 000000000..1ec7c3927 --- /dev/null +++ b/src/test/java/com/checkout/instruments/AchInstrumentSerializationTest.java @@ -0,0 +1,361 @@ +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 com.checkout.instruments.update.UpdateInstrumentAchResponse; +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; +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. + */ +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 and update responses + // ------------------------------------------------------------------ + + @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 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); + 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" + + "}}"; + } + // ------------------------------------------------------------------ + // 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")); + } + +} 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/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); + } +} 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..bf3bdbd7c --- /dev/null +++ b/src/test/java/com/checkout/instruments/InstrumentResponseDispatchTest.java @@ -0,0 +1,129 @@ +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.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, 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 { + + 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("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\"," + + "\"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/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 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/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..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") @@ -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(); } 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/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(); + } +} 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()); + } +} 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()); } 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; + } +}