diff --git a/build.gradle b/build.gradle index aee1d54a..a43cbb1c 100644 --- a/build.gradle +++ b/build.gradle @@ -62,9 +62,23 @@ jacocoTestReport { dependsOn test reports { xml.required = true + html.required = true + csv.required = false } } +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.70 + } + } + } +} + +check.dependsOn jacocoTestCoverageVerification + tasks.register('sourcesJar', Jar) { from sourceSets.main.allJava archiveClassifier = 'sources' diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index 8cbef97a..588fca2e 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -1,7 +1,6 @@ package com.checkout; import com.checkout.accounts.AccountsFileRequest; -import com.checkout.accounts.Headers; import com.checkout.common.AbstractFileRequest; import com.checkout.common.CheckoutUtils; import com.checkout.common.FileRequest; @@ -34,7 +33,6 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; -import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -43,8 +41,6 @@ import java.util.concurrent.Executor; import java.util.stream.Collectors; import java.util.function.Supplier; - -import com.google.gson.annotations.SerializedName; import io.github.resilience4j.circuitbreaker.CircuitBreaker; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.retry.Retry; @@ -101,43 +97,21 @@ public CompletableFuture invoke(final ClientOperation clientOperation, final Object requestObject, final String idempotencyKey, final Map queryParams) { + return invoke(clientOperation, path, authorization, requestObject, idempotencyKey, queryParams, null); + } + + @Override + public CompletableFuture invoke(final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final Object requestObject, + final String idempotencyKey, + final Map queryParams, + final IHeaders headers) { return CompletableFuture.supplyAsync(() -> { - final HttpUriRequest request; - switch (clientOperation) { - case GET: - case GET_CSV_CONTENT: - request = new HttpGet(getRequestUrl(path)); - break; - case PUT: - request = new HttpPut(getRequestUrl(path)); - break; - case POST: - request = new HttpPost(getRequestUrl(path)); - break; - case DELETE: - request = new HttpDelete(getRequestUrl(path)); - break; - case PATCH: - request = new HttpPatch(getRequestUrl(path)); - break; - case QUERY: - final List params = queryParams.entrySet().stream() - .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - try { - request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build()); - } catch (final URISyntaxException e) { - throw new CheckoutException(e); - } - break; - default: - throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation); - } - if (idempotencyKey != null) { - request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey); - } - return performCall(authorization, requestObject, request, clientOperation); + final HttpUriRequest request = buildRequest(clientOperation, path, queryParams, idempotencyKey); + return performCall(authorization, requestObject, request, clientOperation, headers); }, executor); } @@ -157,42 +131,20 @@ public Response invokeSync(final ClientOperation clientOperation, final Object requestObject, final String idempotencyKey, final Map queryParams) { - final HttpUriRequest request; - switch (clientOperation) { - case GET: - case GET_CSV_CONTENT: - request = new HttpGet(getRequestUrl(path)); - break; - case PUT: - request = new HttpPut(getRequestUrl(path)); - break; - case POST: - request = new HttpPost(getRequestUrl(path)); - break; - case DELETE: - request = new HttpDelete(getRequestUrl(path)); - break; - case PATCH: - request = new HttpPatch(getRequestUrl(path)); - break; - case QUERY: - final List params = queryParams.entrySet().stream() - .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - try { - request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build()); - } catch (final URISyntaxException e) { - throw new CheckoutException(e); - } - break; - default: - throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation); - } - if (idempotencyKey != null) { - request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey); - } - - final Supplier callSupplier = () -> performCall(authorization, requestObject, request, clientOperation); + return invokeSync(clientOperation, path, authorization, requestObject, idempotencyKey, queryParams, null); + } + + @Override + public Response invokeSync(final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final Object requestObject, + final String idempotencyKey, + final Map queryParams, + final IHeaders headers) { + final HttpUriRequest request = buildRequest(clientOperation, path, queryParams, idempotencyKey); + + final Supplier callSupplier = () -> performCall(authorization, requestObject, request, clientOperation, headers); return executeWithResilience4j(callSupplier); } @@ -258,13 +210,21 @@ private Response performCall(final SdkAuthorization authorization, final Object requestBody, final HttpUriRequest request, final ClientOperation clientOperation) { + return performCall(authorization, requestBody, request, clientOperation, null); + } + + private Response performCall(final SdkAuthorization authorization, + final Object requestBody, + final HttpUriRequest request, + final ClientOperation clientOperation, + final IHeaders customHeaders) { log.info("{}: {}", clientOperation, request.getURI()); request.setHeader(USER_AGENT, PROJECT_NAME + "/" + getVersionFromManifest()); request.setHeader(ACCEPT, getAcceptHeader(clientOperation)); request.setHeader(AUTHORIZATION, authorization.getAuthorizationHeader()); - // Check and add headers from the request object if needed addHeadersFromRequestBody(requestBody, request); + applyHeaders(customHeaders, request); String currentRequestId = UUID.randomUUID().toString(); @@ -318,19 +278,17 @@ private Response performCall(final SdkAuthorization authorization, } private void addHeadersFromRequestBody(final Object requestBody, final HttpUriRequest request) { - if (requestBody instanceof Headers) { - Headers headers = (Headers) requestBody; - for (Field field : Headers.class.getDeclaredFields()) { - field.setAccessible(true); - try { - Object value = field.get(headers); - if (value != null && !value.toString().isEmpty()) { - SerializedName serializedName = field.getAnnotation(SerializedName.class); - String headerName = serializedName != null ? serializedName.value() : field.getName(); - request.setHeader(headerName, value.toString()); - } - } catch (IllegalAccessException ignored) {} - } + if (requestBody instanceof IHeaders) { + applyHeaders((IHeaders) requestBody, request); + } + } + + private void applyHeaders(final IHeaders headers, final HttpUriRequest request) { + if (headers == null) { + return; + } + for (Map.Entry entry : headers.getHeaders().entrySet()) { + request.setHeader(entry.getKey(), entry.getValue()); } } @@ -363,12 +321,9 @@ private Response handleException(Exception e, String errorMessage) { } private Header[] sanitiseHeaders(final Header[] headers) { - // TODO: discuss whether Cko-Idempotency-Key should also be filtered — it is a per-request - // unique identifier (not a credential), but filtering it reduces exposure in INFO logs. - // Uncomment the line below once agreed. - // .filter(it -> !it.getName().equalsIgnoreCase(CKO_IDEMPOTENCY_KEY)) return Arrays.stream(headers) .filter(it -> !it.getName().equals(AUTHORIZATION)) + .filter(it -> !it.getName().equalsIgnoreCase("Signature")) .toArray(Header[]::new); } @@ -388,6 +343,44 @@ private String getAcceptHeader(final ClientOperation clientOperation) { } } + private HttpUriRequest buildRequest(final ClientOperation clientOperation, final String path, final Map queryParams, final String idempotencyKey) { + final HttpUriRequest request; + switch (clientOperation) { + case GET: + case GET_CSV_CONTENT: + request = new HttpGet(getRequestUrl(path)); + break; + case PUT: + request = new HttpPut(getRequestUrl(path)); + break; + case POST: + request = new HttpPost(getRequestUrl(path)); + break; + case DELETE: + request = new HttpDelete(getRequestUrl(path)); + break; + case PATCH: + request = new HttpPatch(getRequestUrl(path)); + break; + case QUERY: + final List params = queryParams.entrySet().stream() + .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + try { + request = new HttpGet(new URIBuilder(getRequestUrl(path)).addParameters(params).build()); + } catch (final URISyntaxException e) { + throw new CheckoutException(e); + } + break; + default: + throw new UnsupportedOperationException("Unsupported HTTP Method: " + clientOperation); + } + if (idempotencyKey != null) { + request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey); + } + return request; + } + private String getRequestUrl(final String path) { try { return baseUri.resolve(path).toURL().toString(); diff --git a/src/main/java/com/checkout/ApiClient.java b/src/main/java/com/checkout/ApiClient.java index 51502377..4a3b6390 100644 --- a/src/main/java/com/checkout/ApiClient.java +++ b/src/main/java/com/checkout/ApiClient.java @@ -16,6 +16,8 @@ public interface ApiClient { CompletableFuture postAsync(String path, SdkAuthorization authorization, Class responseType, Object request, String idempotencyKey); + CompletableFuture postAsync(String path, SdkAuthorization authorization, Class responseType, Object request, String idempotencyKey, IHeaders headers); + CompletableFuture patchAsync(String path, SdkAuthorization authorization, Type type, Object request, String idempotencyKey); CompletableFuture postAsync(String path, SdkAuthorization authorization, Type responseType, Object request, String idempotencyKey); @@ -47,6 +49,8 @@ public interface ApiClient { T post(String path, SdkAuthorization authorization, Class responseType, Object request, String idempotencyKey); + T post(String path, SdkAuthorization authorization, Class responseType, Object request, String idempotencyKey, IHeaders headers); + T post(String path, SdkAuthorization authorization, Type responseType, Object request, String idempotencyKey); EmptyResponse delete(String path, SdkAuthorization authorization); diff --git a/src/main/java/com/checkout/ApiClientImpl.java b/src/main/java/com/checkout/ApiClientImpl.java index 35e8c8d8..ff20b542 100644 --- a/src/main/java/com/checkout/ApiClientImpl.java +++ b/src/main/java/com/checkout/ApiClientImpl.java @@ -95,6 +95,15 @@ public CompletableFuture postAsync(final String path ); } + @Override + public CompletableFuture postAsync(final String path, final SdkAuthorization authorization, final Class responseType, final Object request, final String idempotencyKey, final IHeaders headers) { + validateParams(PATH, path, AUTHORIZATION, authorization); + return executeAsyncOrSync( + () -> post(path, authorization, responseType, request, idempotencyKey, headers), + () -> sendRequestAsync(POST, path, authorization, request, idempotencyKey, responseType, headers) + ); + } + @Override public CompletableFuture postAsync(final String path, final SdkAuthorization authorization, final Type responseType, final Object request, final String idempotencyKey) { validateParams(PATH, path, AUTHORIZATION, authorization); @@ -236,6 +245,12 @@ private CompletableFuture sendRequestAsync(final Cli .thenApply(response -> deserialize(response, responseType)); } + private CompletableFuture sendRequestAsync(final ClientOperation clientOperation, final String path, final SdkAuthorization authorization, final Object request, final String idempotencyKey, final Type responseType, final IHeaders headers) { + return transport.invoke(clientOperation, path, authorization, request, idempotencyKey, null, headers) + .thenApply(this::errorCheck) + .thenApply(response -> deserialize(response, responseType)); + } + private Response errorCheck(final Response response) { if (!CheckoutUtils.isSuccessHttpStatusCode(response.getStatusCode())) { Map errorDetails = null; @@ -335,6 +350,12 @@ public T post(final String path, final SdkAuthorization return sendRequestSync(POST, path, authorization, request, idempotencyKey, responseType); } + @Override + public T post(final String path, final SdkAuthorization authorization, final Class responseType, final Object request, final String idempotencyKey, final IHeaders headers) { + validateParams(PATH, path, AUTHORIZATION, authorization); + return sendRequestSync(POST, path, authorization, request, idempotencyKey, responseType, headers); + } + @Override public T post(final String path, final SdkAuthorization authorization, final Type responseType, final Object request, final String idempotencyKey) { validateParams(PATH, path, AUTHORIZATION, authorization); @@ -411,4 +432,10 @@ private T sendRequestSync(final ClientOperation clientO return deserialize(checkedResponse, responseType); } + private T sendRequestSync(final ClientOperation clientOperation, final String path, final SdkAuthorization authorization, final Object request, final String idempotencyKey, final Type responseType, final IHeaders headers) { + final Response response = transport.invokeSync(clientOperation, path, authorization, request, idempotencyKey, null, headers); + final Response checkedResponse = errorCheck(response); + return deserialize(checkedResponse, responseType); + } + } diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index 824efbd6..0ec65e95 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -1,6 +1,7 @@ package com.checkout; import com.checkout.accounts.AccountsClient; +import com.checkout.agenticcommerce.AgenticCommerceClient; import com.checkout.balances.BalancesClient; import com.checkout.customers.CustomersClient; import com.checkout.disputes.DisputesClient; @@ -102,4 +103,10 @@ public interface CheckoutApi extends CheckoutApmApi { StandaloneAccountUpdaterClient standaloneAccountUpdaterClient(); + /** + * Returns the client for the Agentic Commerce Protocol (Beta). + * Use to create delegated payment tokens for AI-agent-initiated payments. + */ + AgenticCommerceClient agenticCommerceClient(); + } diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 344371ef..6ff3f576 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -2,6 +2,8 @@ import com.checkout.accounts.AccountsClient; import com.checkout.accounts.AccountsClientImpl; +import com.checkout.agenticcommerce.AgenticCommerceClient; +import com.checkout.agenticcommerce.AgenticCommerceClientImpl; import com.checkout.balances.BalancesClient; import com.checkout.balances.BalancesClientImpl; import com.checkout.customers.CustomersClient; @@ -104,6 +106,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final AmlScreeningClient amlScreeningClient; private final NetworkTokensClient networkTokensClient; private final StandaloneAccountUpdaterClient standaloneAccountUpdaterClient; + private final AgenticCommerceClient agenticCommerceClient; public CheckoutApiImpl(final CheckoutConfiguration configuration) { super(configuration); @@ -142,6 +145,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.amlScreeningClient = new AmlScreeningClientImpl(this.apiClient, configuration); this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); + this.agenticCommerceClient = new AgenticCommerceClientImpl(this.apiClient, configuration); } @Override @@ -277,6 +281,9 @@ public MetadataClient metadataClient() { @Override public StandaloneAccountUpdaterClient standaloneAccountUpdaterClient() { return standaloneAccountUpdaterClient; } + @Override + public AgenticCommerceClient agenticCommerceClient() { return agenticCommerceClient; } + private ApiClient getFilesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new FilesApiUriStrategy(configuration)); } diff --git a/src/main/java/com/checkout/IHeaders.java b/src/main/java/com/checkout/IHeaders.java new file mode 100644 index 00000000..e6bc71ea --- /dev/null +++ b/src/main/java/com/checkout/IHeaders.java @@ -0,0 +1,13 @@ +package com.checkout; + +import java.util.Map; + +/** + * Common interface for custom HTTP headers that can be passed alongside API requests. + * Implementations provide endpoint-specific headers (e.g., Agentic Commerce signatures, + * authentication session headers). + */ +public interface IHeaders { + + Map getHeaders(); +} diff --git a/src/main/java/com/checkout/Transport.java b/src/main/java/com/checkout/Transport.java index 3a1da5a8..c51c9833 100644 --- a/src/main/java/com/checkout/Transport.java +++ b/src/main/java/com/checkout/Transport.java @@ -9,10 +9,14 @@ public interface Transport { CompletableFuture invoke(ClientOperation clientOperation, String path, SdkAuthorization authorization, Object requestObject, String idempotencyKey, Map queryParams); + CompletableFuture invoke(ClientOperation clientOperation, String path, SdkAuthorization authorization, Object requestObject, String idempotencyKey, Map queryParams, IHeaders headers); + CompletableFuture submitFile(String path, SdkAuthorization authorization, AbstractFileRequest fileRequest); Response invokeSync(ClientOperation clientOperation, String path, SdkAuthorization authorization, Object requestObject, String idempotencyKey, Map queryParams); + Response invokeSync(ClientOperation clientOperation, String path, SdkAuthorization authorization, Object requestObject, String idempotencyKey, Map queryParams, IHeaders headers); + Response submitFileSync(String path, SdkAuthorization authorization, AbstractFileRequest fileRequest); } diff --git a/src/main/java/com/checkout/accounts/Headers.java b/src/main/java/com/checkout/accounts/Headers.java index 529d0883..9dce71ab 100644 --- a/src/main/java/com/checkout/accounts/Headers.java +++ b/src/main/java/com/checkout/accounts/Headers.java @@ -1,5 +1,6 @@ package com.checkout.accounts; +import com.checkout.IHeaders; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; @@ -7,12 +8,24 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.util.LinkedHashMap; +import java.util.Map; + @Data @SuperBuilder @NoArgsConstructor @AllArgsConstructor -public class Headers { +public class Headers implements IHeaders { @SerializedName("if-match") private String ifMatch; + + @Override + public Map getHeaders() { + final Map headers = new LinkedHashMap<>(); + if (ifMatch != null) { + headers.put("if-match", ifMatch); + } + return headers; + } } diff --git a/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClient.java b/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClient.java new file mode 100644 index 00000000..fcbafc4e --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClient.java @@ -0,0 +1,14 @@ +package com.checkout.agenticcommerce; + +import com.checkout.agenticcommerce.request.DelegatePaymentHeaders; +import com.checkout.agenticcommerce.request.DelegatePaymentRequest; +import com.checkout.agenticcommerce.response.DelegatePaymentResponse; + +import java.util.concurrent.CompletableFuture; + +public interface AgenticCommerceClient { + + CompletableFuture delegatePayment(DelegatePaymentRequest request, DelegatePaymentHeaders headers); + + DelegatePaymentResponse delegatePaymentSync(DelegatePaymentRequest request, DelegatePaymentHeaders headers); +} diff --git a/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClientImpl.java b/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClientImpl.java new file mode 100644 index 00000000..08a4b302 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/AgenticCommerceClientImpl.java @@ -0,0 +1,46 @@ +package com.checkout.agenticcommerce; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorizationType; +import com.checkout.agenticcommerce.request.DelegatePaymentHeaders; +import com.checkout.agenticcommerce.request.DelegatePaymentRequest; +import com.checkout.agenticcommerce.response.DelegatePaymentResponse; +import com.checkout.common.CheckoutUtils; + +import java.util.concurrent.CompletableFuture; + +public class AgenticCommerceClientImpl extends AbstractClient implements AgenticCommerceClient { + + private static final String AGENTIC_COMMERCE_PATH = "agentic_commerce"; + private static final String DELEGATE_PAYMENT_PATH = "delegate_payment"; + + public AgenticCommerceClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); + } + + @Override + public CompletableFuture delegatePayment(final DelegatePaymentRequest request, final DelegatePaymentHeaders headers) { + CheckoutUtils.validateParams("request", request, "headers", headers); + return apiClient.postAsync( + buildPath(AGENTIC_COMMERCE_PATH, DELEGATE_PAYMENT_PATH), + sdkAuthorization(), + DelegatePaymentResponse.class, + request, + null, + headers); + } + + @Override + public DelegatePaymentResponse delegatePaymentSync(final DelegatePaymentRequest request, final DelegatePaymentHeaders headers) { + CheckoutUtils.validateParams("request", request, "headers", headers); + return apiClient.post( + buildPath(AGENTIC_COMMERCE_PATH, DELEGATE_PAYMENT_PATH), + sdkAuthorization(), + DelegatePaymentResponse.class, + request, + null, + headers); + } +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/AllowanceReason.java b/src/main/java/com/checkout/agenticcommerce/request/AllowanceReason.java new file mode 100644 index 00000000..d09feb12 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/AllowanceReason.java @@ -0,0 +1,9 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; + +public enum AllowanceReason { + + @SerializedName("one_time") + ONE_TIME +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/CardFundingType.java b/src/main/java/com/checkout/agenticcommerce/request/CardFundingType.java new file mode 100644 index 00000000..288186ae --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/CardFundingType.java @@ -0,0 +1,15 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; + +public enum CardFundingType { + + @SerializedName("credit") + CREDIT, + + @SerializedName("debit") + DEBIT, + + @SerializedName("prepaid") + PREPAID +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/CardNumberType.java b/src/main/java/com/checkout/agenticcommerce/request/CardNumberType.java new file mode 100644 index 00000000..33b0f274 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/CardNumberType.java @@ -0,0 +1,18 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; + +public enum CardNumberType { + + /** + * A Funding Primary Account Number. The card number printed on the card. + */ + @SerializedName("fpan") + FPAN, + + /** + * A provisioned network token that represents the underlying card. + */ + @SerializedName("network_token") + NETWORK_TOKEN +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentAllowance.java b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentAllowance.java new file mode 100644 index 00000000..7e631593 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentAllowance.java @@ -0,0 +1,73 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * The spending constraints that define what the delegated payment token is authorized for. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelegatePaymentAllowance { + + /** + * Why this allowance is being granted (e.g. one-time use). + *

+ * [Required] + */ + private AllowanceReason reason; + + /** + * Maximum chargeable amount in the minor currency unit. + *

+ * [Required] + */ + @SerializedName("max_amount") + private Long maxAmount; + + /** + * ISO 4217 alphabetic currency code. + *

+ * [Required] + *

+ * Length: >= 3 characters + *

+ * Length: <= 3 characters + */ + private String currency; + + /** + * Merchant that will process the payment. + *

+ * [Required] + *

+ * Length: <= 256 characters + */ + @SerializedName("merchant_id") + private String merchantId; + + /** + * Checkout session associated with this delegated payment. + *

+ * [Required] + */ + @SerializedName("checkout_session_id") + private String checkoutSessionId; + + /** + * When the delegated payment authorization expires (must be in the future). + *

+ * [Required] + *

+ * Format: date-time (RFC 3339) + */ + @SerializedName("expires_at") + private Instant expiresAt; +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentBillingAddress.java b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentBillingAddress.java new file mode 100644 index 00000000..20029d86 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentBillingAddress.java @@ -0,0 +1,83 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The customer billing address associated with the delegated payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelegatePaymentBillingAddress { + + /** + * Customer or cardholder full name. + *

+ * [Required] + *

+ * Length: <= 256 characters + */ + private String name; + + /** + * First line of the street address. + *

+ * [Required] + *

+ * Length: <= 60 characters + */ + @SerializedName("line_one") + private String lineOne; + + /** + * Second line of the street address. + *

+ * [Optional] + *

+ * Length: <= 60 characters + */ + @SerializedName("line_two") + private String lineTwo; + + /** + * City or locality. + *

+ * [Required] + *

+ * Length: <= 60 characters + */ + private String city; + + /** + * State, county, province, or region. + *

+ * [Optional] + */ + private String state; + + /** + * Postal or ZIP code. + *

+ * [Required] + *

+ * Length: <= 20 characters + */ + @SerializedName("postal_code") + private String postalCode; + + /** + * ISO 3166-1 alpha-2 country code. + *

+ * [Required] + *

+ * Length: >= 2 characters + *

+ * Length: <= 2 characters + */ + private String country; +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentHeaders.java b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentHeaders.java new file mode 100644 index 00000000..db320eb0 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentHeaders.java @@ -0,0 +1,44 @@ +package com.checkout.agenticcommerce.request; + +import com.checkout.IHeaders; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Custom HTTP headers required by the Agentic Commerce delegate payment endpoint. + * + * @see POST /agentic_commerce/delegate_payment + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelegatePaymentHeaders implements IHeaders { + + private String signature; + + private String timestamp; + + @Builder.Default + private String apiVersion = null; + + @Override + public Map getHeaders() { + final Map headers = new LinkedHashMap<>(); + if (signature != null) { + headers.put("Signature", signature); + } + if (timestamp != null) { + headers.put("Timestamp", timestamp); + } + if (apiVersion != null) { + headers.put("API-Version", apiVersion); + } + return headers; + } +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentMethod.java b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentMethod.java new file mode 100644 index 00000000..f0e2be64 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentMethod.java @@ -0,0 +1,163 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * The card details for a delegated payment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelegatePaymentMethod { + + /** + * Payment method type. + *

+ * [Required] + *

+ * Enum: "card" + */ + private String type; + + /** + * Whether the number is a full PAN or a network token. + *

+ * [Required] + */ + @SerializedName("card_number_type") + private CardNumberType cardNumberType; + + /** + * Card or token number. + *

+ * [Required] + */ + private String number; + + /** + * Expiry month (two digits). + *

+ * [Optional] + *

+ * Length: >= 2 characters + *

+ * Length: <= 2 characters + */ + @SerializedName("exp_month") + private String expMonth; + + /** + * Expiry year (four digits). + *

+ * [Optional] + *

+ * Length: >= 4 characters + *

+ * Length: <= 4 characters + */ + @SerializedName("exp_year") + private String expYear; + + /** + * Cardholder name as shown on the card. + *

+ * [Optional] + */ + private String name; + + /** + * Card verification value (CVC/CVV). + *

+ * [Optional] + *

+ * Length: >= 3 characters + *

+ * Length: <= 4 characters + */ + private String cvc; + + /** + * Cryptogram for network token transactions (required when {@code card_number_type} is network token). + *

+ * [Optional] + */ + private String cryptogram; + + /** + * Electronic Commerce Indicator (ECI) or Security Level Indicator (SLI) for network token flows. + *

+ * [Optional] + */ + @SerializedName("eci_value") + private String eciValue; + + /** + * Verification checks already performed on the card (array of strings). + *

+ * [Optional] + */ + @SerializedName("checks_performed") + private List checksPerformed; + + /** + * Issuer Identification Number (IIN/BIN), typically the first six digits of the PAN. + *

+ * [Optional] + *

+ * Length: >= 6 characters + *

+ * Length: <= 6 characters + */ + private String iin; + + /** + * Card funding type for display (credit, debit, or prepaid). + *

+ * [Optional] + */ + @SerializedName("display_card_funding_type") + private CardFundingType displayCardFundingType; + + /** + * Wallet type for display (e.g. Apple Pay, Google Pay). + *

+ * [Optional] + */ + @SerializedName("display_wallet_type") + private String displayWalletType; + + /** + * Card brand for display (e.g. Visa, Mastercard). + *

+ * [Optional] + */ + @SerializedName("display_brand") + private String displayBrand; + + /** + * Last four digits of the card for display. + *

+ * [Optional] + *

+ * Length: >= 4 characters + *

+ * Length: <= 4 characters + */ + @SerializedName("display_last4") + private String displayLast4; + + /** + * Additional payment method metadata (string values only). + *

+ * [Required] + */ + private Map metadata; +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentRequest.java b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentRequest.java new file mode 100644 index 00000000..37167b81 --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/DelegatePaymentRequest.java @@ -0,0 +1,56 @@ +package com.checkout.agenticcommerce.request; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * The request body for creating a delegated payment token. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DelegatePaymentRequest { + + /** + * Card payment method details for the delegated payment. + *

[Required]

+ */ + @SerializedName("payment_method") + private DelegatePaymentMethod paymentMethod; + + /** + * Spending constraints authorizing use of the delegated payment token. + *

[Required]

+ */ + private DelegatePaymentAllowance allowance; + + /** + * Customer billing address for the delegated payment. + *

[Optional]

+ *

When provided, {@code name}, {@code line_one}, {@code city}, {@code postal_code}, and {@code country} are required within this object.

+ */ + @SerializedName("billing_address") + private DelegatePaymentBillingAddress billingAddress; + + /** + * Risk assessment signals from the platform for fraud decisioning. + *

[Required]

+ *

Type: array of object; each element: {@code type} [Required] (string), {@code score} [Required] (integer), {@code action} [Required] (string).

+ */ + @SerializedName("risk_signals") + private List riskSignals; + + /** + * Key-value metadata attached to the delegated payment request (string values only). + *

[Required]

+ *

Type: object with string values ({@code additionalProperties}: string).

+ */ + private Map metadata; +} diff --git a/src/main/java/com/checkout/agenticcommerce/request/RiskSignal.java b/src/main/java/com/checkout/agenticcommerce/request/RiskSignal.java new file mode 100644 index 00000000..30b3dc9b --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/request/RiskSignal.java @@ -0,0 +1,37 @@ +package com.checkout.agenticcommerce.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A risk assessment signal provided by the platform to support fraud decisioning. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RiskSignal { + + /** + * Risk signal type identifier. + *

+ * [Required] + */ + private String type; + + /** + * Numeric risk score for this signal. + *

+ * [Required] + */ + private Integer score; + + /** + * Action recommended or taken from the risk assessment. + *

+ * [Required] + */ + private String action; +} diff --git a/src/main/java/com/checkout/agenticcommerce/response/DelegatePaymentResponse.java b/src/main/java/com/checkout/agenticcommerce/response/DelegatePaymentResponse.java new file mode 100644 index 00000000..d105419e --- /dev/null +++ b/src/main/java/com/checkout/agenticcommerce/response/DelegatePaymentResponse.java @@ -0,0 +1,41 @@ +package com.checkout.agenticcommerce.response; + +import com.checkout.HttpMetadata; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; +import java.util.Map; + +/** + * The response returned when a delegated payment token is successfully created. + */ +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class DelegatePaymentResponse extends HttpMetadata { + + /** + * Unique identifier of the provisioned delegated payment token. + *

+ * [Required] + */ + private String id; + + /** + * Timestamp when the token was created. + *

+ * [Required] + *

+ * Format: date-time (RFC 3339) + */ + private Instant created; + + /** + * Response metadata (string values only). + *

+ * [Required] + */ + private Map metadata; +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClient.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClient.java index 4e36a508..4c94137e 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClient.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClient.java @@ -4,81 +4,27 @@ import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayEnrollmentRequest; import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayRegisterDomainRequest; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayDomainListResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentResponse; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentStateResponse; import java.util.concurrent.CompletableFuture; -/** - * Client interface for Google Pay operations. - */ public interface GooglePayClient { - /** - * Enroll an entity to the Google Pay Service. - * - * @param request the enrollment request containing entity ID, email, and terms acceptance - * @return a {@link CompletableFuture} of {@link EmptyResponse} - */ - CompletableFuture enrollEntity(GooglePayEnrollmentRequest request); + CompletableFuture enrollEntity(GooglePayEnrollmentRequest request); - /** - * Register a web domain for a Google Pay enrolled entity. - * - * @param entityId the unique identifier of the enrolled entity - * @param request the domain registration request - * @return a {@link CompletableFuture} of {@link EmptyResponse} - */ CompletableFuture registerDomain(String entityId, GooglePayRegisterDomainRequest request); - /** - * Get registered domains for a Google Pay enrolled entity. - * - * @param entityId the unique identifier of the enrolled entity - * @return a {@link CompletableFuture} of {@link GooglePayDomainListResponse} - */ CompletableFuture getRegisteredDomains(String entityId); - /** - * Get the enrollment state of a Google Pay entity. - * - * @param entityId the unique identifier of the enrolled entity - * @return a {@link CompletableFuture} of {@link GooglePayEnrollmentStateResponse} - */ CompletableFuture getEnrollmentState(String entityId); - // Synchronous methods + GooglePayEnrollmentResponse enrollEntitySync(GooglePayEnrollmentRequest request); - /** - * Enroll an entity to the Google Pay Service (synchronous version). - * - * @param request the enrollment request - * @return {@link EmptyResponse} - */ - EmptyResponse enrollEntitySync(GooglePayEnrollmentRequest request); - - /** - * Register a web domain for a Google Pay enrolled entity (synchronous version). - * - * @param entityId the unique identifier of the enrolled entity - * @param request the domain registration request - * @return {@link EmptyResponse} - */ EmptyResponse registerDomainSync(String entityId, GooglePayRegisterDomainRequest request); - /** - * Get registered domains for a Google Pay enrolled entity (synchronous version). - * - * @param entityId the unique identifier of the enrolled entity - * @return {@link GooglePayDomainListResponse} - */ GooglePayDomainListResponse getRegisteredDomainsSync(String entityId); - /** - * Get the enrollment state of a Google Pay entity (synchronous version). - * - * @param entityId the unique identifier of the enrolled entity - * @return {@link GooglePayEnrollmentStateResponse} - */ GooglePayEnrollmentStateResponse getEnrollmentStateSync(String entityId); } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImpl.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImpl.java index a22ade62..1bb34f6f 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImpl.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImpl.java @@ -9,13 +9,11 @@ import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayEnrollmentRequest; import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayRegisterDomainRequest; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayDomainListResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentResponse; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentStateResponse; import java.util.concurrent.CompletableFuture; -/** - * Implementation of the Google Pay client. - */ public class GooglePayClientImpl extends AbstractClient implements GooglePayClient { private static final String GOOGLEPAY_PATH = "googlepay"; @@ -29,9 +27,9 @@ public GooglePayClientImpl(final ApiClient apiClient, final CheckoutConfiguratio } @Override - public CompletableFuture enrollEntity(final GooglePayEnrollmentRequest request) { + public CompletableFuture enrollEntity(final GooglePayEnrollmentRequest request) { CheckoutUtils.validateParams("request", request); - return apiClient.postAsync(buildPath(GOOGLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(), EmptyResponse.class, request, null); + return apiClient.postAsync(buildPath(GOOGLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(), GooglePayEnrollmentResponse.class, request, null); } @Override @@ -53,9 +51,9 @@ public CompletableFuture getEnrollmentState(fi } @Override - public EmptyResponse enrollEntitySync(final GooglePayEnrollmentRequest request) { + public GooglePayEnrollmentResponse enrollEntitySync(final GooglePayEnrollmentRequest request) { CheckoutUtils.validateParams("request", request); - return apiClient.post(buildPath(GOOGLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(), EmptyResponse.class, request, null); + return apiClient.post(buildPath(GOOGLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(), GooglePayEnrollmentResponse.class, request, null); } @Override diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayEnrollmentRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayEnrollmentRequest.java index e3e88704..2093d6e7 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayEnrollmentRequest.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayEnrollmentRequest.java @@ -6,9 +6,6 @@ import lombok.Data; import lombok.NoArgsConstructor; -/** - * Request to enroll an entity to the Google Pay Service. - */ @Data @Builder @NoArgsConstructor @@ -17,20 +14,29 @@ public final class GooglePayEnrollmentRequest { /** * The unique identifier of the entity to enroll. + *

+ * [Required] + *

*/ - @SerializedName("entityId") + @SerializedName("entity_id") private String entityId; /** * The email address of the user accepting the Google terms of service. + *

+ * [Required] + *

*/ - @SerializedName("emailAddress") + @SerializedName("email_address") private String emailAddress; /** - * Indicates acceptance of the Google terms of service. Must be true to proceed with enrollment. + * Indicates acceptance of the Google terms of service. Must be true to proceed. + *

+ * [Required] + *

*/ - @SerializedName("acceptTermsOfService") + @SerializedName("accept_terms_of_service") private Boolean acceptTermsOfService; } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayRegisterDomainRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayRegisterDomainRequest.java index d3a286c4..c80caa8f 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayRegisterDomainRequest.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/requests/GooglePayRegisterDomainRequest.java @@ -1,13 +1,11 @@ package com.checkout.handlepaymentsandpayouts.googlepay.requests; +import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -/** - * Request to register a web domain for a Google Pay enrolled entity. - */ @Data @Builder @NoArgsConstructor @@ -16,8 +14,12 @@ public final class GooglePayRegisterDomainRequest { /** * The web domain to register for an actively enrolled entity. - * Example: "some.example.com" + *

+ * [Required] + *

+ * Format: hostname */ + @SerializedName("web_domain") private String webDomain; } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayDomainListResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayDomainListResponse.java index 02d5f987..ed94225c 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayDomainListResponse.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayDomainListResponse.java @@ -7,9 +7,6 @@ import java.util.List; -/** - * Response containing the list of registered domains for a Google Pay enrolled entity. - */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -17,6 +14,10 @@ public final class GooglePayDomainListResponse extends HttpMetadata { /** * The list of domains registered for the entity. + *

+ * [Required] + *

+ * Items format: hostname */ private List domains; diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentResponse.java new file mode 100644 index 00000000..283fb4a8 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentResponse.java @@ -0,0 +1,37 @@ +package com.checkout.handlepaymentsandpayouts.googlepay.responses; + +import com.checkout.HttpMetadata; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.Instant; + +/** + * Response returned when enrolling an entity in Google Pay. + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class GooglePayEnrollmentResponse extends HttpMetadata { + + /** + * When the Google terms of service were accepted. + *

+ * [Required] + *

+ * Format: date-time (RFC 3339) + */ + @SerializedName("tos_accepted_time") + private Instant tosAcceptedTime; + + /** + * The current enrollment state of the entity. + *

+ * [Required] + *

+ */ + private GooglePayEnrollmentState state; + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentState.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentState.java new file mode 100644 index 00000000..84e3e9bb --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentState.java @@ -0,0 +1,13 @@ +package com.checkout.handlepaymentsandpayouts.googlepay.responses; + +import com.google.gson.annotations.SerializedName; + +public enum GooglePayEnrollmentState { + + @SerializedName("ACTIVE") + ACTIVE, + + @SerializedName("INACTIVE") + INACTIVE + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentStateResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentStateResponse.java index 55c9374e..c15cf3ea 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentStateResponse.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/googlepay/responses/GooglePayEnrollmentStateResponse.java @@ -5,9 +5,6 @@ import lombok.EqualsAndHashCode; import lombok.ToString; -/** - * Response containing the enrollment state of a Google Pay entity. - */ @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -15,8 +12,10 @@ public final class GooglePayEnrollmentStateResponse extends HttpMetadata { /** * The current enrollment state of the entity. - * Possible values: ACTIVE, INACTIVE + *

+ * [Required] + *

*/ - private String state; + private GooglePayEnrollmentState state; } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java index 85d24e1e..93cb3cee 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/PaymentMethods.java @@ -2,6 +2,7 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bizum.Bizum; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal.PayPal; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.stcpay.Stcpay; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.tabby.Tabby; import lombok.AllArgsConstructor; @@ -33,6 +34,11 @@ public final class PaymentMethods { */ private Tabby tabby; + /** + * PayPal payment method configuration + */ + private PayPal paypal; + /** * Bizum payment method configuration */ diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/bizum/Bizum.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/bizum/Bizum.java index 6d4806ac..b9b5c241 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/bizum/Bizum.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/bizum/Bizum.java @@ -1,21 +1,15 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bizum; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodOptions; -import com.google.gson.annotations.SerializedName; import lombok.Data; import lombok.EqualsAndHashCode; /** - * Bizum payment method configuration + * Bizum payment method configuration. + *

[Read-only]

+ * No method-specific fields; inherits {@link PaymentMethodBase#status status} and {@link PaymentMethodBase#flags flags} only. */ @Data @EqualsAndHashCode(callSuper = true) public final class Bizum extends PaymentMethodBase { - - /** - * Payment method options specific to Bizum - */ - @SerializedName("payment_method_options") - private PaymentMethodOptions paymentMethodOptions; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodAction.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodAction.java index a06cb28f..52bea3d9 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodAction.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodAction.java @@ -1,12 +1,13 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common; +import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** - * Payment method action configuration + * Next-step action returned by the API for a payment method (read-only). */ @Data @Builder @@ -15,17 +16,30 @@ public final class PaymentMethodAction { /** - * The type of action to be performed with the payment method + * The type of action. + *

[Read-only]

+ * {@code sdk} or {@code otp} (varies by payment method). */ private String type; /** - * The client token for payment method authentication + * The client token for the Klarna or PayPal SDK. + *

[Read-only]

*/ + @SerializedName("client_token") private String clientToken; /** - * The session identifier for the payment method session + * The session identifier for the Klarna payment method session. + *

[Read-only]

*/ + @SerializedName("session_id") private String sessionId; + + /** + * The PayPal order ID to use with the PayPal SDK. + *

[Read-only]

+ */ + @SerializedName("order_id") + private String orderId; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodBase.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodBase.java index c279d67a..db991007 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodBase.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodBase.java @@ -1,31 +1,24 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import lombok.Data; import java.util.List; /** - * Base class for all payment methods with common properties + * Base class for all payment methods with common properties (status and flags). */ @Data public abstract class PaymentMethodBase { /** - * The status of the payment method + * The payment method status. + *

[Read-only]

*/ - private String status; + private PaymentMethodStatus status; /** - * Configuration flags for the payment method + * An array of error codes or indicators that highlight missing or invalid information. + *

[Read-only]

*/ private List flags; - - /** - * Default: "disabled" - * The initialization state of the payment method. - * When you create a Payment Setup, this defaults to disabled. - * Enum: "disabled" "enabled" - */ - private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOption.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOption.java deleted file mode 100644 index b86f6e57..00000000 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOption.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * Payment method option configuration - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public final class PaymentMethodOption { - - /** - * The unique identifier for the payment method option - */ - private String id; - - /** - * The status of the payment method option - */ - private String status; - - /** - * Configuration flags for the payment method option - */ - private List flags; - - /** - * The action configuration for the payment method option - */ - private PaymentMethodAction action; -} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOptions.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOptions.java deleted file mode 100644 index 91e1847e..00000000 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodOptions.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common; - -import com.google.gson.annotations.SerializedName; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Payment method options configuration - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public final class PaymentMethodOptions { - - /** - * Klarna SDK configuration options - */ - private PaymentMethodOption sdk; - - /** - * STC Pay full payment option configuration - */ - @SerializedName("pay_in_full") - private PaymentMethodOption payInFull; - - /** - * Tabby installments payment option configuration - */ - private PaymentMethodOption installments; - - /** - * Bizum immediate payment option configuration - */ - @SerializedName("pay_now") - private PaymentMethodOption payNow; -} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java new file mode 100644 index 00000000..98927ebf --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/common/PaymentMethodStatus.java @@ -0,0 +1,22 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common; + +import com.google.gson.annotations.SerializedName; + +public enum PaymentMethodStatus { + + @SerializedName("unavailable") + UNAVAILABLE, + + @SerializedName("action_required") + ACTION_REQUIRED, + + @SerializedName("pending") + PENDING, + + @SerializedName("ready") + READY, + + @SerializedName("available") + AVAILABLE + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/klarna/Klarna.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/klarna/Klarna.java index 5faa2ffe..3ce72ada 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/klarna/Klarna.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/klarna/Klarna.java @@ -1,27 +1,27 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodAction; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodOptions; -import com.google.gson.annotations.SerializedName; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import lombok.Data; import lombok.EqualsAndHashCode; /** - * Klarna payment method configuration + * Klarna payment method configuration. */ @Data @EqualsAndHashCode(callSuper = true) public final class Klarna extends PaymentMethodBase { /** - * The account holder information for Klarna payments + * The initialization state of the payment method. Defaults to disabled. + *

[Optional]

*/ - @SerializedName("account_holder") - private KlarnaAccountHolder accountHolder; + private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; /** - * Payment method options specific to Klarna + * The next available action. Contains type {@code sdk}, {@code client_token}, {@code session_id}. + *

[Read-only]

*/ - @SerializedName("payment_method_options") - private PaymentMethodOptions paymentMethodOptions; + private PaymentMethodAction action; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPal.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPal.java new file mode 100644 index 00000000..03c57f99 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPal.java @@ -0,0 +1,51 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal; + +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodAction; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * The PayPal payment method's details and configuration. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class PayPal extends PaymentMethodBase { + + /** + * The initialization state of the payment method. Defaults to disabled. + *

[Optional]

+ */ + private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; + + /** + * The user action for the PayPal widget. + *

[Optional]

+ */ + @SerializedName("user_action") + private PayPalUserAction userAction; + + /** + * The brand name to display in the PayPal checkout experience. + *

[Optional]

+ * Maximum length: 127 characters. + */ + @SerializedName("brand_name") + private String brandName; + + /** + * Where to obtain the shipping information. + *

[Optional]

+ */ + @SerializedName("shipping_preference") + private PayPalShippingPreference shippingPreference; + + /** + * The next available action. Contains type {@code sdk} and {@code order_id}. + *

[Read-only]

+ */ + private PaymentMethodAction action; + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalShippingPreference.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalShippingPreference.java new file mode 100644 index 00000000..8bb564f1 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalShippingPreference.java @@ -0,0 +1,25 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal; + +import com.google.gson.annotations.SerializedName; + +public enum PayPalShippingPreference { + + /** + * Redacts the shipping address from the PayPal pages. + */ + @SerializedName("no_shipping") + NO_SHIPPING, + + /** + * Obtains the shipping address from the customer's PayPal account. + */ + @SerializedName("get_from_file") + GET_FROM_FILE, + + /** + * Uses the shipping address provided by the merchant. + */ + @SerializedName("set_provided_address") + SET_PROVIDED_ADDRESS + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalUserAction.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalUserAction.java new file mode 100644 index 00000000..bd721a77 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/paypal/PayPalUserAction.java @@ -0,0 +1,21 @@ +package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal; + +import com.google.gson.annotations.SerializedName; + +public enum PayPalUserAction { + + /** + * After the customer clicks the PayPal button, the customer is redirected to a page to enter + * payment details, then they are immediately directed to finalize the payment. + */ + @SerializedName("pay_now") + PAY_NOW, + + /** + * After the customer clicks the PayPal button, the customer is redirected to a page to enter + * payment details and then is redirected back to the merchant's site to review and finalize the payment. + */ + @SerializedName("continue") + CONTINUE + +} diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/stcpay/Stcpay.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/stcpay/Stcpay.java index 2ebadc8c..bfa232d3 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/stcpay/Stcpay.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/stcpay/Stcpay.java @@ -1,8 +1,8 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.stcpay; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodAction; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodOptions; -import com.google.gson.annotations.SerializedName; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import lombok.Data; import lombok.EqualsAndHashCode; @@ -14,13 +14,20 @@ public final class Stcpay extends PaymentMethodBase { /** - * The one-time password (OTP) for STC Pay authentication + * The initialization state of the payment method. Defaults to disabled. + *

[Optional]

+ */ + private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; + + /** + * The one-time password (OTP) for stc pay. + *

[Write-only]

*/ private String otp; /** - * Payment method options specific to STC Pay + * The next available action. Contains type {@code otp} when an OTP is required. + *

[Read-only]

*/ - @SerializedName("payment_method_options") - private PaymentMethodOptions paymentMethodOptions; + private PaymentMethodAction action; } \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/tabby/Tabby.java b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/tabby/Tabby.java index ba542f96..18a0e2a2 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/tabby/Tabby.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/setups/entities/paymentMethods/tabby/Tabby.java @@ -1,21 +1,30 @@ package com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.tabby; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodOptions; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import com.google.gson.annotations.SerializedName; import lombok.Data; import lombok.EqualsAndHashCode; +import java.util.List; + /** - * Tabby payment method configuration + * Tabby payment method configuration. */ @Data @EqualsAndHashCode(callSuper = true) public final class Tabby extends PaymentMethodBase { /** - * Payment method options specific to Tabby + * The initialization state of the payment method. Defaults to disabled. + *

[Optional]

+ */ + private PaymentMethodInitialization initialization = PaymentMethodInitialization.DISABLED; + + /** + * The available payment types for Tabby (for example, {@code installments}). + *

[Read-only]

*/ - @SerializedName("payment_method_options") - private PaymentMethodOptions paymentMethodOptions; + @SerializedName("payment_types") + private List paymentTypes; } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java index 2a137240..fc0a747e 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/CardRequest.java @@ -1,9 +1,12 @@ package com.checkout.issuing.cards.requests.create; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; import com.checkout.issuing.cards.CardType; import com.google.gson.annotations.SerializedName; import lombok.Data; +import java.time.LocalDate; + @Data public abstract class CardRequest { @@ -16,6 +19,24 @@ public abstract class CardRequest { private String reference; + /** + * User's metadata. + *

+ * [Optional] + *

+ */ + private IssuingCardMetadata metadata; + + /** + * Date for the card to be automatically revoked. Must be after the current date. + *

+ * [Optional] + *

+ * Format: yyyy-MM-dd + */ + @SerializedName("revocation_date") + private LocalDate revocationDate; + @SerializedName("card_product_id") private String cardProductId; diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java index 9a6c52e2..a45e6fd3 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/PhysicalCardRequest.java @@ -1,7 +1,10 @@ package com.checkout.issuing.cards.requests.create; import com.checkout.issuing.cards.CardType; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; import com.google.gson.annotations.SerializedName; + +import java.time.LocalDate; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -24,9 +27,13 @@ private PhysicalCardRequest(final String cardholderId, final String cardProductId, final String displayName, final Boolean activateCard, - final ShippingInstruction shippingInstructions) { + final ShippingInstruction shippingInstructions, + final IssuingCardMetadata metadata, + final LocalDate revocationDate) { super(CardType.PHYSICAL, cardholderId, lifetime, reference, cardProductId, displayName, activateCard); this.shippingInstructions = shippingInstructions; + setMetadata(metadata); + setRevocationDate(revocationDate); } } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java b/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java index 402fd508..7de66acd 100644 --- a/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java +++ b/src/main/java/com/checkout/issuing/cards/requests/create/VirtualCardRequest.java @@ -1,7 +1,10 @@ package com.checkout.issuing.cards.requests.create; import com.checkout.issuing.cards.CardType; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; import com.google.gson.annotations.SerializedName; + +import java.time.LocalDate; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -24,8 +27,12 @@ private VirtualCardRequest(final String cardholderId, final String cardProductId, final String displayName, final Boolean activateCard, - final Boolean isSingleUse) { + final Boolean isSingleUse, + final IssuingCardMetadata metadata, + final LocalDate revocationDate) { super(CardType.VIRTUAL, cardholderId, lifetime, reference, cardProductId, displayName, activateCard); this.isSingleUse = isSingleUse; + setMetadata(metadata); + setRevocationDate(revocationDate); } } \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/PaymentRouting.java b/src/main/java/com/checkout/payments/PaymentRouting.java new file mode 100644 index 00000000..5f4259ff --- /dev/null +++ b/src/main/java/com/checkout/payments/PaymentRouting.java @@ -0,0 +1,44 @@ +package com.checkout.payments; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Controls processor attempts at the payment level. + *

+ * [Optional] + *

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentRouting { + + /** + * Specifies the processing rules for the payment. + *

+ * [Optional] + *

+ */ + private List attempts; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RoutingAttempt { + + /** + * The card scheme to use for the payment attempt. + *

+ * [Optional] + *

+ */ + private PaymentRoutingScheme scheme; + } +} diff --git a/src/main/java/com/checkout/payments/PaymentRoutingScheme.java b/src/main/java/com/checkout/payments/PaymentRoutingScheme.java new file mode 100644 index 00000000..09b23f30 --- /dev/null +++ b/src/main/java/com/checkout/payments/PaymentRoutingScheme.java @@ -0,0 +1,55 @@ +package com.checkout.payments; + +import com.google.gson.annotations.SerializedName; + +public enum PaymentRoutingScheme { + + @SerializedName("accel") + ACCEL, + + @SerializedName("amex") + AMEX, + + @SerializedName("cartes_bancaires") + CARTES_BANCAIRES, + + @SerializedName("diners") + DINERS, + + @SerializedName("discover") + DISCOVER, + + @SerializedName("jcb") + JCB, + + @SerializedName("mada") + MADA, + + @SerializedName("maestro") + MAESTRO, + + @SerializedName("mastercard") + MASTERCARD, + + @SerializedName("nyce") + NYCE, + + @SerializedName("omannet") + OMANNET, + + @SerializedName("pulse") + PULSE, + + @SerializedName("shazam") + SHAZAM, + + @SerializedName("star") + STAR, + + @SerializedName("upi") + UPI, + + @SerializedName("visa") + VISA + +} diff --git a/src/main/java/com/checkout/payments/PaymentSubscription.java b/src/main/java/com/checkout/payments/PaymentSubscription.java new file mode 100644 index 00000000..54b61f0f --- /dev/null +++ b/src/main/java/com/checkout/payments/PaymentSubscription.java @@ -0,0 +1,28 @@ +package com.checkout.payments; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * The details of the subscription. + *

+ * [Optional] + *

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentSubscription { + + /** + * The ID or reference linking a series of recurring payments together. + *

+ * [Optional] + *

+ * Length: <= 50 characters + */ + private String id; +} diff --git a/src/main/java/com/checkout/payments/request/PaymentRequest.java b/src/main/java/com/checkout/payments/request/PaymentRequest.java index 1096ace0..0abe92b8 100644 --- a/src/main/java/com/checkout/payments/request/PaymentRequest.java +++ b/src/main/java/com/checkout/payments/request/PaymentRequest.java @@ -15,6 +15,8 @@ import com.checkout.payments.ThreeDSRequest; import com.checkout.payments.request.source.AbstractRequestSource; import com.checkout.payments.PaymentPlan; +import com.checkout.payments.PaymentRouting; +import com.checkout.payments.PaymentSubscription; import com.checkout.payments.sender.PaymentSender; import com.google.gson.annotations.SerializedName; import lombok.Builder; @@ -65,6 +67,16 @@ public final class PaymentRequest { @SerializedName("capture_on") private Instant captureOn; + /** + * The date and time when the Multibanco payment expires in UTC. + *

+ * [Optional] + *

+ * Format: date-time (ISO 8601) + */ + @SerializedName("expire_on") + private Instant expireOn; + private CustomerRequest customer; @SerializedName("billing_descriptor") @@ -112,11 +124,27 @@ public final class PaymentRequest { private PaymentRetryRequest retry; + /** + * The details of the subscription. + *

+ * [Optional] + *

+ */ + private PaymentSubscription subscription; + @Builder.Default private Map metadata = new HashMap<>(); private PaymentSegment segment; - private PaymentInstruction instruction ; + private PaymentInstruction instruction; + + /** + * Controls processor attempts at the payment level. + *

+ * [Optional] + *

+ */ + private PaymentRouting routing; } diff --git a/src/main/java/com/checkout/payments/response/AuthenticationExperience.java b/src/main/java/com/checkout/payments/response/AuthenticationExperience.java new file mode 100644 index 00000000..38565c2e --- /dev/null +++ b/src/main/java/com/checkout/payments/response/AuthenticationExperience.java @@ -0,0 +1,13 @@ +package com.checkout.payments.response; + +import com.google.gson.annotations.SerializedName; + +public enum AuthenticationExperience { + + @SerializedName("google_spa") + GOOGLE_SPA, + + @SerializedName("3ds") + THREE_DS + +} diff --git a/src/main/java/com/checkout/payments/response/GetPaymentResponse.java b/src/main/java/com/checkout/payments/response/GetPaymentResponse.java index 4c0fc57a..a447db04 100644 --- a/src/main/java/com/checkout/payments/response/GetPaymentResponse.java +++ b/src/main/java/com/checkout/payments/response/GetPaymentResponse.java @@ -80,6 +80,14 @@ public final class GetPaymentResponse extends Resource { @SerializedName("3ds") private ThreeDSData threeDSData; + /** + * Provides information relating to the authentication of the payment. + *

+ * [Optional] + *

+ */ + private PaymentResponseAuthentication authentication; + private RiskAssessment risk; private CustomerResponse customer; diff --git a/src/main/java/com/checkout/payments/response/PaymentResponseAuthentication.java b/src/main/java/com/checkout/payments/response/PaymentResponseAuthentication.java new file mode 100644 index 00000000..12e77189 --- /dev/null +++ b/src/main/java/com/checkout/payments/response/PaymentResponseAuthentication.java @@ -0,0 +1,22 @@ +package com.checkout.payments.response; + +import lombok.Data; + +/** + * Provides information relating to the authentication of the payment. + *

+ * [Optional] + *

+ */ +@Data +public class PaymentResponseAuthentication { + + /** + * The authentication experience that was used to authenticate the payment. + *

+ * [Optional] + *

+ */ + private AuthenticationExperience experience; + +} diff --git a/src/test/java/com/checkout/CheckoutApiImplTest.java b/src/test/java/com/checkout/CheckoutApiImplTest.java index fdedc0ad..e180cca5 100644 --- a/src/test/java/com/checkout/CheckoutApiImplTest.java +++ b/src/test/java/com/checkout/CheckoutApiImplTest.java @@ -36,6 +36,7 @@ void shouldInstantiateAndRetrieveClients() { assertNotNull(checkoutApi.paymentLinksClient()); assertNotNull(checkoutApi.forexClient()); assertNotNull(checkoutApi.metadataClient()); + assertNotNull(checkoutApi.agenticCommerceClient()); // APMs assertNotNull(checkoutApi.idealClient()); } diff --git a/src/test/java/com/checkout/HeadersReflectionTest.java b/src/test/java/com/checkout/HeadersReflectionTest.java new file mode 100644 index 00000000..9f54a641 --- /dev/null +++ b/src/test/java/com/checkout/HeadersReflectionTest.java @@ -0,0 +1,83 @@ +package com.checkout; + +import com.checkout.accounts.Headers; +import com.checkout.agenticcommerce.request.DelegatePaymentHeaders; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Validates that all {@link IHeaders} implementations produce correct header maps. + */ +class HeadersReflectionTest { + + @Test + void accountsHeadersShouldProduceCorrectHeaders() { + final Headers headers = Headers.builder() + .ifMatch("etag-value") + .build(); + + final Map map = headers.getHeaders(); + + assertNotNull(map); + assertEquals(1, map.size()); + assertEquals("etag-value", map.get("if-match")); + } + + @Test + void accountsHeadersShouldOmitNullValues() { + final Headers headers = Headers.builder().build(); + + final Map map = headers.getHeaders(); + + assertNotNull(map); + assertTrue(map.isEmpty()); + } + + @Test + void delegatePaymentHeadersShouldProduceAllHeaders() { + final DelegatePaymentHeaders headers = DelegatePaymentHeaders.builder() + .signature("sha256=abc123") + .timestamp("2026-04-08T12:00:00Z") + .apiVersion("2025-01-01") + .build(); + + final Map map = headers.getHeaders(); + + assertNotNull(map); + assertEquals(3, map.size()); + assertEquals("sha256=abc123", map.get("Signature")); + assertEquals("2026-04-08T12:00:00Z", map.get("Timestamp")); + assertEquals("2025-01-01", map.get("API-Version")); + } + + @Test + void delegatePaymentHeadersShouldOmitOptionalApiVersion() { + final DelegatePaymentHeaders headers = DelegatePaymentHeaders.builder() + .signature("sha256=abc123") + .timestamp("2026-04-08T12:00:00Z") + .build(); + + final Map map = headers.getHeaders(); + + assertNotNull(map); + assertEquals(2, map.size()); + assertEquals("sha256=abc123", map.get("Signature")); + assertEquals("2026-04-08T12:00:00Z", map.get("Timestamp")); + assertTrue(!map.containsKey("API-Version")); + } + + @Test + void delegatePaymentHeadersShouldOmitAllNullValues() { + final DelegatePaymentHeaders headers = DelegatePaymentHeaders.builder().build(); + + final Map map = headers.getHeaders(); + + assertNotNull(map); + assertTrue(map.isEmpty()); + } +} diff --git a/src/test/java/com/checkout/agenticcommerce/AgenticCommerceClientImplTest.java b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceClientImplTest.java new file mode 100644 index 00000000..d54ff66e --- /dev/null +++ b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceClientImplTest.java @@ -0,0 +1,215 @@ +package com.checkout.agenticcommerce; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.agenticcommerce.request.AllowanceReason; +import com.checkout.agenticcommerce.request.CardFundingType; +import com.checkout.agenticcommerce.request.CardNumberType; +import com.checkout.agenticcommerce.request.DelegatePaymentAllowance; +import com.checkout.agenticcommerce.request.DelegatePaymentBillingAddress; +import com.checkout.agenticcommerce.request.DelegatePaymentHeaders; +import com.checkout.agenticcommerce.request.DelegatePaymentMethod; +import com.checkout.agenticcommerce.request.DelegatePaymentRequest; +import com.checkout.agenticcommerce.request.RiskSignal; +import com.checkout.agenticcommerce.response.DelegatePaymentResponse; +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.time.Instant; +import java.util.Arrays; +import java.util.Collections; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class AgenticCommerceClientImplTest { + + private AgenticCommerceClient client; + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + @BeforeEach + void setUp() { + when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new AgenticCommerceClientImpl(apiClient, configuration); + } + + @Test + void shouldDelegatePayment() throws ExecutionException, InterruptedException { + final DelegatePaymentRequest request = buildDelegatePaymentRequest(); + final DelegatePaymentHeaders headers = buildHeaders(); + final DelegatePaymentResponse response = mock(DelegatePaymentResponse.class); + + when(apiClient.postAsync( + eq("agentic_commerce/delegate_payment"), + eq(authorization), + eq(DelegatePaymentResponse.class), + eq(request), + isNull(), + eq(headers))) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.delegatePayment(request, headers); + + assertNotNull(future.get()); + assertEquals(response, future.get()); + } + + @Test + void shouldDelegatePaymentSync() { + final DelegatePaymentRequest request = buildDelegatePaymentRequest(); + final DelegatePaymentHeaders headers = buildHeaders(); + final DelegatePaymentResponse response = mock(DelegatePaymentResponse.class); + + when(apiClient.post( + eq("agentic_commerce/delegate_payment"), + eq(authorization), + eq(DelegatePaymentResponse.class), + eq(request), + isNull(), + eq(headers))) + .thenReturn(response); + + final DelegatePaymentResponse result = client.delegatePaymentSync(request, headers); + + assertNotNull(result); + assertEquals(response, result); + } + + @Test + void shouldDelegatePaymentWithMinimalRequest() throws ExecutionException, InterruptedException { + final DelegatePaymentRequest request = buildMinimalDelegatePaymentRequest(); + final DelegatePaymentHeaders headers = buildHeaders(); + final DelegatePaymentResponse response = mock(DelegatePaymentResponse.class); + + when(apiClient.postAsync( + eq("agentic_commerce/delegate_payment"), + eq(authorization), + eq(DelegatePaymentResponse.class), + eq(request), + isNull(), + any(DelegatePaymentHeaders.class))) + .thenReturn(CompletableFuture.completedFuture(response)); + + final DelegatePaymentResponse result = client.delegatePayment(request, headers).get(); + + assertNotNull(result); + assertEquals(response, result); + } + + // --- builders --- + + static DelegatePaymentHeaders buildHeaders() { + return DelegatePaymentHeaders.builder() + .signature("sha256=abc123def456") + .timestamp("2026-04-08T12:00:00Z") + .apiVersion("2025-01-01") + .build(); + } + + static DelegatePaymentRequest buildDelegatePaymentRequest() { + final DelegatePaymentMethod paymentMethod = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4242424242424242") + .expMonth("11") + .expYear("2026") + .name("Jane Doe") + .cvc("123") + .iin("424242") + .displayCardFundingType(CardFundingType.CREDIT) + .displayBrand("Visa") + .displayLast4("4242") + .checksPerformed(Arrays.asList("avs", "cvv")) + .metadata(Collections.singletonMap("issuing_bank", "test")) + .build(); + + final DelegatePaymentAllowance allowance = DelegatePaymentAllowance.builder() + .reason(AllowanceReason.ONE_TIME) + .maxAmount(10000L) + .currency("USD") + .merchantId("cli_vkuhvk4vjn2edkps7dfsq6emqm") + .checkoutSessionId("1PQrsT") + .expiresAt(Instant.parse("2026-10-09T07:20:50.52Z")) + .build(); + + final DelegatePaymentBillingAddress billingAddress = DelegatePaymentBillingAddress.builder() + .name("John Doe") + .lineOne("123 Fake St.") + .lineTwo("Unit 1") + .city("San Francisco") + .state("CA") + .postalCode("12345") + .country("US") + .build(); + + final RiskSignal riskSignal = RiskSignal.builder() + .type("card_testing") + .score(10) + .action("blocked") + .build(); + + return DelegatePaymentRequest.builder() + .paymentMethod(paymentMethod) + .allowance(allowance) + .billingAddress(billingAddress) + .riskSignals(Collections.singletonList(riskSignal)) + .metadata(Collections.singletonMap("campaign", "q4")) + .build(); + } + + static DelegatePaymentRequest buildMinimalDelegatePaymentRequest() { + final DelegatePaymentMethod paymentMethod = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4242424242424242") + .metadata(Collections.singletonMap("issuing_bank", "test")) + .build(); + + final DelegatePaymentAllowance allowance = DelegatePaymentAllowance.builder() + .reason(AllowanceReason.ONE_TIME) + .maxAmount(5000L) + .currency("EUR") + .merchantId("cli_merchant_123") + .checkoutSessionId("sess_abc") + .expiresAt(Instant.parse("2027-01-01T00:00:00Z")) + .build(); + + final RiskSignal riskSignal = RiskSignal.builder() + .type("velocity") + .score(5) + .action("allow") + .build(); + + return DelegatePaymentRequest.builder() + .paymentMethod(paymentMethod) + .allowance(allowance) + .riskSignals(Collections.singletonList(riskSignal)) + .build(); + } +} diff --git a/src/test/java/com/checkout/agenticcommerce/AgenticCommerceSerializationTest.java b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceSerializationTest.java new file mode 100644 index 00000000..b0625468 --- /dev/null +++ b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceSerializationTest.java @@ -0,0 +1,288 @@ +package com.checkout.agenticcommerce; + +import com.checkout.GsonSerializer; +import com.checkout.agenticcommerce.request.AllowanceReason; +import com.checkout.agenticcommerce.request.CardFundingType; +import com.checkout.agenticcommerce.request.CardNumberType; +import com.checkout.agenticcommerce.request.DelegatePaymentAllowance; +import com.checkout.agenticcommerce.request.DelegatePaymentBillingAddress; +import com.checkout.agenticcommerce.request.DelegatePaymentMethod; +import com.checkout.agenticcommerce.request.DelegatePaymentRequest; +import com.checkout.agenticcommerce.request.RiskSignal; +import com.checkout.agenticcommerce.response.DelegatePaymentResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for GSON serialization/deserialization of Agentic Commerce request and response objects. + * Validates snake_case field naming, enum serialization, and Instant handling. + */ +class AgenticCommerceSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldSerializeDelegatePaymentRequestWithCorrectFieldNames() { + final DelegatePaymentRequest request = buildFullRequest(); + + assertDoesNotThrow(() -> { + final String json = serializer.toJson(request); + + assertNotNull(json); + // Top-level fields + assertTrue(json.contains("\"payment_method\""), "Should contain payment_method"); + assertTrue(json.contains("\"allowance\""), "Should contain allowance"); + assertTrue(json.contains("\"billing_address\""), "Should contain billing_address"); + assertTrue(json.contains("\"risk_signals\""), "Should contain risk_signals"); + assertTrue(json.contains("\"metadata\""), "Should contain metadata"); + + // payment_method fields + assertTrue(json.contains("\"card_number_type\""), "Should contain card_number_type"); + assertTrue(json.contains("\"fpan\""), "Should serialize CardNumberType.FPAN as fpan"); + assertTrue(json.contains("\"exp_month\""), "Should contain exp_month"); + assertTrue(json.contains("\"exp_year\""), "Should contain exp_year"); + assertTrue(json.contains("\"checks_performed\""), "Should contain checks_performed"); + assertTrue(json.contains("\"display_card_funding_type\""), "Should contain display_card_funding_type"); + assertTrue(json.contains("\"credit\""), "Should serialize CardFundingType.CREDIT as credit"); + assertTrue(json.contains("\"display_brand\""), "Should contain display_brand"); + assertTrue(json.contains("\"display_last4\""), "Should contain display_last4"); + + // allowance fields + assertTrue(json.contains("\"max_amount\""), "Should contain max_amount"); + assertTrue(json.contains("\"merchant_id\""), "Should contain merchant_id"); + assertTrue(json.contains("\"checkout_session_id\""), "Should contain checkout_session_id"); + assertTrue(json.contains("\"expires_at\""), "Should contain expires_at"); + assertTrue(json.contains("\"one_time\""), "Should serialize AllowanceReason.ONE_TIME as one_time"); + + // billing_address fields + assertTrue(json.contains("\"line_one\""), "Should contain line_one"); + assertTrue(json.contains("\"postal_code\""), "Should contain postal_code"); + }); + } + + @Test + void shouldSerializeNetworkTokenCardNumberType() { + final DelegatePaymentMethod method = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.NETWORK_TOKEN) + .number("4242424242424242") + .metadata(Collections.emptyMap()) + .build(); + + final String json = serializer.toJson(method); + + assertTrue(json.contains("\"network_token\""), "Should serialize NETWORK_TOKEN as network_token"); + } + + @Test + void shouldSerializeAllCardFundingTypes() { + for (final CardFundingType type : CardFundingType.values()) { + final DelegatePaymentMethod method = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4111111111111111") + .displayCardFundingType(type) + .metadata(Collections.emptyMap()) + .build(); + + final String json = serializer.toJson(method); + assertNotNull(json); + assertTrue(json.contains(type.name().toLowerCase()), "Should serialize " + type + " as lowercase"); + } + } + + @Test + void shouldDeserializeDelegatePaymentResponse() { + final String json = "{" + + "\"id\":\"vt_abc123def456ghi789\"," + + "\"created\":\"2026-03-11T10:30:00Z\"," + + "\"metadata\":{\"psp\":\"checkout.com\"}" + + "}"; + + final DelegatePaymentResponse response = serializer.fromJson(json, DelegatePaymentResponse.class); + + assertNotNull(response); + assertEquals("vt_abc123def456ghi789", response.getId()); + assertNotNull(response.getCreated()); + assertEquals(Instant.parse("2026-03-11T10:30:00Z"), response.getCreated()); + assertNotNull(response.getMetadata()); + assertEquals("checkout.com", response.getMetadata().get("psp")); + } + + @Test + void shouldDeserializeResponseWithoutMetadata() { + final String json = "{" + + "\"id\":\"vt_xyz\"," + + "\"created\":\"2026-01-01T00:00:00Z\"" + + "}"; + + final DelegatePaymentResponse response = serializer.fromJson(json, DelegatePaymentResponse.class); + + assertNotNull(response); + assertEquals("vt_xyz", response.getId()); + assertNotNull(response.getCreated()); + } + + @Test + void shouldSerializeRequestWithoutOptionalFields() { + final DelegatePaymentRequest minimal = DelegatePaymentRequest.builder() + .paymentMethod(DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4242424242424242") + .metadata(Collections.emptyMap()) + .build()) + .allowance(DelegatePaymentAllowance.builder() + .reason(AllowanceReason.ONE_TIME) + .maxAmount(1000L) + .currency("USD") + .merchantId("cli_123") + .checkoutSessionId("sess_456") + .expiresAt(Instant.parse("2027-01-01T00:00:00Z")) + .build()) + .riskSignals(Collections.singletonList(RiskSignal.builder() + .type("velocity") + .score(0) + .action("allow") + .build())) + .build(); + + assertDoesNotThrow(() -> { + final String json = serializer.toJson(minimal); + assertNotNull(json); + assertTrue(json.contains("\"payment_method\"")); + assertTrue(json.contains("\"allowance\"")); + assertTrue(json.contains("\"risk_signals\"")); + }); + } + + @Test + void shouldSerializeRiskSignals() { + final RiskSignal signal1 = RiskSignal.builder().type("card_testing").score(10).action("blocked").build(); + final RiskSignal signal2 = RiskSignal.builder().type("velocity").score(5).action("allow").build(); + + final DelegatePaymentRequest request = DelegatePaymentRequest.builder() + .riskSignals(Arrays.asList(signal1, signal2)) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"card_testing\"")); + assertTrue(json.contains("\"blocked\"")); + assertTrue(json.contains("\"velocity\"")); + assertTrue(json.contains("\"allow\"")); + } + + @Test + void shouldRoundTripDelegatePaymentRequest() { + final DelegatePaymentRequest original = buildFullRequest(); + + final String json = serializer.toJson(original); + final DelegatePaymentRequest deserialized = serializer.fromJson(json, DelegatePaymentRequest.class); + + assertNotNull(deserialized); + // payment_method + assertEquals(original.getPaymentMethod().getType(), deserialized.getPaymentMethod().getType()); + assertEquals(original.getPaymentMethod().getCardNumberType(), deserialized.getPaymentMethod().getCardNumberType()); + assertEquals(original.getPaymentMethod().getNumber(), deserialized.getPaymentMethod().getNumber()); + assertEquals(original.getPaymentMethod().getExpMonth(), deserialized.getPaymentMethod().getExpMonth()); + assertEquals(original.getPaymentMethod().getExpYear(), deserialized.getPaymentMethod().getExpYear()); + assertEquals(original.getPaymentMethod().getName(), deserialized.getPaymentMethod().getName()); + assertEquals(original.getPaymentMethod().getCvc(), deserialized.getPaymentMethod().getCvc()); + assertEquals(original.getPaymentMethod().getIin(), deserialized.getPaymentMethod().getIin()); + assertEquals(original.getPaymentMethod().getDisplayCardFundingType(), deserialized.getPaymentMethod().getDisplayCardFundingType()); + assertEquals(original.getPaymentMethod().getDisplayBrand(), deserialized.getPaymentMethod().getDisplayBrand()); + assertEquals(original.getPaymentMethod().getDisplayLast4(), deserialized.getPaymentMethod().getDisplayLast4()); + assertEquals(original.getPaymentMethod().getChecksPerformed(), deserialized.getPaymentMethod().getChecksPerformed()); + assertEquals(original.getPaymentMethod().getMetadata(), deserialized.getPaymentMethod().getMetadata()); + // allowance + assertEquals(original.getAllowance().getReason(), deserialized.getAllowance().getReason()); + assertEquals(original.getAllowance().getMaxAmount(), deserialized.getAllowance().getMaxAmount()); + assertEquals(original.getAllowance().getCurrency(), deserialized.getAllowance().getCurrency()); + assertEquals(original.getAllowance().getMerchantId(), deserialized.getAllowance().getMerchantId()); + assertEquals(original.getAllowance().getCheckoutSessionId(), deserialized.getAllowance().getCheckoutSessionId()); + // billing_address + assertEquals(original.getBillingAddress().getName(), deserialized.getBillingAddress().getName()); + assertEquals(original.getBillingAddress().getLineOne(), deserialized.getBillingAddress().getLineOne()); + assertEquals(original.getBillingAddress().getLineTwo(), deserialized.getBillingAddress().getLineTwo()); + assertEquals(original.getBillingAddress().getCity(), deserialized.getBillingAddress().getCity()); + assertEquals(original.getBillingAddress().getState(), deserialized.getBillingAddress().getState()); + assertEquals(original.getBillingAddress().getPostalCode(), deserialized.getBillingAddress().getPostalCode()); + assertEquals(original.getBillingAddress().getCountry(), deserialized.getBillingAddress().getCountry()); + // risk_signals + assertEquals(original.getRiskSignals().size(), deserialized.getRiskSignals().size()); + assertEquals(original.getRiskSignals().get(0).getType(), deserialized.getRiskSignals().get(0).getType()); + assertEquals(original.getRiskSignals().get(0).getScore(), deserialized.getRiskSignals().get(0).getScore()); + assertEquals(original.getRiskSignals().get(0).getAction(), deserialized.getRiskSignals().get(0).getAction()); + // metadata + assertEquals(original.getMetadata(), deserialized.getMetadata()); + } + + @Test + void shouldRoundTripDelegatePaymentResponse() { + final DelegatePaymentResponse original = new DelegatePaymentResponse(); + // DelegatePaymentResponse has no builder/setters besides Lombok @Data + final String json = "{\"id\":\"vt_round\",\"created\":\"2026-05-01T12:00:00Z\",\"metadata\":{\"key\":\"val\"}}"; + final DelegatePaymentResponse deserialized = serializer.fromJson(json, DelegatePaymentResponse.class); + + final String reJson = serializer.toJson(deserialized); + final DelegatePaymentResponse reDeserialized = serializer.fromJson(reJson, DelegatePaymentResponse.class); + + assertEquals(deserialized.getId(), reDeserialized.getId()); + assertEquals(deserialized.getCreated(), reDeserialized.getCreated()); + assertEquals(deserialized.getMetadata(), reDeserialized.getMetadata()); + } + + // --- helper --- + + private static DelegatePaymentRequest buildFullRequest() { + return DelegatePaymentRequest.builder() + .paymentMethod(DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4242424242424242") + .expMonth("11") + .expYear("2026") + .name("Jane Doe") + .cvc("123") + .iin("424242") + .displayCardFundingType(CardFundingType.CREDIT) + .displayBrand("Visa") + .displayLast4("4242") + .checksPerformed(Arrays.asList("avs", "cvv")) + .metadata(Collections.singletonMap("issuing_bank", "test")) + .build()) + .allowance(DelegatePaymentAllowance.builder() + .reason(AllowanceReason.ONE_TIME) + .maxAmount(10000L) + .currency("USD") + .merchantId("cli_vkuhvk4vjn2edkps7dfsq6emqm") + .checkoutSessionId("1PQrsT") + .expiresAt(Instant.parse("2026-10-09T07:20:50.52Z")) + .build()) + .billingAddress(DelegatePaymentBillingAddress.builder() + .name("John Doe") + .lineOne("123 Fake St.") + .lineTwo("Unit 1") + .city("San Francisco") + .state("CA") + .postalCode("12345") + .country("US") + .build()) + .riskSignals(Collections.singletonList(RiskSignal.builder() + .type("card_testing") + .score(10) + .action("blocked") + .build())) + .metadata(Collections.singletonMap("campaign", "q4")) + .build(); + } +} diff --git a/src/test/java/com/checkout/agenticcommerce/AgenticCommerceTestIT.java b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceTestIT.java new file mode 100644 index 00000000..3b7ce249 --- /dev/null +++ b/src/test/java/com/checkout/agenticcommerce/AgenticCommerceTestIT.java @@ -0,0 +1,155 @@ +package com.checkout.agenticcommerce; + +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.agenticcommerce.request.AllowanceReason; +import com.checkout.agenticcommerce.request.CardFundingType; +import com.checkout.agenticcommerce.request.CardNumberType; +import com.checkout.agenticcommerce.request.DelegatePaymentAllowance; +import com.checkout.agenticcommerce.request.DelegatePaymentBillingAddress; +import com.checkout.agenticcommerce.request.DelegatePaymentMethod; +import com.checkout.agenticcommerce.request.DelegatePaymentRequest; +import com.checkout.agenticcommerce.request.DelegatePaymentHeaders; +import com.checkout.agenticcommerce.request.RiskSignal; +import com.checkout.agenticcommerce.response.DelegatePaymentResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Integration tests for the Agentic Commerce Protocol (Beta). + * + *

These tests are {@link Disabled} because the endpoint requires specific + * merchant configuration and live credentials to be enabled for Agentic Commerce. + * Enable them individually once sandbox access is provisioned.

+ */ +public class AgenticCommerceTestIT extends SandboxTestFixture { + + public AgenticCommerceTestIT() { + super(PlatformType.DEFAULT); + } + + @Disabled("Requires Agentic Commerce to be enabled on the sandbox account") + @Test + void shouldCreateDelegatePaymentToken() { + final DelegatePaymentRequest request = buildFullRequest(); + + final DelegatePaymentResponse response = + blocking(() -> checkoutApi.agenticCommerceClient().delegatePayment(request, buildHeaders())); + + validateResponse(response); + } + + @Disabled("Requires Agentic Commerce to be enabled on the sandbox account") + @Test + void shouldCreateDelegatePaymentTokenWithNetworkToken() { + final DelegatePaymentMethod paymentMethod = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.NETWORK_TOKEN) + .number("4242424242424242") + .expMonth("12") + .expYear("2027") + .cryptogram("gXc5UCLnM6ckD7pjM1TdPA==") + .eciValue("07") + .iin("424242") + .metadata(Collections.singletonMap("issuing_bank", "test")) + .build(); + + final DelegatePaymentRequest request = DelegatePaymentRequest.builder() + .paymentMethod(paymentMethod) + .allowance(buildAllowance()) + .riskSignals(Collections.singletonList(buildRiskSignal())) + .build(); + + final DelegatePaymentResponse response = + blocking(() -> checkoutApi.agenticCommerceClient().delegatePayment(request, buildHeaders())); + + validateResponse(response); + } + + @Disabled("Requires Agentic Commerce to be enabled on the sandbox account") + @Test + void shouldCreateDelegatePaymentTokenSync() { + final DelegatePaymentRequest request = buildFullRequest(); + + final DelegatePaymentResponse response = + checkoutApi.agenticCommerceClient().delegatePaymentSync(request, buildHeaders()); + + validateResponse(response); + } + + // --- helpers --- + + private static DelegatePaymentHeaders buildHeaders() { + return DelegatePaymentHeaders.builder() + .signature("sha256=test_signature") + .timestamp(Instant.now().toString()) + .build(); + } + + private static DelegatePaymentRequest buildFullRequest() { + final DelegatePaymentMethod paymentMethod = DelegatePaymentMethod.builder() + .type("card") + .cardNumberType(CardNumberType.FPAN) + .number("4242424242424242") + .expMonth("11") + .expYear("2026") + .name("Jane Doe") + .cvc("100") + .iin("424242") + .displayCardFundingType(CardFundingType.CREDIT) + .displayBrand("Visa") + .displayLast4("4242") + .checksPerformed(Arrays.asList("avs", "cvv")) + .metadata(Collections.singletonMap("issuing_bank", "test")) + .build(); + + final DelegatePaymentBillingAddress billingAddress = DelegatePaymentBillingAddress.builder() + .name("John Doe") + .lineOne("123 Fake St.") + .city("San Francisco") + .postalCode("12345") + .country("US") + .build(); + + return DelegatePaymentRequest.builder() + .paymentMethod(paymentMethod) + .allowance(buildAllowance()) + .billingAddress(billingAddress) + .riskSignals(Collections.singletonList(buildRiskSignal())) + .metadata(Collections.singletonMap("campaign", "q4")) + .build(); + } + + private static DelegatePaymentAllowance buildAllowance() { + return DelegatePaymentAllowance.builder() + .reason(AllowanceReason.ONE_TIME) + .maxAmount(10000L) + .currency("USD") + .merchantId("cli_vkuhvk4vjn2edkps7dfsq6emqm") + .checkoutSessionId("1PQrsT") + .expiresAt(Instant.now().plus(1, ChronoUnit.HOURS)) + .build(); + } + + private static RiskSignal buildRiskSignal() { + return RiskSignal.builder() + .type("card_testing") + .score(10) + .action("blocked") + .build(); + } + + private static void validateResponse(final DelegatePaymentResponse response) { + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getCreated()); + assertNotNull(response.getMetadata()); + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImplTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImplTest.java index 1cf096a9..d58e0075 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImplTest.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayClientImplTest.java @@ -9,6 +9,7 @@ import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayEnrollmentRequest; import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayRegisterDomainRequest; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayDomainListResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentResponse; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentStateResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -55,13 +56,13 @@ void setUp() { @Test void shouldEnrollEntity() throws ExecutionException, InterruptedException { final GooglePayEnrollmentRequest request = createEnrollmentRequest(); - final EmptyResponse response = mock(EmptyResponse.class); + final GooglePayEnrollmentResponse response = mock(GooglePayEnrollmentResponse.class); when(apiClient.postAsync(eq("googlepay/enrollments"), eq(oAuthAuthorization), - eq(EmptyResponse.class), eq(request), isNull())) + eq(GooglePayEnrollmentResponse.class), eq(request), isNull())) .thenReturn(CompletableFuture.completedFuture(response)); - validateEmptyResponse(response, client.enrollEntity(request).get()); + validateEnrollmentResponse(response, client.enrollEntity(request).get()); } @Test @@ -103,13 +104,13 @@ void shouldGetEnrollmentState() throws ExecutionException, InterruptedException @Test void shouldEnrollEntitySync() { final GooglePayEnrollmentRequest request = createEnrollmentRequest(); - final EmptyResponse response = mock(EmptyResponse.class); + final GooglePayEnrollmentResponse response = mock(GooglePayEnrollmentResponse.class); when(apiClient.post(eq("googlepay/enrollments"), eq(oAuthAuthorization), - eq(EmptyResponse.class), eq(request), isNull())) + eq(GooglePayEnrollmentResponse.class), eq(request), isNull())) .thenReturn(response); - validateEmptyResponse(response, client.enrollEntitySync(request)); + validateEnrollmentResponse(response, client.enrollEntitySync(request)); } @Test @@ -156,6 +157,11 @@ private GooglePayEnrollmentRequest createEnrollmentRequest() { .build(); } + private void validateEnrollmentResponse(final GooglePayEnrollmentResponse expected, final GooglePayEnrollmentResponse actual) { + assertNotNull(actual); + assertEquals(expected, actual); + } + private void validateEmptyResponse(final EmptyResponse expected, final EmptyResponse actual) { assertNotNull(actual); assertEquals(expected, actual); diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePaySerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePaySerializationTest.java new file mode 100644 index 00000000..6f346d47 --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePaySerializationTest.java @@ -0,0 +1,261 @@ +package com.checkout.handlepaymentsandpayouts.googlepay; + +import com.checkout.GsonSerializer; +import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayEnrollmentRequest; +import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayRegisterDomainRequest; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayDomainListResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentState; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentStateResponse; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +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 / serialization tests for Google Pay request and response DTOs. + * Validates {@link com.google.gson.annotations.SerializedName} wiring and {@link java.time.Instant} handling. + */ +class GooglePaySerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // --- GooglePayEnrollmentRequest --- + + @Test + void shouldSerializeGooglePayEnrollmentRequestWithSnakeCaseFieldNames() { + GooglePayEnrollmentRequest request = GooglePayEnrollmentRequest.builder() + .entityId("ent_abc123") + .emailAddress("merchant@example.com") + .acceptTermsOfService(true) + .build(); + + assertDoesNotThrow(() -> { + String json = serializer.toJson(request); + assertNotNull(json); + assertTrue(json.contains("\"entity_id\""), "JSON should contain entity_id"); + assertTrue(json.contains("\"email_address\""), "JSON should contain email_address"); + assertTrue(json.contains("\"accept_terms_of_service\""), "JSON should contain accept_terms_of_service"); + assertTrue(json.contains("ent_abc123"), "JSON should contain entity id value"); + assertTrue(json.contains("merchant@example.com"), "JSON should contain email value"); + assertTrue(json.contains("true"), "JSON should contain accept_terms_of_service true"); + }); + } + + @Test + void shouldRoundTripGooglePayEnrollmentRequest() { + GooglePayEnrollmentRequest original = GooglePayEnrollmentRequest.builder() + .entityId("ent_xyz") + .emailAddress("user@shop.example.com") + .acceptTermsOfService(Boolean.TRUE) + .build(); + + String json = serializer.toJson(original); + GooglePayEnrollmentRequest deserialized = serializer.fromJson(json, GooglePayEnrollmentRequest.class); + + assertNotNull(deserialized); + assertEquals(original.getEntityId(), deserialized.getEntityId()); + assertEquals(original.getEmailAddress(), deserialized.getEmailAddress()); + assertEquals(original.getAcceptTermsOfService(), deserialized.getAcceptTermsOfService()); + } + + @Test + void shouldDeserializeSwaggerExampleGooglePayEnrollmentRequest() { + String swaggerJson = "{" + + "\"entity_id\":\"ent_swagger_1\"," + + "\"email_address\":\"owner@example.com\"," + + "\"accept_terms_of_service\":true" + + "}"; + + GooglePayEnrollmentRequest request = serializer.fromJson(swaggerJson, GooglePayEnrollmentRequest.class); + + assertNotNull(request); + assertEquals("ent_swagger_1", request.getEntityId()); + assertEquals("owner@example.com", request.getEmailAddress()); + assertEquals(Boolean.TRUE, request.getAcceptTermsOfService()); + } + + // --- GooglePayRegisterDomainRequest --- + + @Test + void shouldSerializeGooglePayRegisterDomainRequestWithWebDomainSnakeCase() { + GooglePayRegisterDomainRequest request = GooglePayRegisterDomainRequest.builder() + .webDomain("pay.example.com") + .build(); + + String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"web_domain\""), "JSON should contain web_domain"); + assertTrue(json.contains("pay.example.com"), "JSON should contain domain value"); + } + + @Test + void shouldRoundTripGooglePayRegisterDomainRequest() { + GooglePayRegisterDomainRequest original = GooglePayRegisterDomainRequest.builder() + .webDomain("checkout.merchant.com") + .build(); + + String json = serializer.toJson(original); + GooglePayRegisterDomainRequest deserialized = serializer.fromJson(json, GooglePayRegisterDomainRequest.class); + + assertNotNull(deserialized); + assertEquals(original.getWebDomain(), deserialized.getWebDomain()); + } + + @Test + void shouldDeserializeSwaggerExampleGooglePayRegisterDomainRequest() { + String swaggerJson = "{\"web_domain\":\"registered.example.com\"}"; + + GooglePayRegisterDomainRequest request = serializer.fromJson(swaggerJson, GooglePayRegisterDomainRequest.class); + + assertNotNull(request); + assertEquals("registered.example.com", request.getWebDomain()); + } + + // --- GooglePayEnrollmentResponse --- + + @Test + void shouldDeserializeGooglePayEnrollmentResponseWithInactiveState() { + String json = "{" + + "\"tos_accepted_time\":\"2023-01-01T00:00:00Z\"," + + "\"state\":\"INACTIVE\"" + + "}"; + + GooglePayEnrollmentResponse response = serializer.fromJson(json, GooglePayEnrollmentResponse.class); + + assertNotNull(response); + assertEquals(Instant.parse("2023-01-01T00:00:00Z"), response.getTosAcceptedTime()); + assertEquals(GooglePayEnrollmentState.INACTIVE, response.getState()); + } + + @Test + void shouldRoundTripGooglePayEnrollmentResponse() { + GooglePayEnrollmentResponse original = new GooglePayEnrollmentResponse(); + original.setTosAcceptedTime(Instant.parse("2024-10-02T15:01:23Z")); + original.setState(GooglePayEnrollmentState.ACTIVE); + + String json = serializer.toJson(original); + GooglePayEnrollmentResponse deserialized = serializer.fromJson(json, GooglePayEnrollmentResponse.class); + + assertNotNull(deserialized); + assertEquals(original.getTosAcceptedTime(), deserialized.getTosAcceptedTime()); + assertEquals(original.getState(), deserialized.getState()); + } + + @Test + void shouldDeserializeSwaggerExampleGooglePayEnrollmentResponse() { + String swaggerJson = "{" + + "\"tos_accepted_time\":\"2024-10-02T15:01:23Z\"," + + "\"state\":\"ACTIVE\"" + + "}"; + + GooglePayEnrollmentResponse response = serializer.fromJson(swaggerJson, GooglePayEnrollmentResponse.class); + + assertNotNull(response); + assertEquals(Instant.parse("2024-10-02T15:01:23Z"), response.getTosAcceptedTime()); + assertEquals(GooglePayEnrollmentState.ACTIVE, response.getState()); + } + + // --- GooglePayEnrollmentStateResponse --- + + @Test + void shouldDeserializeGooglePayEnrollmentStateResponseActive() { + String json = "{\"state\":\"ACTIVE\"}"; + + GooglePayEnrollmentStateResponse response = serializer.fromJson(json, GooglePayEnrollmentStateResponse.class); + + assertNotNull(response); + assertEquals(GooglePayEnrollmentState.ACTIVE, response.getState()); + } + + @Test + void shouldDeserializeGooglePayEnrollmentStateResponseInactive() { + String json = "{\"state\":\"INACTIVE\"}"; + + GooglePayEnrollmentStateResponse response = serializer.fromJson(json, GooglePayEnrollmentStateResponse.class); + + assertNotNull(response); + assertEquals(GooglePayEnrollmentState.INACTIVE, response.getState()); + } + + @Test + void shouldRoundTripGooglePayEnrollmentStateResponse() { + GooglePayEnrollmentStateResponse original = new GooglePayEnrollmentStateResponse(); + original.setState(GooglePayEnrollmentState.INACTIVE); + + String json = serializer.toJson(original); + GooglePayEnrollmentStateResponse deserialized = serializer.fromJson(json, GooglePayEnrollmentStateResponse.class); + + assertNotNull(deserialized); + assertEquals(original.getState(), deserialized.getState()); + } + + // --- GooglePayDomainListResponse --- + + @Test + void shouldDeserializeGooglePayDomainListResponse() { + String json = "{\"domains\":[\"example.com\",\"shop.example.com\"]}"; + + GooglePayDomainListResponse response = serializer.fromJson(json, GooglePayDomainListResponse.class); + + assertNotNull(response); + assertNotNull(response.getDomains()); + assertEquals(2, response.getDomains().size()); + assertEquals("example.com", response.getDomains().get(0)); + assertEquals("shop.example.com", response.getDomains().get(1)); + } + + @Test + void shouldRoundTripGooglePayDomainListResponse() { + GooglePayDomainListResponse original = new GooglePayDomainListResponse(); + original.setDomains(Arrays.asList("example.com", "shop.example.com")); + + String json = serializer.toJson(original); + GooglePayDomainListResponse deserialized = serializer.fromJson(json, GooglePayDomainListResponse.class); + + assertNotNull(deserialized); + assertNotNull(deserialized.getDomains()); + assertEquals(original.getDomains(), deserialized.getDomains()); + } + + @Test + void shouldDeserializeSwaggerExampleGooglePayDomainListResponse() { + String swaggerJson = "{\"domains\":[\"example.com\",\"shop.example.com\"]}"; + + GooglePayDomainListResponse response = serializer.fromJson(swaggerJson, GooglePayDomainListResponse.class); + + assertNotNull(response); + assertEquals(Arrays.asList("example.com", "shop.example.com"), response.getDomains()); + } + + @Test + void shouldSerializeGooglePayEnrollmentRequestWithAcceptTermsFalse() { + GooglePayEnrollmentRequest request = GooglePayEnrollmentRequest.builder() + .entityId("ent_1") + .emailAddress("a@b.co") + .acceptTermsOfService(false) + .build(); + + String json = serializer.toJson(request); + assertTrue(json.contains("false")); + GooglePayEnrollmentRequest back = serializer.fromJson(json, GooglePayEnrollmentRequest.class); + assertEquals(Boolean.FALSE, back.getAcceptTermsOfService()); + } + + @Test + void shouldDeserializeGooglePayDomainListResponseEmptyList() { + String json = "{\"domains\":[]}"; + + GooglePayDomainListResponse response = serializer.fromJson(json, GooglePayDomainListResponse.class); + + assertNotNull(response); + assertNotNull(response.getDomains()); + assertTrue(response.getDomains().isEmpty()); + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayTestIT.java index 685e2716..43f28893 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayTestIT.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/googlepay/GooglePayTestIT.java @@ -6,6 +6,7 @@ import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayEnrollmentRequest; import com.checkout.handlepaymentsandpayouts.googlepay.requests.GooglePayRegisterDomainRequest; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayDomainListResponse; +import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentResponse; import com.checkout.handlepaymentsandpayouts.googlepay.responses.GooglePayEnrollmentStateResponse; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -35,7 +36,7 @@ public GooglePayTestIT() { @Test void shouldEnrollEntity() { - final EmptyResponse response = blocking(() -> + final GooglePayEnrollmentResponse response = blocking(() -> checkoutApi.googlePayClient().enrollEntity(createEnrollmentRequest())); assertNotNull(response); @@ -69,7 +70,7 @@ void shouldGetEnrollmentState() { @Test void shouldEnrollEntitySync() { - final EmptyResponse response = checkoutApi.googlePayClient().enrollEntitySync(createEnrollmentRequest()); + final GooglePayEnrollmentResponse response = checkoutApi.googlePayClient().enrollEntitySync(createEnrollmentRequest()); assertNotNull(response); } diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java new file mode 100644 index 00000000..64fe518f --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentMethodsSerializationTest.java @@ -0,0 +1,548 @@ +package com.checkout.handlepaymentsandpayouts.setups; + +import com.checkout.GsonSerializer; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.PaymentMethods; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.bizum.Bizum; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodAction; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodBase; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodStatus; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal.PayPal; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal.PayPalShippingPreference; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.paypal.PayPalUserAction; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.stcpay.Stcpay; +import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.tabby.Tabby; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; + +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; + +/** + * GSON serialization / deserialization tests for Payment Setup {@link PaymentMethods} DTOs + * and related payment method types (aligned with swagger {@code CreatePaymentSetup.payment_methods}). + */ +class PaymentMethodsSerializationTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + @Test + void shouldDeserializeKlarnaWithAction() { + final String json = "{" + + "\"status\":\"action_required\"," + + "\"flags\":[\"missing_phone\"]," + + "\"initialization\":\"enabled\"," + + "\"action\":{" + + "\"type\":\"sdk\"," + + "\"client_token\":\"ct_klarna_abc\"," + + "\"session_id\":\"sess_111\"" + + "}" + + "}"; + + final Klarna klarna = serializer.fromJson(json, Klarna.class); + + assertNotNull(klarna); + assertEquals(PaymentMethodStatus.ACTION_REQUIRED, klarna.getStatus()); + assertEquals(Collections.singletonList("missing_phone"), klarna.getFlags()); + assertEquals(PaymentMethodInitialization.ENABLED, klarna.getInitialization()); + assertNotNull(klarna.getAction()); + assertEquals("sdk", klarna.getAction().getType()); + assertEquals("ct_klarna_abc", klarna.getAction().getClientToken()); + assertEquals("sess_111", klarna.getAction().getSessionId()); + assertNull(klarna.getAction().getOrderId()); + } + + @Test + void shouldRoundTripKlarnaWithAllProperties() { + final Klarna original = new Klarna(); + original.setStatus(PaymentMethodStatus.READY); + original.setFlags(Arrays.asList("a", "b")); + original.setInitialization(PaymentMethodInitialization.ENABLED); + original.setAction(PaymentMethodAction.builder() + .type("sdk") + .clientToken("tok") + .sessionId("sid") + .orderId(null) + .build()); + + final String json = serializer.toJson(original); + final Klarna restored = serializer.fromJson(json, Klarna.class); + + assertNotNull(restored); + assertEquals(original.getStatus(), restored.getStatus()); + assertEquals(original.getFlags(), restored.getFlags()); + assertEquals(original.getInitialization(), restored.getInitialization()); + assertNotNull(restored.getAction()); + assertEquals(original.getAction().getType(), restored.getAction().getType()); + assertEquals(original.getAction().getClientToken(), restored.getAction().getClientToken()); + assertEquals(original.getAction().getSessionId(), restored.getAction().getSessionId()); + assertEquals(original.getAction().getOrderId(), restored.getAction().getOrderId()); + } + + @Test + void shouldDeserializeStcpayWithOtpAction() { + final String json = "{" + + "\"status\":\"pending\"," + + "\"flags\":[\"stc_flag\"]," + + "\"initialization\":\"enabled\"," + + "\"otp\":\"123456\"," + + "\"action\":{\"type\":\"otp\"}" + + "}"; + + final Stcpay stcpay = serializer.fromJson(json, Stcpay.class); + + assertNotNull(stcpay); + assertEquals(PaymentMethodStatus.PENDING, stcpay.getStatus()); + assertEquals(Collections.singletonList("stc_flag"), stcpay.getFlags()); + assertEquals(PaymentMethodInitialization.ENABLED, stcpay.getInitialization()); + assertEquals("123456", stcpay.getOtp()); + assertNotNull(stcpay.getAction()); + assertEquals("otp", stcpay.getAction().getType()); + assertNull(stcpay.getAction().getClientToken()); + assertNull(stcpay.getAction().getSessionId()); + assertNull(stcpay.getAction().getOrderId()); + } + + @Test + void shouldSerializeStcpayOtp() { + final Stcpay stcpay = new Stcpay(); + stcpay.setStatus(PaymentMethodStatus.AVAILABLE); + stcpay.setFlags(Collections.emptyList()); + stcpay.setInitialization(PaymentMethodInitialization.DISABLED); + stcpay.setOtp("654321"); + + final String json = serializer.toJson(stcpay); + + assertTrue(json.contains("\"otp\"")); + assertTrue(json.contains("654321")); + } + + @Test + void shouldRoundTripStcpayWithAllProperties() { + final Stcpay original = new Stcpay(); + original.setStatus(PaymentMethodStatus.AVAILABLE); + original.setFlags(Collections.singletonList("f")); + original.setInitialization(PaymentMethodInitialization.DISABLED); + original.setOtp("111222"); + original.setAction(PaymentMethodAction.builder().type("otp").build()); + + final String json = serializer.toJson(original); + final Stcpay restored = serializer.fromJson(json, Stcpay.class); + + assertNotNull(restored); + assertEquals(original.getStatus(), restored.getStatus()); + assertEquals(original.getFlags(), restored.getFlags()); + assertEquals(original.getInitialization(), restored.getInitialization()); + assertEquals(original.getOtp(), restored.getOtp()); + assertNotNull(restored.getAction()); + assertEquals("otp", restored.getAction().getType()); + } + + @Test + void shouldDeserializeTabbyWithPaymentTypes() { + final String json = "{" + + "\"status\":\"ready\"," + + "\"flags\":[]," + + "\"initialization\":\"enabled\"," + + "\"payment_types\":[\"pay_later\"]" + + "}"; + + final Tabby tabby = serializer.fromJson(json, Tabby.class); + + assertNotNull(tabby); + assertEquals(PaymentMethodStatus.READY, tabby.getStatus()); + assertEquals(Collections.emptyList(), tabby.getFlags()); + assertEquals(PaymentMethodInitialization.ENABLED, tabby.getInitialization()); + assertEquals(Collections.singletonList("pay_later"), tabby.getPaymentTypes()); + } + + @Test + void shouldRoundTripTabbyWithAllProperties() { + final Tabby original = new Tabby(); + original.setStatus(PaymentMethodStatus.UNAVAILABLE); + original.setFlags(Arrays.asList("t1", "t2")); + original.setInitialization(PaymentMethodInitialization.DISABLED); + original.setPaymentTypes(Arrays.asList("pay_later", "installments")); + + final String json = serializer.toJson(original); + final Tabby restored = serializer.fromJson(json, Tabby.class); + + assertNotNull(restored); + assertEquals(original.getStatus(), restored.getStatus()); + assertEquals(original.getFlags(), restored.getFlags()); + assertEquals(original.getInitialization(), restored.getInitialization()); + assertEquals(original.getPaymentTypes(), restored.getPaymentTypes()); + } + + @Test + void shouldDeserializePayPalWithAction() { + final String json = "{" + + "\"status\":\"available\"," + + "\"flags\":[\"paypal_flag\"]," + + "\"initialization\":\"enabled\"," + + "\"user_action\":\"pay_now\"," + + "\"brand_name\":\"Test Brand\"," + + "\"shipping_preference\":\"no_shipping\"," + + "\"action\":{" + + "\"type\":\"sdk\"," + + "\"order_id\":\"ord_paypal_1\"" + + "}" + + "}"; + + final PayPal paypal = serializer.fromJson(json, PayPal.class); + + assertNotNull(paypal); + assertEquals(PaymentMethodStatus.AVAILABLE, paypal.getStatus()); + assertEquals(Collections.singletonList("paypal_flag"), paypal.getFlags()); + assertEquals(PaymentMethodInitialization.ENABLED, paypal.getInitialization()); + assertEquals(PayPalUserAction.PAY_NOW, paypal.getUserAction()); + assertEquals("Test Brand", paypal.getBrandName()); + assertEquals(PayPalShippingPreference.NO_SHIPPING, paypal.getShippingPreference()); + assertNotNull(paypal.getAction()); + assertEquals("sdk", paypal.getAction().getType()); + assertEquals("ord_paypal_1", paypal.getAction().getOrderId()); + assertNull(paypal.getAction().getClientToken()); + assertNull(paypal.getAction().getSessionId()); + } + + @Test + void shouldRoundTripPayPalWithAllProperties() { + final PayPal original = new PayPal(); + original.setStatus(PaymentMethodStatus.PENDING); + original.setFlags(Collections.singletonList("p")); + original.setInitialization(PaymentMethodInitialization.ENABLED); + original.setUserAction(PayPalUserAction.CONTINUE); + original.setBrandName("Acme"); + original.setShippingPreference(PayPalShippingPreference.GET_FROM_FILE); + original.setAction(PaymentMethodAction.builder() + .type("sdk") + .orderId("oid") + .clientToken(null) + .sessionId(null) + .build()); + + final String json = serializer.toJson(original); + final PayPal restored = serializer.fromJson(json, PayPal.class); + + assertNotNull(restored); + assertEquals(original.getStatus(), restored.getStatus()); + assertEquals(original.getFlags(), restored.getFlags()); + assertEquals(original.getInitialization(), restored.getInitialization()); + assertEquals(original.getUserAction(), restored.getUserAction()); + assertEquals(original.getBrandName(), restored.getBrandName()); + assertEquals(original.getShippingPreference(), restored.getShippingPreference()); + assertNotNull(restored.getAction()); + assertEquals(original.getAction().getType(), restored.getAction().getType()); + assertEquals(original.getAction().getOrderId(), restored.getAction().getOrderId()); + } + + @Test + void shouldDeserializeBizumWithoutInitialization() { + final String json = "{" + + "\"status\":\"unavailable\"," + + "\"flags\":[\"bizum_err\"]" + + "}"; + + final Bizum bizum = serializer.fromJson(json, Bizum.class); + + assertNotNull(bizum); + assertEquals(PaymentMethodStatus.UNAVAILABLE, bizum.getStatus()); + assertEquals(Collections.singletonList("bizum_err"), bizum.getFlags()); + assertFalse(hasFieldNamed(PaymentMethodBase.class, "initialization")); + assertFalse(hasFieldNamed(Bizum.class, "initialization")); + } + + @Test + void shouldRoundTripBizumWithAllProperties() { + final Bizum original = new Bizum(); + original.setStatus(PaymentMethodStatus.ACTION_REQUIRED); + original.setFlags(Arrays.asList("x", "y")); + + final String json = serializer.toJson(original); + final Bizum restored = serializer.fromJson(json, Bizum.class); + + assertNotNull(restored); + assertEquals(original.getStatus(), restored.getStatus()); + assertEquals(original.getFlags(), restored.getFlags()); + } + + @Test + void shouldSerializePaymentMethodsContainer() { + final Klarna klarna = new Klarna(); + klarna.setStatus(PaymentMethodStatus.READY); + klarna.setFlags(Collections.singletonList("k")); + klarna.setInitialization(PaymentMethodInitialization.ENABLED); + + final PayPal paypal = new PayPal(); + paypal.setStatus(PaymentMethodStatus.AVAILABLE); + paypal.setFlags(Collections.emptyList()); + paypal.setInitialization(PaymentMethodInitialization.DISABLED); + paypal.setBrandName("Shop"); + + final PaymentMethods methods = PaymentMethods.builder() + .klarna(klarna) + .paypal(paypal) + .build(); + + final String json = serializer.toJson(methods); + + assertTrue(json.contains("\"klarna\"")); + assertTrue(json.contains("\"paypal\"")); + assertTrue(json.contains("\"ready\"")); + assertTrue(json.contains("\"available\"")); + assertTrue(json.contains("Shop")); + } + + @Test + void shouldRoundTripPaymentMethodsContainer() { + final PaymentMethods original = PaymentMethods.builder() + .klarna(buildSampleKlarna()) + .stcpay(buildSampleStcpay()) + .tabby(buildSampleTabby()) + .paypal(buildSamplePayPal()) + .bizum(buildSampleBizum()) + .build(); + + final String json = serializer.toJson(original); + final PaymentMethods restored = serializer.fromJson(json, PaymentMethods.class); + + assertNotNull(restored); + assertNotNull(restored.getKlarna()); + assertNotNull(restored.getStcpay()); + assertNotNull(restored.getTabby()); + assertNotNull(restored.getPaypal()); + assertNotNull(restored.getBizum()); + + assertEquals(original.getKlarna().getStatus(), restored.getKlarna().getStatus()); + assertEquals(original.getKlarna().getFlags(), restored.getKlarna().getFlags()); + assertEquals(original.getKlarna().getInitialization(), restored.getKlarna().getInitialization()); + assertEquals(original.getKlarna().getAction().getType(), restored.getKlarna().getAction().getType()); + + assertEquals(original.getStcpay().getOtp(), restored.getStcpay().getOtp()); + assertEquals(original.getTabby().getPaymentTypes(), restored.getTabby().getPaymentTypes()); + assertEquals(original.getPaypal().getBrandName(), restored.getPaypal().getBrandName()); + assertEquals(original.getBizum().getStatus(), restored.getBizum().getStatus()); + } + + @Test + void shouldDeserializeFullPaymentMethodsFromSwaggerExample() { + final String json = "{" + + "\"klarna\":{" + + "\"status\":\"action_required\"," + + "\"flags\":[\"missing_phone\"]," + + "\"initialization\":\"enabled\"," + + "\"action\":{" + + "\"type\":\"sdk\"," + + "\"client_token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzZXNzaW9uX2lkIiA6ICIw\"," + + "\"session_id\":\"0b1d9815-165e-42e2-8867-35bc03789e00\"" + + "}" + + "}," + + "\"stcpay\":{" + + "\"status\":\"pending\"," + + "\"flags\":[]," + + "\"initialization\":\"disabled\"," + + "\"action\":{\"type\":\"otp\"}" + + "}," + + "\"tabby\":{" + + "\"status\":\"ready\"," + + "\"flags\":[]," + + "\"initialization\":\"enabled\"," + + "\"payment_types\":[\"pay_later\"]" + + "}," + + "\"paypal\":{" + + "\"status\":\"available\"," + + "\"flags\":[]," + + "\"initialization\":\"enabled\"," + + "\"user_action\":\"pay_now\"," + + "\"brand_name\":\"Acme Corporation\"," + + "\"shipping_preference\":\"no_shipping\"," + + "\"action\":{" + + "\"type\":\"sdk\"," + + "\"order_id\":\"dd4b1a85-31de-404b-99f4-f143f8ba35ba\"" + + "}" + + "}," + + "\"bizum\":{" + + "\"status\":\"unavailable\"," + + "\"flags\":[\"bizum_setup_incomplete\"]" + + "}" + + "}"; + + final PaymentMethods methods = serializer.fromJson(json, PaymentMethods.class); + + assertNotNull(methods.getKlarna()); + assertEquals("sdk", methods.getKlarna().getAction().getType()); + assertEquals("0b1d9815-165e-42e2-8867-35bc03789e00", methods.getKlarna().getAction().getSessionId()); + + assertNotNull(methods.getStcpay()); + assertEquals("otp", methods.getStcpay().getAction().getType()); + + assertNotNull(methods.getTabby()); + assertEquals(Collections.singletonList("pay_later"), methods.getTabby().getPaymentTypes()); + + assertNotNull(methods.getPaypal()); + assertEquals(PayPalUserAction.PAY_NOW, methods.getPaypal().getUserAction()); + assertEquals("Acme Corporation", methods.getPaypal().getBrandName()); + assertEquals("dd4b1a85-31de-404b-99f4-f143f8ba35ba", methods.getPaypal().getAction().getOrderId()); + + assertNotNull(methods.getBizum()); + assertEquals(PaymentMethodStatus.UNAVAILABLE, methods.getBizum().getStatus()); + } + + @Test + void shouldSerializePayPalUserAction() { + final PayPal payNow = new PayPal(); + payNow.setUserAction(PayPalUserAction.PAY_NOW); + assertTrue(serializer.toJson(payNow).contains("\"pay_now\"")); + + final PayPal cont = new PayPal(); + cont.setUserAction(PayPalUserAction.CONTINUE); + assertTrue(serializer.toJson(cont).contains("\"continue\"")); + } + + @Test + void shouldSerializePayPalShippingPreference() { + final PayPal noShipping = new PayPal(); + noShipping.setShippingPreference(PayPalShippingPreference.NO_SHIPPING); + assertTrue(serializer.toJson(noShipping).contains("\"no_shipping\"")); + + final PayPal fromFile = new PayPal(); + fromFile.setShippingPreference(PayPalShippingPreference.GET_FROM_FILE); + assertTrue(serializer.toJson(fromFile).contains("\"get_from_file\"")); + + final PayPal provided = new PayPal(); + provided.setShippingPreference(PayPalShippingPreference.SET_PROVIDED_ADDRESS); + assertTrue(serializer.toJson(provided).contains("\"set_provided_address\"")); + } + + @Test + void shouldRoundTripPaymentMethodActionWithAllProperties() { + final PaymentMethodAction original = PaymentMethodAction.builder() + .type("sdk") + .clientToken("ct_all") + .sessionId("sid_all") + .orderId("ord_all") + .build(); + + final String json = serializer.toJson(original); + final PaymentMethodAction restored = serializer.fromJson(json, PaymentMethodAction.class); + + assertNotNull(restored); + assertEquals(original.getType(), restored.getType()); + assertEquals(original.getClientToken(), restored.getClientToken()); + assertEquals(original.getSessionId(), restored.getSessionId()); + assertEquals(original.getOrderId(), restored.getOrderId()); + assertTrue(json.contains("\"client_token\"")); + assertTrue(json.contains("\"session_id\"")); + assertTrue(json.contains("\"order_id\"")); + } + + @Test + void shouldDeserializeAllPaymentMethodStatusWireValues() { + assertEquals(PaymentMethodStatus.UNAVAILABLE, serializer.fromJson("{\"status\":\"unavailable\"}", Klarna.class).getStatus()); + assertEquals(PaymentMethodStatus.ACTION_REQUIRED, serializer.fromJson("{\"status\":\"action_required\"}", Klarna.class).getStatus()); + assertEquals(PaymentMethodStatus.PENDING, serializer.fromJson("{\"status\":\"pending\"}", Klarna.class).getStatus()); + assertEquals(PaymentMethodStatus.READY, serializer.fromJson("{\"status\":\"ready\"}", Klarna.class).getStatus()); + assertEquals(PaymentMethodStatus.AVAILABLE, serializer.fromJson("{\"status\":\"available\"}", Klarna.class).getStatus()); + } + + @Test + void shouldSerializeAllPaymentMethodStatusValues() { + for (final PaymentMethodStatus status : PaymentMethodStatus.values()) { + final Klarna k = new Klarna(); + k.setStatus(status); + final String json = serializer.toJson(k); + switch (status) { + case UNAVAILABLE: + assertTrue(json.contains("\"unavailable\"")); + break; + case ACTION_REQUIRED: + assertTrue(json.contains("\"action_required\"")); + break; + case PENDING: + assertTrue(json.contains("\"pending\"")); + break; + case READY: + assertTrue(json.contains("\"ready\"")); + break; + case AVAILABLE: + assertTrue(json.contains("\"available\"")); + break; + default: + throw new IllegalStateException("Unhandled status: " + status); + } + } + } + + @Test + void shouldRoundTripPaymentMethodInitialization() { + for (final PaymentMethodInitialization init : PaymentMethodInitialization.values()) { + final Klarna k = new Klarna(); + k.setInitialization(init); + final Klarna restored = serializer.fromJson(serializer.toJson(k), Klarna.class); + assertEquals(init, restored.getInitialization()); + } + } + + private static Klarna buildSampleKlarna() { + final Klarna k = new Klarna(); + k.setStatus(PaymentMethodStatus.READY); + k.setFlags(Collections.singletonList("kf")); + k.setInitialization(PaymentMethodInitialization.ENABLED); + k.setAction(PaymentMethodAction.builder().type("sdk").clientToken("c").sessionId("s").build()); + return k; + } + + private static Stcpay buildSampleStcpay() { + final Stcpay s = new Stcpay(); + s.setStatus(PaymentMethodStatus.PENDING); + s.setFlags(Collections.emptyList()); + s.setInitialization(PaymentMethodInitialization.DISABLED); + s.setOtp("999888"); + s.setAction(PaymentMethodAction.builder().type("otp").build()); + return s; + } + + private static Tabby buildSampleTabby() { + final Tabby t = new Tabby(); + t.setStatus(PaymentMethodStatus.AVAILABLE); + t.setFlags(Collections.singletonList("tf")); + t.setInitialization(PaymentMethodInitialization.ENABLED); + t.setPaymentTypes(Collections.singletonList("pay_later")); + return t; + } + + private static PayPal buildSamplePayPal() { + final PayPal p = new PayPal(); + p.setStatus(PaymentMethodStatus.ACTION_REQUIRED); + p.setFlags(Collections.singletonList("pf")); + p.setInitialization(PaymentMethodInitialization.DISABLED); + p.setUserAction(PayPalUserAction.PAY_NOW); + p.setBrandName("Brand"); + p.setShippingPreference(PayPalShippingPreference.SET_PROVIDED_ADDRESS); + p.setAction(PaymentMethodAction.builder().type("sdk").orderId("o1").build()); + return p; + } + + private static Bizum buildSampleBizum() { + final Bizum b = new Bizum(); + b.setStatus(PaymentMethodStatus.UNAVAILABLE); + b.setFlags(Arrays.asList("b1", "b2")); + return b; + } + + private static boolean hasFieldNamed(final Class clazz, final String name) { + for (final Field f : clazz.getDeclaredFields()) { + if (name.equals(f.getName())) { + return true; + } + } + return false; + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java index 72199533..9339e244 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/setups/PaymentSetupsTestIT.java @@ -2,8 +2,6 @@ import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; -import com.checkout.common.Address; -import com.checkout.common.CountryCode; import com.checkout.common.Currency; import com.checkout.common.Phone; import com.checkout.handlepaymentsandpayouts.setups.entities.customer.Customer; @@ -12,7 +10,6 @@ import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.PaymentMethods; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.common.PaymentMethodInitialization; import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.Klarna; -import com.checkout.handlepaymentsandpayouts.setups.entities.paymentMethods.klarna.KlarnaAccountHolder; import com.checkout.handlepaymentsandpayouts.setups.entities.settings.Settings; import com.checkout.payments.PaymentType; import com.checkout.handlepaymentsandpayouts.setups.requests.PaymentSetupsRequest; @@ -129,17 +126,6 @@ void confirmPaymentSetup_ShouldReturnValidResponse() { private PaymentSetupsRequest createValidPaymentSetupsRequest() { final Klarna klarna = new Klarna(); klarna.setInitialization(PaymentMethodInitialization.DISABLED); - - final KlarnaAccountHolder accountHolder = KlarnaAccountHolder.builder() - .billingAddress(Address.builder() - .addressLine1("123 High Street") - .city("London") - .zip("SW1A 1AA") - .country(CountryCode.GB) - .build()) - .build(); - - klarna.setAccountHolder(accountHolder); return PaymentSetupsRequest.builder() .processingChannelId(System.getenv("CHECKOUT_PROCESSING_CHANNEL_ID")) diff --git a/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsIT.java b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsIT.java new file mode 100644 index 00000000..cf9e7853 --- /dev/null +++ b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsIT.java @@ -0,0 +1,128 @@ +package com.checkout.issuing.newfields; + +import com.checkout.issuing.BaseIssuingTestIT; +import com.checkout.issuing.cardholders.CardholderResponse; +import com.checkout.issuing.cards.requests.create.CardLifetime; +import com.checkout.issuing.cards.requests.create.LifetimeUnit; +import com.checkout.issuing.cards.requests.create.PhysicalCardRequest; +import com.checkout.issuing.cards.requests.create.VirtualCardRequest; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; +import com.checkout.issuing.cards.responses.CardResponse; +import org.junit.jupiter.api.BeforeAll; + +import java.time.LocalDate; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Integration tests for new fields on card creation: + * {@code metadata} (udf1–udf5) and {@code revocation_date}. + * + *

Disabled by default — requires issuing OAuth credentials and + * the {@code CHECKOUT_DEFAULT_OAUTH_ISSUING_*} environment variables.

+ */ +@Disabled("Requires issuing OAuth credentials and an enabled issuing sandbox account") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class IssuingCardNewFieldsIT extends BaseIssuingTestIT { + + private CardholderResponse cardholder; + + @BeforeAll + void setUp() { + cardholder = createCardholder(); + } + + @Test + void shouldCreateVirtualCardWithMetadata() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(cardholder.getId()) + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.YEARS).value(1).build()) + .metadata(IssuingCardMetadata.builder() + .udf1("integration_test") + .udf2("new_fields_suite") + .build()) + .build(); + + final CardResponse response = blocking(() -> + issuingApi.issuingClient().createCard(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldCreateVirtualCardWithRevocationDate() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(cardholder.getId()) + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.YEARS).value(1).build()) + .revocationDate(LocalDate.of(2027, 12, 31)) + .build(); + + final CardResponse response = blocking(() -> + issuingApi.issuingClient().createCard(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldCreateVirtualCardWithBothNewFields() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(cardholder.getId()) + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.MONTHS).value(12).build()) + .metadata(IssuingCardMetadata.builder() + .udf1("campaign_q4") + .udf3("region_emea") + .udf5("priority_high") + .build()) + .revocationDate(LocalDate.of(2027, 6, 30)) + .build(); + + final CardResponse response = blocking(() -> + issuingApi.issuingClient().createCard(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldCreatePhysicalCardWithMetadata() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(cardholder.getId()) + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.YEARS).value(3).build()) + .metadata(IssuingCardMetadata.builder() + .udf1("physical_card_test") + .build()) + .build(); + + final CardResponse response = blocking(() -> + issuingApi.issuingClient().createCard(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldCreatePhysicalCardWithRevocationDate() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(cardholder.getId()) + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.YEARS).value(2).build()) + .revocationDate(LocalDate.of(2026, 9, 15)) + .build(); + + final CardResponse response = blocking(() -> + issuingApi.issuingClient().createCard(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } +} diff --git a/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsSchemaTest.java b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsSchemaTest.java new file mode 100644 index 00000000..d8403ee9 --- /dev/null +++ b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsSchemaTest.java @@ -0,0 +1,176 @@ +package com.checkout.issuing.newfields; + +import com.checkout.GsonSerializer; +import com.checkout.issuing.cards.requests.create.CardLifetime; +import com.checkout.issuing.cards.requests.create.LifetimeUnit; +import com.checkout.issuing.cards.requests.create.PhysicalCardRequest; +import com.checkout.issuing.cards.requests.create.VirtualCardRequest; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Schema / serialization tests for the new fields added to + * {@link com.checkout.issuing.cards.requests.create.CardRequest}: + *
    + *
  • {@code metadata} (udf1–udf5) — available on card creation, not just update
  • + *
  • {@code revocation_date} — automatic revocation date (yyyy-MM-dd)
  • + *
+ */ +class IssuingCardNewFieldsSchemaTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ─── VirtualCardRequest ───────────────────────────────────────────────── + + @Test + void shouldSerializeVirtualCardRequestWithMetadata() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .metadata(IssuingCardMetadata.builder() + .udf1("customer_tier_gold") + .udf2("campaign_2026_q1") + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"metadata\""), "Should contain metadata field"); + assertTrue(json.contains("\"udf1\""), "Should contain udf1"); + assertTrue(json.contains("\"customer_tier_gold\""), "Should contain udf1 value"); + assertTrue(json.contains("\"udf2\""), "Should contain udf2"); + assertTrue(json.contains("\"campaign_2026_q1\""), "Should contain udf2 value"); + } + + @Test + void shouldSerializeVirtualCardRequestWithRevocationDate() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .revocationDate(LocalDate.of(2027, 3, 12)) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"revocation_date\""), "Should contain revocation_date"); + assertTrue(json.contains("2027-03-12"), "Should contain revocation date value"); + } + + @Test + void shouldSerializeVirtualCardRequestWithBothNewFields() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .metadata(IssuingCardMetadata.builder() + .udf1("value1") + .udf3("value3") + .udf5("value5") + .build()) + .revocationDate(LocalDate.of(2028, 6, 30)) + .build(); + + assertDoesNotThrow(() -> { + final String json = serializer.toJson(request); + assertNotNull(json); + assertTrue(json.contains("\"metadata\"")); + assertTrue(json.contains("\"revocation_date\"")); + assertTrue(json.contains("\"udf1\"")); + assertTrue(json.contains("\"udf5\"")); + assertTrue(json.contains("2028-06-30")); + }); + } + + @Test + void shouldNotIncludeNullMetadataInSerialization() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .build(); + + final String json = serializer.toJson(request); + + assertFalse(json.contains("\"metadata\""), + "Should not include metadata when null"); + assertFalse(json.contains("\"revocation_date\""), + "Should not include revocation_date when null"); + } + + @Test + void shouldSerializeAllFiveUdfFields() { + final IssuingCardMetadata metadata = IssuingCardMetadata.builder() + .udf1("v1").udf2("v2").udf3("v3").udf4("v4").udf5("v5") + .build(); + + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .metadata(metadata) + .build(); + + final String json = serializer.toJson(request); + + for (int i = 1; i <= 5; i++) { + assertTrue(json.contains("\"udf" + i + "\""), "Should contain udf" + i); + assertTrue(json.contains("\"v" + i + "\""), "Should contain value for udf" + i); + } + } + + // ─── PhysicalCardRequest ───────────────────────────────────────────────── + + @Test + void shouldSerializePhysicalCardRequestWithMetadata() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .metadata(IssuingCardMetadata.builder() + .udf1("physical_card_ref") + .build()) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"metadata\"")); + assertTrue(json.contains("\"physical_card_ref\"")); + } + + @Test + void shouldSerializePhysicalCardRequestWithRevocationDate() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId("crh_d3ozhf43pcq2xbldn2g45qnb44") + .cardProductId("pro_7syjig3jq3mezlc3vjrdpfitl4") + .lifetime(CardLifetime.builder().unit(LifetimeUnit.YEARS).value(3).build()) + .revocationDate(LocalDate.of(2027, 12, 31)) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"revocation_date\"")); + assertTrue(json.contains("2027-12-31")); + } + + @Test + void shouldDeserializeSwaggerExampleVirtualCardRequest() { + final String json = "{" + + "\"type\":\"virtual\"," + + "\"cardholder_id\":\"crh_d3ozhf43pcq2xbldn2g45qnb44\"," + + "\"card_product_id\":\"pro_7syjig3jq3mezlc3vjrdpfitl4\"," + + "\"lifetime\":{\"unit\":\"Months\",\"value\":6}," + + "\"reference\":\"X-123456-N11\"," + + "\"metadata\":{\"udf1\":\"metadata1\",\"udf2\":\"metadata2\"}," + + "\"revocation_date\":\"2027-03-12\"" + + "}"; + + final VirtualCardRequest deserialized = serializer.fromJson(json, VirtualCardRequest.class); + + assertNotNull(deserialized); + assertNotNull(deserialized.getMetadata()); + assertEquals("metadata1", deserialized.getMetadata().getUdf1()); + assertEquals("metadata2", deserialized.getMetadata().getUdf2()); + } +} diff --git a/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsUnitTest.java b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsUnitTest.java new file mode 100644 index 00000000..1f5ced03 --- /dev/null +++ b/src/test/java/com/checkout/issuing/newfields/IssuingCardNewFieldsUnitTest.java @@ -0,0 +1,159 @@ +package com.checkout.issuing.newfields; + +import com.checkout.issuing.cards.requests.create.CardLifetime; +import com.checkout.issuing.cards.requests.create.LifetimeUnit; +import com.checkout.issuing.cards.requests.create.PhysicalCardRequest; +import com.checkout.issuing.cards.requests.create.VirtualCardRequest; +import com.checkout.issuing.cards.requests.update.IssuingCardMetadata; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests verifying that new fields ({@code metadata}, {@code revocation_date}) + * are correctly wired into both {@link VirtualCardRequest} and + * {@link PhysicalCardRequest} builders. + */ +class IssuingCardNewFieldsUnitTest { + + private static final String CARDHOLDER_ID = "crh_d3ozhf43pcq2xbldn2g45qnb44"; + private static final String CARD_PRODUCT_ID = "pro_7syjig3jq3mezlc3vjrdpfitl4"; + + // ─── VirtualCardRequest ───────────────────────────────────────────────── + + @Test + void shouldSetMetadataOnVirtualCardRequestViaBuilder() { + final IssuingCardMetadata metadata = IssuingCardMetadata.builder() + .udf1("tier_gold") + .udf2("region_eu") + .build(); + + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .metadata(metadata) + .build(); + + assertNotNull(request.getMetadata()); + assertEquals("tier_gold", request.getMetadata().getUdf1()); + assertEquals("region_eu", request.getMetadata().getUdf2()); + assertNull(request.getMetadata().getUdf3()); + } + + @Test + void shouldSetRevocationDateOnVirtualCardRequestViaBuilder() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .revocationDate(LocalDate.of(2027, 6, 15)) + .build(); + + assertEquals(LocalDate.of(2027, 6, 15), request.getRevocationDate()); + } + + @Test + void shouldSetBothNewFieldsOnVirtualCardRequest() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .metadata(IssuingCardMetadata.builder().udf1("v1").udf5("v5").build()) + .revocationDate(LocalDate.of(2028, 1, 1)) + .isSingleUse(true) + .build(); + + assertNotNull(request.getMetadata()); + assertEquals("v1", request.getMetadata().getUdf1()); + assertEquals("v5", request.getMetadata().getUdf5()); + assertEquals(LocalDate.of(2028, 1, 1), request.getRevocationDate()); + assertTrue(request.getIsSingleUse()); + } + + @Test + void shouldHaveNullMetadataAndRevocationDateByDefault() { + final VirtualCardRequest request = VirtualCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .build(); + + assertNull(request.getMetadata()); + assertNull(request.getRevocationDate()); + } + + // ─── PhysicalCardRequest ───────────────────────────────────────────────── + + @Test + void shouldSetMetadataOnPhysicalCardRequestViaBuilder() { + final IssuingCardMetadata metadata = IssuingCardMetadata.builder() + .udf1("physical_udf1") + .udf3("physical_udf3") + .build(); + + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .metadata(metadata) + .build(); + + assertNotNull(request.getMetadata()); + assertEquals("physical_udf1", request.getMetadata().getUdf1()); + assertNull(request.getMetadata().getUdf2()); + assertEquals("physical_udf3", request.getMetadata().getUdf3()); + } + + @Test + void shouldSetRevocationDateOnPhysicalCardRequestViaBuilder() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .lifetime(CardLifetime.builder().unit(LifetimeUnit.MONTHS).value(18).build()) + .revocationDate(LocalDate.of(2026, 9, 30)) + .build(); + + assertEquals(LocalDate.of(2026, 9, 30), request.getRevocationDate()); + assertNotNull(request.getLifetime()); + assertEquals(LifetimeUnit.MONTHS, request.getLifetime().getUnit()); + assertEquals(18, request.getLifetime().getValue()); + } + + @Test + void shouldSetBothNewFieldsOnPhysicalCardRequest() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .metadata(IssuingCardMetadata.builder().udf2("shipping_priority").build()) + .revocationDate(LocalDate.of(2027, 3, 12)) + .build(); + + assertNotNull(request.getMetadata()); + assertEquals("shipping_priority", request.getMetadata().getUdf2()); + assertEquals(LocalDate.of(2027, 3, 12), request.getRevocationDate()); + } + + @Test + void shouldHaveNullNewFieldsByDefaultOnPhysicalCardRequest() { + final PhysicalCardRequest request = PhysicalCardRequest.builder() + .cardholderId(CARDHOLDER_ID) + .cardProductId(CARD_PRODUCT_ID) + .build(); + + assertNull(request.getMetadata()); + assertNull(request.getRevocationDate()); + } + + // ─── IssuingCardMetadata ───────────────────────────────────────────────── + + @Test + void shouldBuildIssuingCardMetadataWithAllFields() { + final IssuingCardMetadata metadata = IssuingCardMetadata.builder() + .udf1("a").udf2("b").udf3("c").udf4("d").udf5("e") + .build(); + + assertEquals("a", metadata.getUdf1()); + assertEquals("b", metadata.getUdf2()); + assertEquals("c", metadata.getUdf3()); + assertEquals("d", metadata.getUdf4()); + assertEquals("e", metadata.getUdf5()); + } +} diff --git a/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsIT.java b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsIT.java new file mode 100644 index 00000000..b27862f5 --- /dev/null +++ b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsIT.java @@ -0,0 +1,129 @@ +package com.checkout.payments.newfields; + +import com.checkout.CardSourceHelper; +import com.checkout.common.Currency; +import com.checkout.payments.PaymentRouting; +import com.checkout.payments.PaymentRoutingScheme; +import com.checkout.payments.PaymentSubscription; +import com.checkout.payments.AbstractPaymentsTestIT; +import com.checkout.payments.PaymentsClient; +import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.request.source.RequestCardSource; +import com.checkout.payments.response.GetPaymentResponse; +import com.checkout.payments.response.PaymentResponse; +import com.checkout.payments.sender.PaymentIndividualSender; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.UUID; + +import static com.checkout.CardSourceHelper.getCorporateSender; +import static com.checkout.CardSourceHelper.getIndividualSender; +import static com.checkout.CardSourceHelper.getRequestCardSource; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Integration tests for new {@link PaymentRequest} fields introduced in the swagger update: + * {@code expire_on}, {@code routing}, {@code subscription}. + * + *

Also verifies that {@code GetPaymentResponse.authentication} is populated + * when returned by the API.

+ */ +class PaymentNewFieldsIT extends AbstractPaymentsTestIT { + + @Test + void shouldRequestPaymentWithSubscription() { + final RequestCardSource source = getRequestCardSource(); + final PaymentIndividualSender sender = getIndividualSender(); + + final PaymentRequest request = PaymentRequest.builder() + .source(source) + .sender(sender) + .amount(100L) + .currency(Currency.EUR) + .capture(false) + .reference(UUID.randomUUID().toString()) + .subscription(PaymentSubscription.builder() + .id("sub_" + UUID.randomUUID().toString().replace("-", "").substring(0, 20)) + .build()) + .build(); + + final PaymentResponse response = blocking(() -> paymentsClient.requestPayment(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldRequestPaymentWithRouting() { + final RequestCardSource source = getRequestCardSource(); + final PaymentIndividualSender sender = getIndividualSender(); + + final PaymentRequest request = PaymentRequest.builder() + .source(source) + .sender(sender) + .amount(100L) + .currency(Currency.EUR) + .capture(false) + .reference(UUID.randomUUID().toString()) + .routing(PaymentRouting.builder() + .attempts(Arrays.asList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build(), + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.MASTERCARD).build() + )) + .build()) + .build(); + + final PaymentResponse response = blocking(() -> paymentsClient.requestPayment(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Disabled("expire_on is Multibanco-specific; requires an APM-enabled processing channel") + @Test + void shouldRequestMultibancoPaymentWithExpireOn() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.EUR) + .amount(1000L) + .reference(UUID.randomUUID().toString()) + .expireOn(Instant.now().plus(2, ChronoUnit.DAYS)) + .build(); + + final PaymentResponse response = blocking(() -> paymentsClient.requestPayment(request)); + + assertNotNull(response); + assertNotNull(response.getId()); + } + + @Test + void shouldGetPaymentAndReadAuthenticationField() { + // First make a payment + final RequestCardSource source = getRequestCardSource(); + final PaymentIndividualSender sender = getIndividualSender(); + + final PaymentRequest request = PaymentRequest.builder() + .source(source) + .sender(sender) + .amount(100L) + .currency(Currency.EUR) + .capture(false) + .reference(UUID.randomUUID().toString()) + .build(); + + final PaymentResponse paymentResponse = blocking(() -> paymentsClient.requestPayment(request)); + assertNotNull(paymentResponse); + + // Retrieve it and check the authentication field (may be null for non-3DS flows) + final GetPaymentResponse getResponse = blocking(() -> + paymentsClient.getPayment(paymentResponse.getId())); + + assertNotNull(getResponse); + assertNotNull(getResponse.getId()); + // authentication field present only for 3DS/Google-SPA flows — no hard assert on value + } +} diff --git a/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsSchemaTest.java b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsSchemaTest.java new file mode 100644 index 00000000..461caa18 --- /dev/null +++ b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsSchemaTest.java @@ -0,0 +1,262 @@ +package com.checkout.payments.newfields; + +import com.checkout.GsonSerializer; +import com.checkout.common.Currency; +import com.checkout.payments.PaymentRouting; +import com.checkout.payments.PaymentRoutingScheme; +import com.checkout.payments.PaymentSubscription; +import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.response.GetPaymentResponse; +import com.checkout.payments.response.AuthenticationExperience; +import com.checkout.payments.response.PaymentResponseAuthentication; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Schema / serialization tests for new fields added to {@link PaymentRequest} + * and {@link GetPaymentResponse} in the latest swagger update: + *
    + *
  • {@code expire_on} ({@link Instant})
  • + *
  • {@code routing} ({@link PaymentRouting})
  • + *
  • {@code subscription} ({@link PaymentSubscription})
  • + *
  • {@code authentication} ({@link PaymentResponseAuthentication})
  • + *
+ */ +class PaymentNewFieldsSchemaTest { + + private final GsonSerializer serializer = new GsonSerializer(); + + // ─── PaymentRequest ───────────────────────────────────────────────────── + + @Test + void shouldSerializeExpireOn() { + final Instant expiry = Instant.parse("2025-01-31T10:20:30.456Z"); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.EUR) + .expireOn(expiry) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"expire_on\""), + "Should serialize expireOn as expire_on"); + assertTrue(json.contains("2025-01-31"), + "Should contain the expiry date value"); + } + + @Test + void shouldSerializePaymentRouting() { + final PaymentRouting routing = PaymentRouting.builder() + .attempts(Arrays.asList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build(), + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.MASTERCARD).build() + )) + .build(); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .routing(routing) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"routing\""), "Should contain routing field"); + assertTrue(json.contains("\"attempts\""), "Should contain attempts array"); + assertTrue(json.contains("\"visa\""), "Should contain visa scheme"); + assertTrue(json.contains("\"mastercard\""), "Should contain mastercard scheme"); + } + + @Test + void shouldSerializePaymentRoutingWithSingleAttempt() { + final PaymentRouting routing = PaymentRouting.builder() + .attempts(Collections.singletonList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.AMEX).build() + )) + .build(); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.GBP) + .routing(routing) + .build(); + + final String json = serializer.toJson(request); + + assertTrue(json.contains("\"amex\"")); + } + + @Test + void shouldSerializePaymentSubscription() { + final PaymentSubscription subscription = PaymentSubscription.builder() + .id("sub_ref_12345678901234567890") + .build(); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .subscription(subscription) + .build(); + + final String json = serializer.toJson(request); + + assertNotNull(json); + assertTrue(json.contains("\"subscription\""), "Should contain subscription field"); + assertTrue(json.contains("\"sub_ref_12345678901234567890\""), "Should contain subscription id"); + } + + @Test + void shouldSerializeAllThreeNewFieldsTogether() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .amount(1000L) + .expireOn(Instant.parse("2025-06-30T23:59:59Z")) + .routing(PaymentRouting.builder() + .attempts(Collections.singletonList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build())) + .build()) + .subscription(PaymentSubscription.builder().id("sub_abc123").build()) + .build(); + + assertDoesNotThrow(() -> { + final String json = serializer.toJson(request); + assertNotNull(json); + assertTrue(json.contains("\"expire_on\"")); + assertTrue(json.contains("\"routing\"")); + assertTrue(json.contains("\"subscription\"")); + }); + } + + @Test + void shouldNotSerializeNullNewFields() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .amount(100L) + .build(); + + final String json = serializer.toJson(request); + + // Gson skips null fields by default + assertFalse(json.contains("\"expire_on\""), + "Should not include expire_on when null"); + assertFalse(json.contains("\"routing\""), + "Should not include routing when null"); + assertFalse(json.contains("\"subscription\""), + "Should not include subscription when null"); + } + + // ─── GetPaymentResponse ────────────────────────────────────────────────── + + @Test + void shouldDeserializePaymentResponseAuthenticationWith3ds() { + final String json = "{" + + "\"id\":\"pay_abc123\"," + + "\"status\":\"Authorized\"," + + "\"authentication\":{\"experience\":\"3ds\"}" + + "}"; + + final GetPaymentResponse response = serializer.fromJson(json, GetPaymentResponse.class); + + assertNotNull(response); + assertNotNull(response.getAuthentication(), "authentication field should not be null"); + assertEquals(AuthenticationExperience.THREE_DS, response.getAuthentication().getExperience()); + } + + @Test + void shouldDeserializePaymentResponseAuthenticationWithGoogleSpa() { + final String json = "{" + + "\"id\":\"pay_xyz789\"," + + "\"status\":\"Authorized\"," + + "\"authentication\":{\"experience\":\"google_spa\"}" + + "}"; + + final GetPaymentResponse response = serializer.fromJson(json, GetPaymentResponse.class); + + assertNotNull(response); + assertNotNull(response.getAuthentication()); + assertEquals(AuthenticationExperience.GOOGLE_SPA, response.getAuthentication().getExperience()); + } + + @Test + void shouldDeserializeGetPaymentResponseWithoutAuthenticationField() { + final String json = "{" + + "\"id\":\"pay_no_auth\"," + + "\"status\":\"Authorized\"," + + "\"amount\":1000," + + "\"currency\":\"USD\"" + + "}"; + + final GetPaymentResponse response = serializer.fromJson(json, GetPaymentResponse.class); + + assertNotNull(response); + assertNull(response.getAuthentication(), + "authentication should be null when absent from response"); + } + + @Test + void shouldDeserializeGetPaymentResponseWithAllNewFields() { + final String json = "{" + + "\"id\":\"pay_full\"," + + "\"status\":\"Authorized\"," + + "\"amount\":5000," + + "\"currency\":\"EUR\"," + + "\"authentication\":{\"experience\":\"3ds\"}," + + "\"scheme_id\":\"scheme_abc\"," + + "\"cko_network_token_available\":true" + + "}"; + + final GetPaymentResponse response = serializer.fromJson(json, GetPaymentResponse.class); + + assertNotNull(response); + assertEquals("pay_full", response.getId()); + assertNotNull(response.getAuthentication()); + assertEquals(AuthenticationExperience.THREE_DS, response.getAuthentication().getExperience()); + } + + @Test + void shouldRoundTripPaymentRouting() { + final PaymentRouting original = PaymentRouting.builder() + .attempts(Arrays.asList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build(), + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.MASTERCARD).build() + )) + .build(); + + final String json = serializer.toJson(original); + final PaymentRouting deserialized = serializer.fromJson(json, PaymentRouting.class); + + assertNotNull(deserialized); + assertEquals(2, deserialized.getAttempts().size()); + assertEquals(PaymentRoutingScheme.VISA, deserialized.getAttempts().get(0).getScheme()); + assertEquals(PaymentRoutingScheme.MASTERCARD, deserialized.getAttempts().get(1).getScheme()); + } + + @Test + void shouldRoundTripPaymentSubscription() { + final PaymentSubscription original = PaymentSubscription.builder() + .id("sub_round_trip_test") + .build(); + + final String json = serializer.toJson(original); + final PaymentSubscription deserialized = serializer.fromJson(json, PaymentSubscription.class); + + assertNotNull(deserialized); + assertEquals("sub_round_trip_test", deserialized.getId()); + } + + @Test + void shouldRoundTripPaymentResponseAuthentication() { + final String json = "{\"experience\":\"google_spa\"}"; + final PaymentResponseAuthentication auth = serializer.fromJson(json, PaymentResponseAuthentication.class); + + final String reJson = serializer.toJson(auth); + final PaymentResponseAuthentication reAuth = serializer.fromJson(reJson, PaymentResponseAuthentication.class); + + assertEquals(AuthenticationExperience.GOOGLE_SPA, reAuth.getExperience()); + } +} diff --git a/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsUnitTest.java b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsUnitTest.java new file mode 100644 index 00000000..89584dee --- /dev/null +++ b/src/test/java/com/checkout/payments/newfields/PaymentNewFieldsUnitTest.java @@ -0,0 +1,166 @@ +package com.checkout.payments.newfields; + +import com.checkout.common.Currency; +import com.checkout.payments.PaymentRouting; +import com.checkout.payments.PaymentRoutingScheme; +import com.checkout.payments.PaymentSubscription; +import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.response.GetPaymentResponse; +import com.checkout.payments.response.AuthenticationExperience; +import com.checkout.payments.response.PaymentResponseAuthentication; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests verifying that new fields on {@link PaymentRequest} and + * {@link GetPaymentResponse} are correctly modelled (builder, getters, equals). + */ +class PaymentNewFieldsUnitTest { + + // ─── PaymentRouting ────────────────────────────────────────────────────── + + @Test + void shouldBuildPaymentRoutingWithMultipleAttempts() { + final PaymentRouting routing = PaymentRouting.builder() + .attempts(Arrays.asList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build(), + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.MASTERCARD).build(), + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.AMEX).build() + )) + .build(); + + assertNotNull(routing.getAttempts()); + assertEquals(3, routing.getAttempts().size()); + assertEquals(PaymentRoutingScheme.VISA, routing.getAttempts().get(0).getScheme()); + assertEquals(PaymentRoutingScheme.MASTERCARD, routing.getAttempts().get(1).getScheme()); + assertEquals(PaymentRoutingScheme.AMEX, routing.getAttempts().get(2).getScheme()); + } + + @Test + void shouldBuildPaymentRoutingAttempt() { + final PaymentRouting.RoutingAttempt attempt = + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.JCB).build(); + + assertEquals(PaymentRoutingScheme.JCB, attempt.getScheme()); + } + + @Test + void shouldAllowEmptyRoutingAttemptsList() { + final PaymentRouting routing = PaymentRouting.builder() + .attempts(Collections.emptyList()) + .build(); + + assertNotNull(routing); + assertTrue(routing.getAttempts().isEmpty()); + } + + // ─── PaymentSubscription ───────────────────────────────────────────────── + + @Test + void shouldBuildPaymentSubscription() { + final PaymentSubscription subscription = PaymentSubscription.builder() + .id("sub_monthly_plan_abc") + .build(); + + assertNotNull(subscription); + assertEquals("sub_monthly_plan_abc", subscription.getId()); + } + + @Test + void shouldAllowNullSubscriptionId() { + final PaymentSubscription subscription = PaymentSubscription.builder().build(); + + assertNull(subscription.getId()); + } + + // ─── PaymentRequest new fields ─────────────────────────────────────────── + + @Test + void shouldSetExpireOnInPaymentRequest() { + final Instant expiry = Instant.parse("2025-12-31T23:59:59Z"); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .expireOn(expiry) + .build(); + + assertEquals(expiry, request.getExpireOn()); + } + + @Test + void shouldSetRoutingInPaymentRequest() { + final PaymentRouting routing = PaymentRouting.builder() + .attempts(Collections.singletonList( + PaymentRouting.RoutingAttempt.builder().scheme(PaymentRoutingScheme.VISA).build())) + .build(); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.USD) + .routing(routing) + .build(); + + assertNotNull(request.getRouting()); + assertEquals(1, request.getRouting().getAttempts().size()); + assertEquals(PaymentRoutingScheme.VISA, request.getRouting().getAttempts().get(0).getScheme()); + } + + @Test + void shouldSetSubscriptionInPaymentRequest() { + final PaymentSubscription subscription = PaymentSubscription.builder() + .id("sub_ref_001") + .build(); + + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.EUR) + .subscription(subscription) + .build(); + + assertNotNull(request.getSubscription()); + assertEquals("sub_ref_001", request.getSubscription().getId()); + } + + @Test + void shouldHaveNullNewFieldsByDefault() { + final PaymentRequest request = PaymentRequest.builder() + .currency(Currency.GBP) + .amount(500L) + .build(); + + assertNull(request.getExpireOn()); + assertNull(request.getRouting()); + assertNull(request.getSubscription()); + } + + // ─── GetPaymentResponse.authentication ─────────────────────────────────── + + @Test + void shouldSetAuthenticationOnGetPaymentResponse() { + final PaymentResponseAuthentication auth = new PaymentResponseAuthentication(); + auth.setExperience(AuthenticationExperience.GOOGLE_SPA); + + final GetPaymentResponse response = new GetPaymentResponse(); + response.setAuthentication(auth); + + assertNotNull(response.getAuthentication()); + assertEquals(AuthenticationExperience.GOOGLE_SPA, response.getAuthentication().getExperience()); + } + + @Test + void shouldBuildPaymentResponseAuthentication() { + final PaymentResponseAuthentication auth = new PaymentResponseAuthentication(); + auth.setExperience(AuthenticationExperience.THREE_DS); + + assertEquals(AuthenticationExperience.THREE_DS, auth.getExperience()); + } + + @Test + void shouldHaveNullAuthenticationOnGetPaymentResponseByDefault() { + final GetPaymentResponse response = new GetPaymentResponse(); + assertNull(response.getAuthentication()); + } +}