From 07d5e9a67dc852554ecc7879f62d80161e8e13da Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 5 Mar 2026 13:18:32 +0100 Subject: [PATCH 01/19] New Forward/Secrets module + tests (unit/integration) --- src/main/java/com/checkout/OAuthScope.java | 3 +- .../com/checkout/forward/ForwardClient.java | 44 ++++++ .../checkout/forward/ForwardClientImpl.java | 62 ++++++-- .../forward/requests/CreateSecretRequest.java | 16 ++ .../forward/requests/UpdateSecretRequest.java | 14 ++ .../forward/responses/SecretResponse.java | 23 +++ .../responses/SecretsListResponse.java | 15 ++ .../java/com/checkout/SandboxTestFixture.java | 4 +- .../forward/ForwardClientImplTest.java | 143 ++++++++++++++++++ .../com/checkout/forward/ForwardTestIT.java | 123 ++++++++++++++- 10 files changed, 435 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/checkout/forward/requests/CreateSecretRequest.java create mode 100644 src/main/java/com/checkout/forward/requests/UpdateSecretRequest.java create mode 100644 src/main/java/com/checkout/forward/responses/SecretResponse.java create mode 100644 src/main/java/com/checkout/forward/responses/SecretsListResponse.java diff --git a/src/main/java/com/checkout/OAuthScope.java b/src/main/java/com/checkout/OAuthScope.java index 47053b72..a9238277 100644 --- a/src/main/java/com/checkout/OAuthScope.java +++ b/src/main/java/com/checkout/OAuthScope.java @@ -55,7 +55,8 @@ public enum OAuthScope { VAULT_CARD_METADATA("vault:card-metadata"), VAULT_INSTRUMENTS("vault:instruments"), VAULT_TOKENIZATION("vault:tokenization"), - FORWARD("forward"); + FORWARD("forward"), + FORWARD_SECRETS("forward:secrets"); private final String scope; diff --git a/src/main/java/com/checkout/forward/ForwardClient.java b/src/main/java/com/checkout/forward/ForwardClient.java index c9b19642..f361a8b0 100644 --- a/src/main/java/com/checkout/forward/ForwardClient.java +++ b/src/main/java/com/checkout/forward/ForwardClient.java @@ -1,8 +1,13 @@ package com.checkout.forward; +import com.checkout.EmptyResponse; +import com.checkout.forward.requests.CreateSecretRequest; import com.checkout.forward.requests.ForwardRequest; +import com.checkout.forward.requests.UpdateSecretRequest; import com.checkout.forward.responses.ForwardAnApiResponse; import com.checkout.forward.responses.GetForwardResponse; +import com.checkout.forward.responses.SecretResponse; +import com.checkout.forward.responses.SecretsListResponse; import java.util.concurrent.CompletableFuture; @@ -23,9 +28,48 @@ public interface ForwardClient { */ CompletableFuture getForwardRequest(String forwardId); + /** + * Create secret + * Create a new secret with a plaintext value. + * Validation Rules: + * - name: 1-64 characters, alphanumeric + underscore + * - value: max 8KB + * - entity_id (optional): when provided, secret is scoped to this entity + */ + CompletableFuture createSecret(CreateSecretRequest createSecretRequest); + + /** + * List secrets + * Returns metadata for secrets scoped for client_id. + */ + CompletableFuture listSecrets(); + + /** + * Update secret + * Update an existing secret. After updating, the version is automatically incremented. + * Validation Rules: + * - Only value and entity_id can be updated + * - value: max 8KB + */ + CompletableFuture updateSecret(String name, UpdateSecretRequest updateSecretRequest); + + /** + * Delete secret + * Permanently delete a secret by name. + */ + CompletableFuture deleteSecret(String name); + // Synchronous methods ForwardAnApiResponse forwardAnApiRequestSync(ForwardRequest forwardRequest); GetForwardResponse getForwardRequestSync(String forwardId); + SecretResponse createSecretSync(CreateSecretRequest createSecretRequest); + + SecretsListResponse listSecretsSync(); + + SecretResponse updateSecretSync(String name, UpdateSecretRequest updateSecretRequest); + + EmptyResponse deleteSecretSync(String name); + } diff --git a/src/main/java/com/checkout/forward/ForwardClientImpl.java b/src/main/java/com/checkout/forward/ForwardClientImpl.java index b122bf8c..ed1cf048 100644 --- a/src/main/java/com/checkout/forward/ForwardClientImpl.java +++ b/src/main/java/com/checkout/forward/ForwardClientImpl.java @@ -3,17 +3,23 @@ import com.checkout.AbstractClient; import com.checkout.ApiClient; import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; import com.checkout.SdkAuthorizationType; import com.checkout.common.CheckoutUtils; +import com.checkout.forward.requests.CreateSecretRequest; import com.checkout.forward.requests.ForwardRequest; +import com.checkout.forward.requests.UpdateSecretRequest; import com.checkout.forward.responses.ForwardAnApiResponse; import com.checkout.forward.responses.GetForwardResponse; +import com.checkout.forward.responses.SecretResponse; +import com.checkout.forward.responses.SecretsListResponse; import java.util.concurrent.CompletableFuture; public class ForwardClientImpl extends AbstractClient implements ForwardClient { private static final String FORWARD_PATH = "forward"; + private static final String SECRETS_PATH = "secrets"; public ForwardClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); @@ -21,35 +27,73 @@ public ForwardClientImpl(final ApiClient apiClient, final CheckoutConfiguration @Override public CompletableFuture forwardAnApiRequest(final ForwardRequest forwardRequest) { - validateForwardRequest(forwardRequest); + CheckoutUtils.validateParams("forwardRequest", forwardRequest); return apiClient.postAsync(FORWARD_PATH, sdkAuthorization(), ForwardAnApiResponse.class, forwardRequest, null); } @Override public CompletableFuture getForwardRequest(final String forwardId) { - validateForwardId(forwardId); + CheckoutUtils.validateParams("forwardId", forwardId); return apiClient.getAsync(buildPath(FORWARD_PATH, forwardId), sdkAuthorization(), GetForwardResponse.class); } + @Override + public CompletableFuture createSecret(final CreateSecretRequest createSecretRequest) { + CheckoutUtils.validateParams("createSecretRequest", createSecretRequest); + return apiClient.postAsync(buildPath(FORWARD_PATH, SECRETS_PATH), sdkAuthorization(), SecretResponse.class, createSecretRequest, null); + } + + @Override + public CompletableFuture listSecrets() { + return apiClient.getAsync(buildPath(FORWARD_PATH, SECRETS_PATH), sdkAuthorization(), SecretsListResponse.class); + } + + @Override + public CompletableFuture updateSecret(final String name, final UpdateSecretRequest updateSecretRequest) { + CheckoutUtils.validateParams("name", name,"updateSecretRequest", updateSecretRequest); + return apiClient.patchAsync(buildPath(FORWARD_PATH, SECRETS_PATH, name), sdkAuthorization(), SecretResponse.class, updateSecretRequest, null); + } + + @Override + public CompletableFuture deleteSecret(final String name) { + CheckoutUtils.validateParams("name", name); + return apiClient.deleteAsync(buildPath(FORWARD_PATH, SECRETS_PATH, name), sdkAuthorization()); + } + // Synchronous methods @Override public ForwardAnApiResponse forwardAnApiRequestSync(final ForwardRequest forwardRequest) { - validateForwardRequest(forwardRequest); + CheckoutUtils.validateParams("forwardRequest", forwardRequest); return apiClient.post(FORWARD_PATH, sdkAuthorization(), ForwardAnApiResponse.class, forwardRequest, null); } @Override public GetForwardResponse getForwardRequestSync(final String forwardId) { - validateForwardId(forwardId); + CheckoutUtils.validateParams("forwardId", forwardId); return apiClient.get(buildPath(FORWARD_PATH, forwardId), sdkAuthorization(), GetForwardResponse.class); } - // Common methods - private void validateForwardId(final String forwardId) { - CheckoutUtils.validateParams("forwardId", forwardId); + @Override + public SecretResponse createSecretSync(final CreateSecretRequest createSecretRequest) { + CheckoutUtils.validateParams("createSecretRequest", createSecretRequest); + return apiClient.post(buildPath(FORWARD_PATH, SECRETS_PATH), sdkAuthorization(), SecretResponse.class, createSecretRequest, null); } - private void validateForwardRequest(final ForwardRequest forwardRequest) { - CheckoutUtils.validateParams("forwardRequest", forwardRequest); + @Override + public SecretsListResponse listSecretsSync() { + return apiClient.get(buildPath(FORWARD_PATH, SECRETS_PATH), sdkAuthorization(), SecretsListResponse.class); } + + @Override + public SecretResponse updateSecretSync(final String name, final UpdateSecretRequest updateSecretRequest) { + CheckoutUtils.validateParams("name", name, "updateSecretRequest", updateSecretRequest); + return apiClient.patch(buildPath(FORWARD_PATH, SECRETS_PATH, name), sdkAuthorization(), SecretResponse.class, updateSecretRequest, null); + } + + @Override + public EmptyResponse deleteSecretSync(final String name) { + CheckoutUtils.validateParams("name", name); + return apiClient.delete(buildPath(FORWARD_PATH, SECRETS_PATH, name), sdkAuthorization()); + } + } diff --git a/src/main/java/com/checkout/forward/requests/CreateSecretRequest.java b/src/main/java/com/checkout/forward/requests/CreateSecretRequest.java new file mode 100644 index 00000000..878a7fc4 --- /dev/null +++ b/src/main/java/com/checkout/forward/requests/CreateSecretRequest.java @@ -0,0 +1,16 @@ +package com.checkout.forward.requests; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CreateSecretRequest { + + private String name; + + private String value; + + private String entityId; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/forward/requests/UpdateSecretRequest.java b/src/main/java/com/checkout/forward/requests/UpdateSecretRequest.java new file mode 100644 index 00000000..74e54519 --- /dev/null +++ b/src/main/java/com/checkout/forward/requests/UpdateSecretRequest.java @@ -0,0 +1,14 @@ +package com.checkout.forward.requests; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UpdateSecretRequest { + + private String value; + + private String entityId; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/forward/responses/SecretResponse.java b/src/main/java/com/checkout/forward/responses/SecretResponse.java new file mode 100644 index 00000000..a858dd1c --- /dev/null +++ b/src/main/java/com/checkout/forward/responses/SecretResponse.java @@ -0,0 +1,23 @@ +package com.checkout.forward.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SecretResponse extends Resource { + + private String name; + + private Instant createdAt; + + private Instant updatedAt; + + private Integer version; + + private String entityId; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/forward/responses/SecretsListResponse.java b/src/main/java/com/checkout/forward/responses/SecretsListResponse.java new file mode 100644 index 00000000..6bae8d45 --- /dev/null +++ b/src/main/java/com/checkout/forward/responses/SecretsListResponse.java @@ -0,0 +1,15 @@ +package com.checkout.forward.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class SecretsListResponse extends Resource { + + private List data; + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/SandboxTestFixture.java b/src/test/java/com/checkout/SandboxTestFixture.java index 88c16d37..b7eee6f0 100644 --- a/src/test/java/com/checkout/SandboxTestFixture.java +++ b/src/test/java/com/checkout/SandboxTestFixture.java @@ -89,10 +89,12 @@ public SandboxTestFixture(final PlatformType platformType) { OAuthScope.ACCOUNTS, OAuthScope.SESSIONS_APP, OAuthScope.SESSIONS_BROWSER, OAuthScope.VAULT, OAuthScope.PAYOUTS_BANK_DETAILS, OAuthScope.DISPUTES, OAuthScope.TRANSFERS_CREATE, OAuthScope.TRANSFERS_VIEW, OAuthScope.BALANCES_VIEW, - OAuthScope.VAULT_CARD_METADATA, OAuthScope.FINANCIAL_ACTIONS, OAuthScope.FORWARD) + OAuthScope.VAULT_CARD_METADATA, OAuthScope.FINANCIAL_ACTIONS, OAuthScope.FORWARD, + OAuthScope.FORWARD_SECRETS) .environment(Environment.SANDBOX) .executor(CUSTOM_EXECUTOR) .build(); + case CUSTOM: break; } } diff --git a/src/test/java/com/checkout/forward/ForwardClientImplTest.java b/src/test/java/com/checkout/forward/ForwardClientImplTest.java index c7f19ea2..ac305098 100644 --- a/src/test/java/com/checkout/forward/ForwardClientImplTest.java +++ b/src/test/java/com/checkout/forward/ForwardClientImplTest.java @@ -2,12 +2,17 @@ import com.checkout.ApiClient; import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; import com.checkout.SdkAuthorization; import com.checkout.SdkAuthorizationType; import com.checkout.SdkCredentials; +import com.checkout.forward.requests.CreateSecretRequest; import com.checkout.forward.requests.ForwardRequest; +import com.checkout.forward.requests.UpdateSecretRequest; import com.checkout.forward.responses.ForwardAnApiResponse; import com.checkout.forward.responses.GetForwardResponse; +import com.checkout.forward.responses.SecretResponse; +import com.checkout.forward.responses.SecretsListResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -76,6 +81,60 @@ void shouldGetForwardRequest() throws ExecutionException, InterruptedException { validateForwardResponse(response, future.get()); } + @Test + void shouldCreateSecret() throws ExecutionException, InterruptedException { + final CreateSecretRequest request = createSecretRequest(); + final SecretResponse response = mock(SecretResponse.class); + + when(apiClient.postAsync(eq("forward/secrets"), eq(authorization), eq(SecretResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.createSecret(request); + + validateSecretResponse(response, future.get()); + } + + @Test + void shouldListSecrets() throws ExecutionException, InterruptedException { + final SecretsListResponse response = mock(SecretsListResponse.class); + + when(apiClient.getAsync(eq("forward/secrets"), eq(authorization), eq(SecretsListResponse.class))) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.listSecrets(); + + validateSecretsListResponse(response, future.get()); + } + + @Test + void shouldUpdateSecret() throws ExecutionException, InterruptedException { + final String name = "secret_name"; + final UpdateSecretRequest request = createUpdateSecretRequest(); + final SecretResponse response = mock(SecretResponse.class); + + when(apiClient.patchAsync(eq("forward/secrets/" + name), eq(authorization), eq(SecretResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.updateSecret(name, request); + + validateSecretResponse(response, future.get()); + } + + @Test + void shouldDeleteSecret() throws ExecutionException, InterruptedException { + final String name = "secret_name"; + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.deleteAsync(eq("forward/secrets/" + name), eq(authorization))) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.deleteSecret(name); + + validateEmptyResponse(response, future.get()); + } + // Synchronous methods @Test void shouldForwardAnApiRequestSync() throws ExecutionException, InterruptedException { @@ -105,6 +164,60 @@ void shouldGetForwardRequestSync() throws ExecutionException, InterruptedExcepti validateForwardResponse(response, result); } + @Test + void shouldCreateSecretSync() throws ExecutionException, InterruptedException { + final CreateSecretRequest request = createSecretRequest(); + final SecretResponse response = mock(SecretResponse.class); + + when(apiClient.post(eq("forward/secrets"), eq(authorization), eq(SecretResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final SecretResponse result = client.createSecretSync(request); + + validateSecretResponse(response, result); + } + + @Test + void shouldListSecretsSync() throws ExecutionException, InterruptedException { + final SecretsListResponse response = mock(SecretsListResponse.class); + + when(apiClient.get(eq("forward/secrets"), eq(authorization), eq(SecretsListResponse.class))) + .thenReturn(response); + + final SecretsListResponse result = client.listSecretsSync(); + + validateSecretsListResponse(response, result); + } + + @Test + void shouldUpdateSecretSync() throws ExecutionException, InterruptedException { + final String name = "secret_name"; + final UpdateSecretRequest request = createUpdateSecretRequest(); + final SecretResponse response = mock(SecretResponse.class); + + when(apiClient.patch(eq("forward/secrets/" + name), eq(authorization), eq(SecretResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final SecretResponse result = client.updateSecretSync(name, request); + + validateSecretResponse(response, result); + } + + @Test + void shouldDeleteSecretSync() throws ExecutionException, InterruptedException { + final String name = "secret_name"; + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.delete(eq("forward/secrets/" + name), eq(authorization))) + .thenReturn(response); + + final EmptyResponse result = client.deleteSecretSync(name); + + validateEmptyResponse(response, result); + } + // Common methods private void validateForwardAnApiResponse(final ForwardAnApiResponse response, final ForwardAnApiResponse result) { assertNotNull(result); @@ -116,4 +229,34 @@ private void validateForwardResponse(final GetForwardResponse response, final Ge assertEquals(response, result); } + private CreateSecretRequest createSecretRequest() { + return CreateSecretRequest.builder() + .name("test_secret") + .value("test_value") + .entityId("ent_123") + .build(); + } + + private UpdateSecretRequest createUpdateSecretRequest() { + return UpdateSecretRequest.builder() + .value("updated_value") + .entityId("ent_456") + .build(); + } + + private void validateSecretResponse(final SecretResponse response, final SecretResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateSecretsListResponse(final SecretsListResponse response, final SecretsListResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateEmptyResponse(final EmptyResponse response, final EmptyResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + } diff --git a/src/test/java/com/checkout/forward/ForwardTestIT.java b/src/test/java/com/checkout/forward/ForwardTestIT.java index b592302b..e2df0070 100644 --- a/src/test/java/com/checkout/forward/ForwardTestIT.java +++ b/src/test/java/com/checkout/forward/ForwardTestIT.java @@ -1,17 +1,22 @@ package com.checkout.forward; +import com.checkout.EmptyResponse; import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; +import com.checkout.forward.requests.CreateSecretRequest; import com.checkout.forward.requests.DestinationRequest; import com.checkout.forward.requests.ForwardRequest; import com.checkout.forward.requests.Headers; import com.checkout.forward.requests.MethodType; import com.checkout.forward.requests.NetworkToken; +import com.checkout.forward.requests.UpdateSecretRequest; import com.checkout.forward.requests.signatures.DlocalParameters; import com.checkout.forward.requests.signatures.DlocalSignature; import com.checkout.forward.requests.sources.IdSource; import com.checkout.forward.responses.ForwardAnApiResponse; import com.checkout.forward.responses.GetForwardResponse; +import com.checkout.forward.responses.SecretResponse; +import com.checkout.forward.responses.SecretsListResponse; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -20,6 +25,7 @@ import static java.util.Objects.requireNonNull; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ForwardTestIT extends SandboxTestFixture { @@ -53,6 +59,47 @@ void shouldGetForwardRequest() { } + @Disabled("This test requires a valid id or Token source") + @Test + void shouldCreateSecret() { + final CreateSecretRequest request = createSecretRequest(); + + final SecretResponse response = blocking(() -> checkoutApi.forwardClient().createSecret(request)); + + validateSecretResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldListSecrets() { + final SecretsListResponse response = blocking(() -> checkoutApi.forwardClient().listSecrets()); + + validateSecretsListResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldUpdateSecret() { + final CreateSecretRequest createRequest = createSecretRequest(); + final SecretResponse createResponse = blocking(() -> checkoutApi.forwardClient().createSecret(createRequest)); + + final UpdateSecretRequest updateRequest = createUpdateSecretRequest(); + final SecretResponse response = blocking(() -> checkoutApi.forwardClient().updateSecret(createResponse.getName(), updateRequest)); + + validateSecretResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldDeleteSecret() { + final CreateSecretRequest createRequest = createSecretRequest(); + final SecretResponse createResponse = blocking(() -> checkoutApi.forwardClient().createSecret(createRequest)); + + final EmptyResponse response = blocking(() -> checkoutApi.forwardClient().deleteSecret(createResponse.getName())); + + validateEmptyResponse(response); + } + // Sync methods @Disabled("This test requires a valid id or Token source") @Test @@ -79,6 +126,47 @@ void shouldGetForwardRequestSync() { } + @Disabled("This test requires a valid id or Token source") + @Test + void shouldCreateSecretSync() { + final CreateSecretRequest request = createSecretRequest(); + + final SecretResponse response = checkoutApi.forwardClient().createSecretSync(request); + + validateSecretResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldListSecretsSync() { + final SecretsListResponse response = checkoutApi.forwardClient().listSecretsSync(); + + validateSecretsListResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldUpdateSecretSync() { + final CreateSecretRequest createRequest = createSecretRequest(); + final SecretResponse createResponse = checkoutApi.forwardClient().createSecretSync(createRequest); + + final UpdateSecretRequest updateRequest = createUpdateSecretRequest(); + final SecretResponse response = checkoutApi.forwardClient().updateSecretSync(createResponse.getName(), updateRequest); + + validateSecretResponse(response); + } + + @Disabled("This test requires a valid id or Token source") + @Test + void shouldDeleteSecretSync() { + final CreateSecretRequest createRequest = createSecretRequest(); + final SecretResponse createResponse = checkoutApi.forwardClient().createSecretSync(createRequest); + + final EmptyResponse response = checkoutApi.forwardClient().deleteSecretSync(createResponse.getName()); + + validateEmptyResponse(response); + } + // Common methods private static ForwardRequest createForwardRequest() { final IdSource source = IdSource.builder() @@ -110,7 +198,7 @@ private static ForwardRequest createForwardRequest() { .method(MethodType.POST) .url("https://example.com/payments") .headers(headers) - .body("{\"amount\": 1000, \"currency\": \"USD\", \"reference\": \"some_reference\", \"source\": {\"type\": \"card\", \\\"number\\\": \\\"{{card_number}}\\\", \\\"expiry_month\\\": \\\"{{card_expiry_month}}\\\", \\\"expiry_year\\\": \\\"{{card_expiry_year_yyyy}}\\\", \\\"name\\\": \\\"Ali Farid\\\"}, \\\"payment_type\\\": \\\"Regular\\\", \\\"authorization_type\\\": \\\"Final\\\", \\\"capture\\\": true, \\\"processing_channel_id\\\": \\\"pc_xxxxxxxxxxx\\\", \\\"risk\\\": {\\\"enabled\\\": false}, \\\"merchant_initiated\\\": true}") + .body("{\"amount\": 1000, \"currency\": \"USD\", \"reference\": \"some_reference\", \"source\": {\"type\": \"card\", \"number\": \"{{card_number}}\", \"expiry_month\": \"{{card_expiry_month}}\", \"expiry_year\": \"{{card_expiry_year_yyyy}}\", \"name\": \"Ali Farid\"}, \"payment_type\": \"Regular\", \"authorization_type\": \"Final\", \"capture\": true, \"processing_channel_id\": \"pc_xxxxxxxxxxx\", \"risk\": {\"enabled\": false}, \"merchant_initiated\": true}") .signature(dlocalSignature) .build(); @@ -123,6 +211,21 @@ private static ForwardRequest createForwardRequest() { .build(); } + private CreateSecretRequest createSecretRequest() { + return CreateSecretRequest.builder() + .name("test_secret_" + System.currentTimeMillis()) + .value("test_secret_value") + .entityId("ent_test_123") + .build(); + } + + private UpdateSecretRequest createUpdateSecretRequest() { + return UpdateSecretRequest.builder() + .value("updated_secret_value") + .entityId("ent_test_456") + .build(); + } + private void validateForwardAnApiResponse(final ForwardAnApiResponse response) { assertNotNull(response); assertNotNull(response.getRequestId()); @@ -138,4 +241,22 @@ private void validateGetForwardResponse(final GetForwardResponse response) { assertNotNull(response.getReference()); assertNotNull(response.getDestinationResponse()); } + + private void validateSecretResponse(final SecretResponse response) { + assertNotNull(response); + assertNotNull(response.getName()); + assertNotNull(response.getCreatedAt()); + assertNotNull(response.getUpdatedAt()); + assertNotNull(response.getVersion()); + assertTrue(response.getVersion() >= 1); + } + + private void validateSecretsListResponse(final SecretsListResponse response) { + assertNotNull(response); + assertNotNull(response.getData()); + } + + private void validateEmptyResponse(final EmptyResponse response) { + assertNotNull(response); + } } From 6d583bab8b963a6781b15574cde1b8f4eaf02edc Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 5 Mar 2026 15:53:49 +0100 Subject: [PATCH 02/19] New module network token + test --- src/main/java/com/checkout/CheckoutApi.java | 3 + .../java/com/checkout/CheckoutApiImpl.java | 5 + .../networkTokens/NetworkTokensClient.java | 56 +++++ .../NetworkTokensClientImpl.java | 78 +++++++ .../networkTokens/entities/CardDetails.java | 14 ++ .../entities/DeletionReason.java | 17 ++ .../networkTokens/entities/InitiatedBy.java | 17 ++ .../entities/NetworkTokenDetails.java | 28 +++ .../entities/NetworkTokenState.java | 20 ++ .../entities/NetworkTokenType.java | 17 ++ .../entities/TransactionType.java | 19 ++ .../requests/DeleteNetworkTokenRequest.java | 17 ++ .../ProvisionNetworkTokenRequest.java | 13 ++ .../requests/RequestCryptogramRequest.java | 14 ++ .../sources/AbstractNetworkTokenSource.java | 18 ++ .../sources/CardNetworkTokenSource.java | 28 +++ .../sources/IdNetworkTokenSource.java | 19 ++ .../sources/NetworkTokenSourceType.java | 17 ++ .../responses/CryptogramResponse.java | 15 ++ .../responses/NetworkTokenResponse.java | 22 ++ .../NetworkTokensClientImplTest.java | 212 ++++++++++++++++++ .../networkTokens/NetworkTokensTestIT.java | 184 +++++++++++++++ 22 files changed, 833 insertions(+) create mode 100644 src/main/java/com/checkout/networkTokens/NetworkTokensClient.java create mode 100644 src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/CardDetails.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/DeletionReason.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java create mode 100644 src/main/java/com/checkout/networkTokens/entities/TransactionType.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java create mode 100644 src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java create mode 100644 src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java create mode 100644 src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java create mode 100644 src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java create mode 100644 src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index 408f5203..c246cf89 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -17,6 +17,7 @@ import com.checkout.instruments.InstrumentsClient; import com.checkout.issuing.IssuingClient; import com.checkout.metadata.MetadataClient; +import com.checkout.networkTokens.NetworkTokensClient; import com.checkout.payments.PaymentsClient; import com.checkout.payments.contexts.PaymentContextsClient; import com.checkout.payments.hosted.HostedPaymentsClient; @@ -84,4 +85,6 @@ public interface CheckoutApi extends CheckoutApmApi { AmlScreeningClient amlScreeningClient(); + NetworkTokensClient networkTokensClient(); + } diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 14587a70..702cb766 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -34,6 +34,8 @@ import com.checkout.issuing.IssuingClientImpl; import com.checkout.metadata.MetadataClient; import com.checkout.metadata.MetadataClientImpl; +import com.checkout.networkTokens.NetworkTokensClient; +import com.checkout.networkTokens.NetworkTokensClientImpl; import com.checkout.payments.PaymentsClient; import com.checkout.payments.PaymentsClientImpl; import com.checkout.payments.contexts.PaymentContextsClient; @@ -86,6 +88,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final IdentityVerificationClient identityVerificationClient; private final IdDocumentVerificationClient idDocumentVerificationClient; private final AmlScreeningClient amlScreeningClient; + private final NetworkTokensClient networkTokensClient; public CheckoutApiImpl(final CheckoutConfiguration configuration) { super(configuration); @@ -118,6 +121,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.identityVerificationClient = new IdentityVerificationClientImpl(this.apiClient, configuration); this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(this.apiClient, configuration); this.amlScreeningClient = new AmlScreeningClientImpl(this.apiClient, configuration); + this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); } @Override @@ -232,6 +236,7 @@ public MetadataClient metadataClient() { @Override public AmlScreeningClient amlScreeningClient() { return amlScreeningClient; } + public NetworkTokensClient networkTokensClient() { return networkTokensClient; } private ApiClient getFilesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new FilesApiUriStrategy(configuration)); diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java b/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java new file mode 100644 index 00000000..a13ce29e --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java @@ -0,0 +1,56 @@ +package com.checkout.networkTokens; + +import com.checkout.EmptyResponse; +import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networkTokens.requests.RequestCryptogramRequest; +import com.checkout.networkTokens.responses.CryptogramResponse; +import com.checkout.networkTokens.responses.NetworkTokenResponse; + +import java.util.concurrent.CompletableFuture; + +public interface NetworkTokensClient { + + /** + * Provision a Network Token + * Beta + * Provisions a network token synchronously. If the merchant stores their cards with Checkout.com, + * then source ID can be used to request a network token for the given card. If the merchant does + * not store their cards with Checkout.com, then card details have to be provided. + */ + CompletableFuture provisionNetworkToken(ProvisionNetworkTokenRequest provisionNetworkTokenRequest); + + /** + * Get Network Token + * Beta + * Given network token ID, this endpoint returns network token details: DPAN, expiry date, state, + * TRID and also card details like last four and expiry date. + */ + CompletableFuture getNetworkToken(String networkTokenId); + + /** + * Request a cryptogram + * Beta + * Using network token ID as an input, this endpoint returns token cryptogram. + */ + CompletableFuture requestCryptogram(String networkTokenId, RequestCryptogramRequest requestCryptogramRequest); + + /** + * Permanently deletes a network token + * Beta + * This endpoint is for permanently deleting a network token. A network token should be deleted + * when a payment instrument it is associated with is removed from file or if the security of the + * token has been compromised. + */ + CompletableFuture deleteNetworkToken(String networkTokenId, DeleteNetworkTokenRequest deleteNetworkTokenRequest); + + // Synchronous methods + NetworkTokenResponse provisionNetworkTokenSync(ProvisionNetworkTokenRequest provisionNetworkTokenRequest); + + NetworkTokenResponse getNetworkTokenSync(String networkTokenId); + + CryptogramResponse requestCryptogramSync(String networkTokenId, RequestCryptogramRequest requestCryptogramRequest); + + EmptyResponse deleteNetworkTokenSync(String networkTokenId, DeleteNetworkTokenRequest deleteNetworkTokenRequest); + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java b/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java new file mode 100644 index 00000000..6e2a32d0 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java @@ -0,0 +1,78 @@ +package com.checkout.networkTokens; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; +import com.checkout.SdkAuthorizationType; +import com.checkout.common.CheckoutUtils; +import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networkTokens.requests.RequestCryptogramRequest; +import com.checkout.networkTokens.responses.CryptogramResponse; +import com.checkout.networkTokens.responses.NetworkTokenResponse; + +import java.util.concurrent.CompletableFuture; + +public class NetworkTokensClientImpl extends AbstractClient implements NetworkTokensClient { + + private static final String NETWORK_TOKENS_PATH = "network-tokens"; + + public NetworkTokensClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.OAUTH); + } + + @Override + public CompletableFuture provisionNetworkToken(final ProvisionNetworkTokenRequest provisionNetworkTokenRequest) { + CheckoutUtils.validateParams("provisionNetworkTokenRequest", provisionNetworkTokenRequest); + return apiClient.postAsync(NETWORK_TOKENS_PATH, sdkAuthorization(), NetworkTokenResponse.class, provisionNetworkTokenRequest, null); + } + + @Override + public CompletableFuture getNetworkToken(final String networkTokenId) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + return apiClient.getAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId), sdkAuthorization(), NetworkTokenResponse.class); + } + + @Override + public CompletableFuture requestCryptogram(final String networkTokenId, final RequestCryptogramRequest requestCryptogramRequest) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + CheckoutUtils.validateParams("requestCryptogramRequest", requestCryptogramRequest); + return apiClient.postAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "cryptograms"), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); + } + + @Override + public CompletableFuture deleteNetworkToken(final String networkTokenId, final DeleteNetworkTokenRequest deleteNetworkTokenRequest) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + CheckoutUtils.validateParams("deleteNetworkTokenRequest", deleteNetworkTokenRequest); + return apiClient.patchAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "delete"), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); + } + + // Synchronous methods + @Override + public NetworkTokenResponse provisionNetworkTokenSync(final ProvisionNetworkTokenRequest provisionNetworkTokenRequest) { + CheckoutUtils.validateParams("provisionNetworkTokenRequest", provisionNetworkTokenRequest); + return apiClient.post(NETWORK_TOKENS_PATH, sdkAuthorization(), NetworkTokenResponse.class, provisionNetworkTokenRequest, null); + } + + @Override + public NetworkTokenResponse getNetworkTokenSync(final String networkTokenId) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + return apiClient.get(buildPath(NETWORK_TOKENS_PATH, networkTokenId), sdkAuthorization(), NetworkTokenResponse.class); + } + + @Override + public CryptogramResponse requestCryptogramSync(final String networkTokenId, final RequestCryptogramRequest requestCryptogramRequest) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + CheckoutUtils.validateParams("requestCryptogramRequest", requestCryptogramRequest); + return apiClient.post(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "cryptograms"), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); + } + + @Override + public EmptyResponse deleteNetworkTokenSync(final String networkTokenId, final DeleteNetworkTokenRequest deleteNetworkTokenRequest) { + CheckoutUtils.validateParams("networkTokenId", networkTokenId); + CheckoutUtils.validateParams("deleteNetworkTokenRequest", deleteNetworkTokenRequest); + return apiClient.patch(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "delete"), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); + } + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/CardDetails.java b/src/main/java/com/checkout/networkTokens/entities/CardDetails.java new file mode 100644 index 00000000..2549f88c --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/CardDetails.java @@ -0,0 +1,14 @@ +package com.checkout.networkTokens.entities; + +import lombok.Data; + +@Data +public class CardDetails { + + private String last4; + + private String expiryMonth; + + private String expiryYear; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java b/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java new file mode 100644 index 00000000..0ae1001c --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java @@ -0,0 +1,17 @@ +package com.checkout.networkTokens.entities; + +public enum DeletionReason { + + FRAUD("fraud"), + OTHER("other"); + + private final String reason; + + DeletionReason(final String reason) { + this.reason = reason; + } + + public String getReason() { + return reason; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java b/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java new file mode 100644 index 00000000..1233221b --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java @@ -0,0 +1,17 @@ +package com.checkout.networkTokens.entities; + +public enum InitiatedBy { + + CARDHOLDER("cardholder"), + TOKEN_REQUESTOR("token_requestor"); + + private final String initiatedBy; + + InitiatedBy(final String initiatedBy) { + this.initiatedBy = initiatedBy; + } + + public String getInitiatedBy() { + return initiatedBy; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java new file mode 100644 index 00000000..2ccafa46 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java @@ -0,0 +1,28 @@ +package com.checkout.networkTokens.entities; + +import lombok.Data; + +import java.time.Instant; + +@Data +public class NetworkTokenDetails { + + private String id; + + private NetworkTokenState state; + + private String number; + + private String expiryMonth; + + private String expiryYear; + + private NetworkTokenType type; + + private Instant createdOn; + + private Instant modifiedOn; + + private String paymentAccountReference; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java new file mode 100644 index 00000000..5e1ffd92 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java @@ -0,0 +1,20 @@ +package com.checkout.networkTokens.entities; + +public enum NetworkTokenState { + + ACTIVE("active"), + SUSPENDED("suspended"), + INACTIVE("inactive"), + DECLINED("declined"), + REQUESTED("requested"); + + private final String state; + + NetworkTokenState(final String state) { + this.state = state; + } + + public String getState() { + return state; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java new file mode 100644 index 00000000..ffe9ac15 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java @@ -0,0 +1,17 @@ +package com.checkout.networkTokens.entities; + +public enum NetworkTokenType { + + VTS("vts"), + MDES("mdes"); + + private final String type; + + NetworkTokenType(final String type) { + this.type = type; + } + + public String getType() { + return type; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/TransactionType.java b/src/main/java/com/checkout/networkTokens/entities/TransactionType.java new file mode 100644 index 00000000..a1c873c5 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/TransactionType.java @@ -0,0 +1,19 @@ +package com.checkout.networkTokens.entities; + +public enum TransactionType { + + ECOM("ecom"), + RECURRING("recurring"), + POS("pos"), + AFT("aft"); + + private final String transactionType; + + TransactionType(final String transactionType) { + this.transactionType = transactionType; + } + + public String getTransactionType() { + return transactionType; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java new file mode 100644 index 00000000..2f026d87 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java @@ -0,0 +1,17 @@ +package com.checkout.networkTokens.requests; + +import com.checkout.networkTokens.entities.DeletionReason; +import com.checkout.networkTokens.entities.InitiatedBy; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DeleteNetworkTokenRequest { + + private InitiatedBy initiatedBy; + + private DeletionReason reason; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java new file mode 100644 index 00000000..3b416094 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java @@ -0,0 +1,13 @@ +package com.checkout.networkTokens.requests; + +import com.checkout.networkTokens.requests.sources.AbstractNetworkTokenSource; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ProvisionNetworkTokenRequest { + + private AbstractNetworkTokenSource source; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java b/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java new file mode 100644 index 00000000..54f751d2 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java @@ -0,0 +1,14 @@ +package com.checkout.networkTokens.requests; + +import com.checkout.networkTokens.entities.TransactionType; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class RequestCryptogramRequest { + + private TransactionType transactionType; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java new file mode 100644 index 00000000..771beb52 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java @@ -0,0 +1,18 @@ +package com.checkout.networkTokens.requests.sources; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public abstract class AbstractNetworkTokenSource { + + @SerializedName("type") + private NetworkTokenSourceType type; + + public AbstractNetworkTokenSource(final NetworkTokenSourceType type) { + this.type = type; + } + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java new file mode 100644 index 00000000..6d204dea --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java @@ -0,0 +1,28 @@ +package com.checkout.networkTokens.requests.sources; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CardNetworkTokenSource extends AbstractNetworkTokenSource { + + private String number; + + private String expiryMonth; + + private String expiryYear; + + private String cvv; + + @Builder + public CardNetworkTokenSource(final String number, final String expiryMonth, final String expiryYear, final String cvv) { + super(NetworkTokenSourceType.CARD); + this.number = number; + this.expiryMonth = expiryMonth; + this.expiryYear = expiryYear; + this.cvv = cvv; + } + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java new file mode 100644 index 00000000..3657d296 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java @@ -0,0 +1,19 @@ +package com.checkout.networkTokens.requests.sources; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class IdNetworkTokenSource extends AbstractNetworkTokenSource { + + private String id; + + @Builder + public IdNetworkTokenSource(final String id) { + super(NetworkTokenSourceType.ID); + this.id = id; + } + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java b/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java new file mode 100644 index 00000000..266214e2 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java @@ -0,0 +1,17 @@ +package com.checkout.networkTokens.requests.sources; + +public enum NetworkTokenSourceType { + + ID("id"), + CARD("card"); + + private final String sourceType; + + NetworkTokenSourceType(final String sourceType) { + this.sourceType = sourceType; + } + + public String getSourceType() { + return sourceType; + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java b/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java new file mode 100644 index 00000000..72fac1c5 --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java @@ -0,0 +1,15 @@ +package com.checkout.networkTokens.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class CryptogramResponse extends Resource { + + private String cryptogram; + + private String eci; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java b/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java new file mode 100644 index 00000000..640e2f0e --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java @@ -0,0 +1,22 @@ +package com.checkout.networkTokens.responses; + +import com.checkout.common.Resource; +import com.checkout.networkTokens.entities.CardDetails; +import com.checkout.networkTokens.entities.NetworkTokenDetails; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class NetworkTokenResponse extends Resource { + + private CardDetails card; + + private NetworkTokenDetails networkToken; + + private String tokenRequestorId; + + private String tokenSchemeId; + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java new file mode 100644 index 00000000..8a61896a --- /dev/null +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java @@ -0,0 +1,212 @@ +package com.checkout.networkTokens; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.networkTokens.entities.DeletionReason; +import com.checkout.networkTokens.entities.InitiatedBy; +import com.checkout.networkTokens.entities.TransactionType; +import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networkTokens.requests.RequestCryptogramRequest; +import com.checkout.networkTokens.requests.sources.IdNetworkTokenSource; +import com.checkout.networkTokens.responses.CryptogramResponse; +import com.checkout.networkTokens.responses.NetworkTokenResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.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 NetworkTokensClientImplTest { + + private NetworkTokensClient client; + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + @BeforeEach + void setUp() { + when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new NetworkTokensClientImpl(apiClient, configuration); + } + + @Test + void shouldProvisionNetworkToken() throws ExecutionException, InterruptedException { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequest(); + final NetworkTokenResponse response = mock(NetworkTokenResponse.class); + + when(apiClient.postAsync(eq("network-tokens"), eq(authorization), eq(NetworkTokenResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.provisionNetworkToken(request); + + validateNetworkTokenResponse(response, future.get()); + } + + @Test + void shouldGetNetworkToken() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final NetworkTokenResponse response = mock(NetworkTokenResponse.class); + + when(apiClient.getAsync(eq("network-tokens/" + networkTokenId), eq(authorization), eq(NetworkTokenResponse.class))) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.getNetworkToken(networkTokenId); + + validateNetworkTokenResponse(response, future.get()); + } + + @Test + void shouldRequestCryptogram() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final RequestCryptogramRequest request = createRequestCryptogramRequest(); + final CryptogramResponse response = mock(CryptogramResponse.class); + + when(apiClient.postAsync(eq("network-tokens/" + networkTokenId + "/cryptograms"), eq(authorization), eq(CryptogramResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.requestCryptogram(networkTokenId, request); + + validateCryptogramResponse(response, future.get()); + } + + @Test + void shouldDeleteNetworkToken() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final DeleteNetworkTokenRequest request = createDeleteNetworkTokenRequest(); + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.patchAsync(eq("network-tokens/" + networkTokenId + "/delete"), eq(authorization), eq(EmptyResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.deleteNetworkToken(networkTokenId, request); + + validateEmptyResponse(response, future.get()); + } + + // Synchronous methods + @Test + void shouldProvisionNetworkTokenSync() throws ExecutionException, InterruptedException { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequest(); + final NetworkTokenResponse response = mock(NetworkTokenResponse.class); + + when(apiClient.post(eq("network-tokens"), eq(authorization), eq(NetworkTokenResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final NetworkTokenResponse result = client.provisionNetworkTokenSync(request); + + validateNetworkTokenResponse(response, result); + } + + @Test + void shouldGetNetworkTokenSync() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final NetworkTokenResponse response = mock(NetworkTokenResponse.class); + + when(apiClient.get(eq("network-tokens/" + networkTokenId), eq(authorization), eq(NetworkTokenResponse.class))) + .thenReturn(response); + + final NetworkTokenResponse result = client.getNetworkTokenSync(networkTokenId); + + validateNetworkTokenResponse(response, result); + } + + @Test + void shouldRequestCryptogramSync() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final RequestCryptogramRequest request = createRequestCryptogramRequest(); + final CryptogramResponse response = mock(CryptogramResponse.class); + + when(apiClient.post(eq("network-tokens/" + networkTokenId + "/cryptograms"), eq(authorization), eq(CryptogramResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final CryptogramResponse result = client.requestCryptogramSync(networkTokenId, request); + + validateCryptogramResponse(response, result); + } + + @Test + void shouldDeleteNetworkTokenSync() throws ExecutionException, InterruptedException { + final String networkTokenId = "nt_xgu3isllqfyu7ktpk5z2yxbwna"; + final DeleteNetworkTokenRequest request = createDeleteNetworkTokenRequest(); + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.patch(eq("network-tokens/" + networkTokenId + "/delete"), eq(authorization), eq(EmptyResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final EmptyResponse result = client.deleteNetworkTokenSync(networkTokenId, request); + + validateEmptyResponse(response, result); + } + + // Common methods + private ProvisionNetworkTokenRequest createProvisionNetworkTokenRequest() { + final IdNetworkTokenSource source = IdNetworkTokenSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + return ProvisionNetworkTokenRequest.builder() + .source(source) + .build(); + } + + private RequestCryptogramRequest createRequestCryptogramRequest() { + return RequestCryptogramRequest.builder() + .transactionType(TransactionType.ECOM) + .build(); + } + + private DeleteNetworkTokenRequest createDeleteNetworkTokenRequest() { + return DeleteNetworkTokenRequest.builder() + .initiatedBy(InitiatedBy.TOKEN_REQUESTOR) + .reason(DeletionReason.OTHER) + .build(); + } + + private void validateNetworkTokenResponse(final NetworkTokenResponse response, final NetworkTokenResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateCryptogramResponse(final CryptogramResponse response, final CryptogramResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateEmptyResponse(final EmptyResponse response, final EmptyResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java new file mode 100644 index 00000000..62d39a2d --- /dev/null +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java @@ -0,0 +1,184 @@ +package com.checkout.networkTokens; + +import com.checkout.EmptyResponse; +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.networkTokens.entities.DeletionReason; +import com.checkout.networkTokens.entities.InitiatedBy; +import com.checkout.networkTokens.entities.TransactionType; +import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networkTokens.requests.RequestCryptogramRequest; +import com.checkout.networkTokens.requests.sources.CardNetworkTokenSource; +import com.checkout.networkTokens.requests.sources.IdNetworkTokenSource; +import com.checkout.networkTokens.responses.CryptogramResponse; +import com.checkout.networkTokens.responses.NetworkTokenResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Disabled("Network token endpoints not yet available in test environment") +public class NetworkTokensTestIT extends SandboxTestFixture { + + public NetworkTokensTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Test + void shouldProvisionNetworkTokenWithId() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithId(); + + final NetworkTokenResponse response = blocking(() -> checkoutApi.networkTokensClient().provisionNetworkToken(request)); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldProvisionNetworkTokenWithCard() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + + final NetworkTokenResponse response = blocking(() -> checkoutApi.networkTokensClient().provisionNetworkToken(request)); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldGetNetworkToken() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = blocking(() -> checkoutApi.networkTokensClient().provisionNetworkToken(request)); + + final NetworkTokenResponse response = blocking(() -> checkoutApi.networkTokensClient().getNetworkToken(provisionResponse.getNetworkToken().getId())); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldRequestCryptogram() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = blocking(() -> checkoutApi.networkTokensClient().provisionNetworkToken(request)); + + final RequestCryptogramRequest cryptogramRequest = createRequestCryptogramRequest(); + final CryptogramResponse response = blocking(() -> checkoutApi.networkTokensClient().requestCryptogram(provisionResponse.getNetworkToken().getId(), cryptogramRequest)); + + validateCryptogramResponse(response); + } + + @Test + void shouldDeleteNetworkToken() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = blocking(() -> checkoutApi.networkTokensClient().provisionNetworkToken(request)); + + final DeleteNetworkTokenRequest deleteRequest = createDeleteNetworkTokenRequest(); + final EmptyResponse response = blocking(() -> checkoutApi.networkTokensClient().deleteNetworkToken(provisionResponse.getNetworkToken().getId(), deleteRequest)); + + validateEmptyResponse(response); + } + + // Synchronous methods + @Test + void shouldProvisionNetworkTokenWithIdSync() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithId(); + + final NetworkTokenResponse response = checkoutApi.networkTokensClient().provisionNetworkTokenSync(request); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldProvisionNetworkTokenWithCardSync() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + + final NetworkTokenResponse response = checkoutApi.networkTokensClient().provisionNetworkTokenSync(request); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldGetNetworkTokenSync() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = checkoutApi.networkTokensClient().provisionNetworkTokenSync(request); + + final NetworkTokenResponse response = checkoutApi.networkTokensClient().getNetworkTokenSync(provisionResponse.getNetworkToken().getId()); + + validateNetworkTokenResponse(response); + } + + @Test + void shouldRequestCryptogramSync() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = checkoutApi.networkTokensClient().provisionNetworkTokenSync(request); + + final RequestCryptogramRequest cryptogramRequest = createRequestCryptogramRequest(); + final CryptogramResponse response = checkoutApi.networkTokensClient().requestCryptogramSync(provisionResponse.getNetworkToken().getId(), cryptogramRequest); + + validateCryptogramResponse(response); + } + + @Test + void shouldDeleteNetworkTokenSync() { + final ProvisionNetworkTokenRequest request = createProvisionNetworkTokenRequestWithCard(); + final NetworkTokenResponse provisionResponse = checkoutApi.networkTokensClient().provisionNetworkTokenSync(request); + + final DeleteNetworkTokenRequest deleteRequest = createDeleteNetworkTokenRequest(); + final EmptyResponse response = checkoutApi.networkTokensClient().deleteNetworkTokenSync(provisionResponse.getNetworkToken().getId(), deleteRequest); + + validateEmptyResponse(response); + } + + // Common methods + private ProvisionNetworkTokenRequest createProvisionNetworkTokenRequestWithId() { + final IdNetworkTokenSource source = IdNetworkTokenSource.builder() + .id("src_wmlfc3zyhqzehihu7giusaaawu") + .build(); + + return ProvisionNetworkTokenRequest.builder() + .source(source) + .build(); + } + + private ProvisionNetworkTokenRequest createProvisionNetworkTokenRequestWithCard() { + final CardNetworkTokenSource source = CardNetworkTokenSource.builder() + .number("4539467987109256") + .expiryMonth("10") + .expiryYear("2027") + .build(); + + return ProvisionNetworkTokenRequest.builder() + .source(source) + .build(); + } + + private RequestCryptogramRequest createRequestCryptogramRequest() { + return RequestCryptogramRequest.builder() + .transactionType(TransactionType.ECOM) + .build(); + } + + private DeleteNetworkTokenRequest createDeleteNetworkTokenRequest() { + return DeleteNetworkTokenRequest.builder() + .initiatedBy(InitiatedBy.TOKEN_REQUESTOR) + .reason(DeletionReason.OTHER) + .build(); + } + + private void validateNetworkTokenResponse(final NetworkTokenResponse response) { + assertNotNull(response); + assertNotNull(response.getCard()); + assertNotNull(response.getNetworkToken()); + assertNotNull(response.getNetworkToken().getId()); + assertNotNull(response.getNetworkToken().getState()); + assertNotNull(response.getNetworkToken().getType()); + } + + private void validateCryptogramResponse(final CryptogramResponse response) { + assertNotNull(response); + assertNotNull(response.getCryptogram()); + } + + private void validateEmptyResponse(final EmptyResponse response) { + assertNotNull(response); + } + +} \ No newline at end of file From ac91466182032213047ccfac67102d705fb3b6fa Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 5 Mar 2026 18:17:31 +0100 Subject: [PATCH 03/19] New module network tokens + tests (unit + integration) --- src/main/java/com/checkout/CheckoutApi.java | 2 +- .../java/com/checkout/CheckoutApiImpl.java | 4 +-- src/main/java/com/checkout/OAuthScope.java | 1 + .../networkTokens/NetworkTokensClient.java | 12 +++---- .../NetworkTokensClientImpl.java | 34 +++++++++---------- .../AbstractNetworkTokenSource.java | 4 +-- .../networkTokens/entities/CardDetails.java | 2 +- .../CardNetworkTokenSource.java | 2 +- .../entities/DeletionReason.java | 19 ++++------- .../IdNetworkTokenSource.java | 2 +- .../networkTokens/entities/InitiatedBy.java | 20 ++++------- .../entities/NetworkTokenDetails.java | 2 +- .../entities/NetworkTokenSourceType.java | 11 ++++++ .../entities/NetworkTokenState.java | 28 +++++++-------- .../entities/NetworkTokenType.java | 20 ++++------- .../entities/TransactionType.java | 24 ++++++------- .../requests/DeleteNetworkTokenRequest.java | 6 ++-- .../ProvisionNetworkTokenRequest.java | 5 +-- .../requests/RequestCryptogramRequest.java | 4 +-- .../sources/NetworkTokenSourceType.java | 17 ---------- .../responses/CryptogramResponse.java | 2 +- .../responses/NetworkTokenResponse.java | 6 ++-- .../NetworkTokensClientImplTest.java | 21 +++++++----- .../networkTokens/NetworkTokensTestIT.java | 26 +++++++------- 24 files changed, 125 insertions(+), 149 deletions(-) rename src/main/java/com/checkout/networkTokens/{requests/sources => entities}/AbstractNetworkTokenSource.java (68%) rename src/main/java/com/checkout/networkTokens/{requests/sources => entities}/CardNetworkTokenSource.java (92%) rename src/main/java/com/checkout/networkTokens/{requests/sources => entities}/IdNetworkTokenSource.java (86%) create mode 100644 src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java delete mode 100644 src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index c246cf89..aadbbebd 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -17,7 +17,7 @@ import com.checkout.instruments.InstrumentsClient; import com.checkout.issuing.IssuingClient; import com.checkout.metadata.MetadataClient; -import com.checkout.networkTokens.NetworkTokensClient; +import com.checkout.networktokens.NetworkTokensClient; import com.checkout.payments.PaymentsClient; import com.checkout.payments.contexts.PaymentContextsClient; import com.checkout.payments.hosted.HostedPaymentsClient; diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 702cb766..b966206c 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -34,8 +34,8 @@ import com.checkout.issuing.IssuingClientImpl; import com.checkout.metadata.MetadataClient; import com.checkout.metadata.MetadataClientImpl; -import com.checkout.networkTokens.NetworkTokensClient; -import com.checkout.networkTokens.NetworkTokensClientImpl; +import com.checkout.networktokens.NetworkTokensClient; +import com.checkout.networktokens.NetworkTokensClientImpl; import com.checkout.payments.PaymentsClient; import com.checkout.payments.PaymentsClientImpl; import com.checkout.payments.contexts.PaymentContextsClient; diff --git a/src/main/java/com/checkout/OAuthScope.java b/src/main/java/com/checkout/OAuthScope.java index a9238277..53911ef9 100644 --- a/src/main/java/com/checkout/OAuthScope.java +++ b/src/main/java/com/checkout/OAuthScope.java @@ -55,6 +55,7 @@ public enum OAuthScope { VAULT_CARD_METADATA("vault:card-metadata"), VAULT_INSTRUMENTS("vault:instruments"), VAULT_TOKENIZATION("vault:tokenization"), + VAULT_NETWORK_TOKENS("vault:network-tokens"), FORWARD("forward"), FORWARD_SECRETS("forward:secrets"); diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java b/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java index a13ce29e..cb5e62c8 100644 --- a/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java +++ b/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java @@ -1,11 +1,11 @@ -package com.checkout.networkTokens; +package com.checkout.networktokens; import com.checkout.EmptyResponse; -import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; -import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; -import com.checkout.networkTokens.requests.RequestCryptogramRequest; -import com.checkout.networkTokens.responses.CryptogramResponse; -import com.checkout.networkTokens.responses.NetworkTokenResponse; +import com.checkout.networktokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networktokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networktokens.requests.RequestCryptogramRequest; +import com.checkout.networktokens.responses.CryptogramResponse; +import com.checkout.networktokens.responses.NetworkTokenResponse; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java b/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java index 6e2a32d0..17ef8d01 100644 --- a/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java +++ b/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens; +package com.checkout.networktokens; import com.checkout.AbstractClient; import com.checkout.ApiClient; @@ -6,17 +6,19 @@ import com.checkout.EmptyResponse; import com.checkout.SdkAuthorizationType; import com.checkout.common.CheckoutUtils; -import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; -import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; -import com.checkout.networkTokens.requests.RequestCryptogramRequest; -import com.checkout.networkTokens.responses.CryptogramResponse; -import com.checkout.networkTokens.responses.NetworkTokenResponse; +import com.checkout.networktokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networktokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networktokens.requests.RequestCryptogramRequest; +import com.checkout.networktokens.responses.CryptogramResponse; +import com.checkout.networktokens.responses.NetworkTokenResponse; import java.util.concurrent.CompletableFuture; public class NetworkTokensClientImpl extends AbstractClient implements NetworkTokensClient { private static final String NETWORK_TOKENS_PATH = "network-tokens"; + private static final String CRYPTOGRAMS_PATH = "cryptograms"; + private static final String DELETE_PATH = "delete"; public NetworkTokensClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { super(apiClient, configuration, SdkAuthorizationType.OAUTH); @@ -36,16 +38,14 @@ public CompletableFuture getNetworkToken(final String netw @Override public CompletableFuture requestCryptogram(final String networkTokenId, final RequestCryptogramRequest requestCryptogramRequest) { - CheckoutUtils.validateParams("networkTokenId", networkTokenId); - CheckoutUtils.validateParams("requestCryptogramRequest", requestCryptogramRequest); - return apiClient.postAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "cryptograms"), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); + CheckoutUtils.validateParams("networkTokenId", networkTokenId, "requestCryptogramRequest", requestCryptogramRequest); + return apiClient.postAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, CRYPTOGRAMS_PATH), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); } @Override public CompletableFuture deleteNetworkToken(final String networkTokenId, final DeleteNetworkTokenRequest deleteNetworkTokenRequest) { - CheckoutUtils.validateParams("networkTokenId", networkTokenId); - CheckoutUtils.validateParams("deleteNetworkTokenRequest", deleteNetworkTokenRequest); - return apiClient.patchAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "delete"), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); + CheckoutUtils.validateParams("networkTokenId", networkTokenId, "deleteNetworkTokenRequest", deleteNetworkTokenRequest); + return apiClient.patchAsync(buildPath(NETWORK_TOKENS_PATH, networkTokenId, DELETE_PATH), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); } // Synchronous methods @@ -63,16 +63,14 @@ public NetworkTokenResponse getNetworkTokenSync(final String networkTokenId) { @Override public CryptogramResponse requestCryptogramSync(final String networkTokenId, final RequestCryptogramRequest requestCryptogramRequest) { - CheckoutUtils.validateParams("networkTokenId", networkTokenId); - CheckoutUtils.validateParams("requestCryptogramRequest", requestCryptogramRequest); - return apiClient.post(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "cryptograms"), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); + CheckoutUtils.validateParams("networkTokenId", networkTokenId, "requestCryptogramRequest", requestCryptogramRequest); + return apiClient.post(buildPath(NETWORK_TOKENS_PATH, networkTokenId, CRYPTOGRAMS_PATH), sdkAuthorization(), CryptogramResponse.class, requestCryptogramRequest, null); } @Override public EmptyResponse deleteNetworkTokenSync(final String networkTokenId, final DeleteNetworkTokenRequest deleteNetworkTokenRequest) { - CheckoutUtils.validateParams("networkTokenId", networkTokenId); - CheckoutUtils.validateParams("deleteNetworkTokenRequest", deleteNetworkTokenRequest); - return apiClient.patch(buildPath(NETWORK_TOKENS_PATH, networkTokenId, "delete"), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); + CheckoutUtils.validateParams("networkTokenId", networkTokenId, "deleteNetworkTokenRequest", deleteNetworkTokenRequest); + return apiClient.patch(buildPath(NETWORK_TOKENS_PATH, networkTokenId, DELETE_PATH), sdkAuthorization(), EmptyResponse.class, deleteNetworkTokenRequest, null); } } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/entities/AbstractNetworkTokenSource.java similarity index 68% rename from src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens/entities/AbstractNetworkTokenSource.java index 771beb52..10420bfe 100644 --- a/src/main/java/com/checkout/networkTokens/requests/sources/AbstractNetworkTokenSource.java +++ b/src/main/java/com/checkout/networkTokens/entities/AbstractNetworkTokenSource.java @@ -1,6 +1,5 @@ -package com.checkout.networkTokens.requests.sources; +package com.checkout.networktokens.entities; -import com.google.gson.annotations.SerializedName; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,7 +7,6 @@ @NoArgsConstructor public abstract class AbstractNetworkTokenSource { - @SerializedName("type") private NetworkTokenSourceType type; public AbstractNetworkTokenSource(final NetworkTokenSourceType type) { diff --git a/src/main/java/com/checkout/networkTokens/entities/CardDetails.java b/src/main/java/com/checkout/networkTokens/entities/CardDetails.java index 2549f88c..1c9dc7e8 100644 --- a/src/main/java/com/checkout/networkTokens/entities/CardDetails.java +++ b/src/main/java/com/checkout/networkTokens/entities/CardDetails.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/entities/CardNetworkTokenSource.java similarity index 92% rename from src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens/entities/CardNetworkTokenSource.java index 6d204dea..467d2d8d 100644 --- a/src/main/java/com/checkout/networkTokens/requests/sources/CardNetworkTokenSource.java +++ b/src/main/java/com/checkout/networkTokens/entities/CardNetworkTokenSource.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens.requests.sources; +package com.checkout.networktokens.entities; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java b/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java index 0ae1001c..420f8537 100644 --- a/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java +++ b/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java @@ -1,17 +1,12 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; public enum DeletionReason { - FRAUD("fraud"), - OTHER("other"); - - private final String reason; - - DeletionReason(final String reason) { - this.reason = reason; - } + @SerializedName("fraud") + FRAUD, - public String getReason() { - return reason; - } + @SerializedName("other") + OTHER } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens/entities/IdNetworkTokenSource.java similarity index 86% rename from src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens/entities/IdNetworkTokenSource.java index 3657d296..84cb1799 100644 --- a/src/main/java/com/checkout/networkTokens/requests/sources/IdNetworkTokenSource.java +++ b/src/main/java/com/checkout/networkTokens/entities/IdNetworkTokenSource.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens.requests.sources; +package com.checkout.networktokens.entities; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java b/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java index 1233221b..bc6da695 100644 --- a/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java +++ b/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java @@ -1,17 +1,11 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; public enum InitiatedBy { + @SerializedName("cardholder") + CARDHOLDER, - CARDHOLDER("cardholder"), - TOKEN_REQUESTOR("token_requestor"); - - private final String initiatedBy; - - InitiatedBy(final String initiatedBy) { - this.initiatedBy = initiatedBy; - } - - public String getInitiatedBy() { - return initiatedBy; - } + @SerializedName("token_requestor") + TOKEN_REQUESTOR } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java index 2ccafa46..ae505fde 100644 --- a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java new file mode 100644 index 00000000..dba0e0fc --- /dev/null +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java @@ -0,0 +1,11 @@ +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; + +public enum NetworkTokenSourceType { + @SerializedName("id") + ID, + + @SerializedName("card") + CARD +} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java index 5e1ffd92..120adc52 100644 --- a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java @@ -1,20 +1,20 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; public enum NetworkTokenState { + @SerializedName("active") + ACTIVE, - ACTIVE("active"), - SUSPENDED("suspended"), - INACTIVE("inactive"), - DECLINED("declined"), - REQUESTED("requested"); - - private final String state; + @SerializedName("suspended") + SUSPENDED, + + @SerializedName("inactive") + INACTIVE, - NetworkTokenState(final String state) { - this.state = state; - } + @SerializedName("declined") + DECLINED, - public String getState() { - return state; - } + @SerializedName("requested") + REQUESTED } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java index ffe9ac15..97817851 100644 --- a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java +++ b/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java @@ -1,17 +1,11 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; public enum NetworkTokenType { + @SerializedName("vts") + VTS, - VTS("vts"), - MDES("mdes"); - - private final String type; - - NetworkTokenType(final String type) { - this.type = type; - } - - public String getType() { - return type; - } + @SerializedName("mdes") + MDES } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/entities/TransactionType.java b/src/main/java/com/checkout/networkTokens/entities/TransactionType.java index a1c873c5..f0155f59 100644 --- a/src/main/java/com/checkout/networkTokens/entities/TransactionType.java +++ b/src/main/java/com/checkout/networkTokens/entities/TransactionType.java @@ -1,19 +1,17 @@ -package com.checkout.networkTokens.entities; +package com.checkout.networktokens.entities; + +import com.google.gson.annotations.SerializedName; public enum TransactionType { - - ECOM("ecom"), - RECURRING("recurring"), - POS("pos"), - AFT("aft"); + @SerializedName("ecom") + ECOM, - private final String transactionType; + @SerializedName("recurring") + RECURRING, - TransactionType(final String transactionType) { - this.transactionType = transactionType; - } + @SerializedName("pos") + POS, - public String getTransactionType() { - return transactionType; - } + @SerializedName("aft") + AFT } \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java index 2f026d87..4ed5dc8f 100644 --- a/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java +++ b/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java @@ -1,7 +1,7 @@ -package com.checkout.networkTokens.requests; +package com.checkout.networktokens.requests; -import com.checkout.networkTokens.entities.DeletionReason; -import com.checkout.networkTokens.entities.InitiatedBy; +import com.checkout.networktokens.entities.DeletionReason; +import com.checkout.networktokens.entities.InitiatedBy; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java index 3b416094..69e6fa3d 100644 --- a/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java +++ b/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java @@ -1,6 +1,7 @@ -package com.checkout.networkTokens.requests; +package com.checkout.networktokens.requests; + +import com.checkout.networktokens.entities.AbstractNetworkTokenSource; -import com.checkout.networkTokens.requests.sources.AbstractNetworkTokenSource; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java b/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java index 54f751d2..195feb00 100644 --- a/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java +++ b/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java @@ -1,6 +1,6 @@ -package com.checkout.networkTokens.requests; +package com.checkout.networktokens.requests; -import com.checkout.networkTokens.entities.TransactionType; +import com.checkout.networktokens.entities.TransactionType; import lombok.Builder; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java b/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java deleted file mode 100644 index 266214e2..00000000 --- a/src/main/java/com/checkout/networkTokens/requests/sources/NetworkTokenSourceType.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.checkout.networkTokens.requests.sources; - -public enum NetworkTokenSourceType { - - ID("id"), - CARD("card"); - - private final String sourceType; - - NetworkTokenSourceType(final String sourceType) { - this.sourceType = sourceType; - } - - public String getSourceType() { - return sourceType; - } -} \ No newline at end of file diff --git a/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java b/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java index 72fac1c5..bc976224 100644 --- a/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java +++ b/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java @@ -1,4 +1,4 @@ -package com.checkout.networkTokens.responses; +package com.checkout.networktokens.responses; import com.checkout.common.Resource; import lombok.Data; diff --git a/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java b/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java index 640e2f0e..28afdf27 100644 --- a/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java +++ b/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java @@ -1,8 +1,8 @@ -package com.checkout.networkTokens.responses; +package com.checkout.networktokens.responses; import com.checkout.common.Resource; -import com.checkout.networkTokens.entities.CardDetails; -import com.checkout.networkTokens.entities.NetworkTokenDetails; +import com.checkout.networktokens.entities.CardDetails; +import com.checkout.networktokens.entities.NetworkTokenDetails; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java index 8a61896a..516b5e57 100644 --- a/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensClientImplTest.java @@ -6,15 +6,18 @@ import com.checkout.SdkAuthorization; import com.checkout.SdkAuthorizationType; import com.checkout.SdkCredentials; -import com.checkout.networkTokens.entities.DeletionReason; -import com.checkout.networkTokens.entities.InitiatedBy; -import com.checkout.networkTokens.entities.TransactionType; -import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; -import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; -import com.checkout.networkTokens.requests.RequestCryptogramRequest; -import com.checkout.networkTokens.requests.sources.IdNetworkTokenSource; -import com.checkout.networkTokens.responses.CryptogramResponse; -import com.checkout.networkTokens.responses.NetworkTokenResponse; +import com.checkout.networktokens.NetworkTokensClient; +import com.checkout.networktokens.NetworkTokensClientImpl; +import com.checkout.networktokens.entities.DeletionReason; +import com.checkout.networktokens.entities.IdNetworkTokenSource; +import com.checkout.networktokens.entities.InitiatedBy; +import com.checkout.networktokens.entities.TransactionType; +import com.checkout.networktokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networktokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networktokens.requests.RequestCryptogramRequest; +import com.checkout.networktokens.responses.CryptogramResponse; +import com.checkout.networktokens.responses.NetworkTokenResponse; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java index 62d39a2d..483497b7 100644 --- a/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java +++ b/src/test/java/com/checkout/networkTokens/NetworkTokensTestIT.java @@ -3,23 +3,23 @@ import com.checkout.EmptyResponse; import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; -import com.checkout.networkTokens.entities.DeletionReason; -import com.checkout.networkTokens.entities.InitiatedBy; -import com.checkout.networkTokens.entities.TransactionType; -import com.checkout.networkTokens.requests.DeleteNetworkTokenRequest; -import com.checkout.networkTokens.requests.ProvisionNetworkTokenRequest; -import com.checkout.networkTokens.requests.RequestCryptogramRequest; -import com.checkout.networkTokens.requests.sources.CardNetworkTokenSource; -import com.checkout.networkTokens.requests.sources.IdNetworkTokenSource; -import com.checkout.networkTokens.responses.CryptogramResponse; -import com.checkout.networkTokens.responses.NetworkTokenResponse; -import org.junit.jupiter.api.Disabled; +import com.checkout.networktokens.entities.CardNetworkTokenSource; +import com.checkout.networktokens.entities.DeletionReason; +import com.checkout.networktokens.entities.IdNetworkTokenSource; +import com.checkout.networktokens.entities.InitiatedBy; +import com.checkout.networktokens.entities.TransactionType; +import com.checkout.networktokens.requests.DeleteNetworkTokenRequest; +import com.checkout.networktokens.requests.ProvisionNetworkTokenRequest; +import com.checkout.networktokens.requests.RequestCryptogramRequest; +import com.checkout.networktokens.responses.CryptogramResponse; +import com.checkout.networktokens.responses.NetworkTokenResponse; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -@Disabled("Network token endpoints not yet available in test environment") +@Disabled("Network token scope not enable for our credentials in test environment") public class NetworkTokensTestIT extends SandboxTestFixture { public NetworkTokensTestIT() { From 9cd653404c1f7df4d40b1912d495f185e2e812a6 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 6 Mar 2026 13:20:00 +0100 Subject: [PATCH 04/19] New module issuing/disputes + unit and integration tests --- .../com/checkout/issuing/IssuingClient.java | 22 ++ .../checkout/issuing/IssuingClientImpl.java | 143 ++++++++++- .../disputes/entities/DisputeAmount.java | 28 +++ .../disputes/entities/DisputeArbitration.java | 39 +++ .../disputes/entities/DisputeChargeback.java | 45 ++++ .../disputes/entities/DisputeEvidence.java | 33 +++ .../entities/DisputeFileEvidence.java | 27 +++ .../disputes/entities/DisputeMerchant.java | 54 +++++ .../entities/DisputePreArbitration.java | 54 +++++ .../entities/DisputeReasonChange.java | 26 ++ .../entities/DisputeRepresentment.java | 35 +++ .../entities/IssuingDisputeStatus.java | 27 +++ .../entities/IssuingDisputeStatusReason.java | 63 +++++ .../requests/CreateDisputeRequest.java | 71 ++++++ .../requests/EscalateDisputeRequest.java | 48 ++++ .../requests/SubmitDisputeRequest.java | 41 ++++ .../disputes/responses/DisputeResponse.java | 95 ++++++++ .../issuing/IssuingClientImplTest.java | 203 +++++++++++++++- .../issuing/IssuingDisputesTestIT.java | 228 ++++++++++++++++++ 19 files changed, 1277 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeAmount.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeArbitration.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeChargeback.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeEvidence.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeFileEvidence.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeMerchant.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputePreArbitration.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeReasonChange.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/DisputeRepresentment.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatus.java create mode 100644 src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatusReason.java create mode 100644 src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java create mode 100644 src/main/java/com/checkout/issuing/disputes/requests/EscalateDisputeRequest.java create mode 100644 src/main/java/com/checkout/issuing/disputes/requests/SubmitDisputeRequest.java create mode 100644 src/main/java/com/checkout/issuing/disputes/responses/DisputeResponse.java create mode 100644 src/test/java/com/checkout/issuing/IssuingDisputesTestIT.java diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index 45901b64..f75d38f6 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -49,6 +49,10 @@ import com.checkout.issuing.testing.responses.CardAuthorizationIncrementingResponse; import com.checkout.issuing.testing.responses.CardAuthorizationResponse; import com.checkout.issuing.testing.responses.CardAuthorizationReversalResponse; +import com.checkout.issuing.disputes.requests.CreateDisputeRequest; +import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; +import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; +import com.checkout.issuing.disputes.responses.DisputeResponse; import com.checkout.payments.VoidResponse; import java.util.concurrent.CompletableFuture; @@ -146,6 +150,15 @@ CompletableFuture simulateReversal( CompletableFuture scheduleCardRevocation(final String cardId, final ScheduleRevocationRequest scheduleRevocationRequest); CompletableFuture deleteScheduledRevocation(final String cardId); + CompletableFuture createDispute(final CreateDisputeRequest createDisputeRequest, String idempotencyKey); + + CompletableFuture getDispute(final String disputeId); + + CompletableFuture cancelDispute(final String disputeId, String idempotencyKey); + + CompletableFuture escalateDispute(final String disputeId, String idempotencyKey, final EscalateDisputeRequest escalateDisputeRequest); + + CompletableFuture submitDispute(final String disputeId, String idempotencyKey, final SubmitDisputeRequest submitDisputeRequest); // Synchronous methods CardholderAccessTokenResponse requestCardholderAccessTokenSync(CardholderAccessTokenRequest cardholderAccessTokenRequest); @@ -239,4 +252,13 @@ CardAuthorizationReversalResponse simulateReversalSync( VoidResponse scheduleCardRevocationSync(String cardId, ScheduleRevocationRequest scheduleRevocationRequest); VoidResponse deleteScheduledRevocationSync(String cardId); + DisputeResponse createDisputeSync(CreateDisputeRequest createDisputeRequest, String idempotencyKey); + + DisputeResponse getDisputeSync(String disputeId); + + VoidResponse cancelDisputeSync(String disputeId, String idempotencyKey); + + VoidResponse escalateDisputeSync(String disputeId, String idempotencyKey, EscalateDisputeRequest escalateDisputeRequest); + + DisputeResponse submitDisputeSync(String disputeId, String idempotencyKey, SubmitDisputeRequest submitDisputeRequest); } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/IssuingClientImpl.java b/src/main/java/com/checkout/issuing/IssuingClientImpl.java index 8df74e07..601b12bf 100644 --- a/src/main/java/com/checkout/issuing/IssuingClientImpl.java +++ b/src/main/java/com/checkout/issuing/IssuingClientImpl.java @@ -54,10 +54,12 @@ import com.checkout.issuing.testing.responses.CardAuthorizationIncrementingResponse; import com.checkout.issuing.testing.responses.CardAuthorizationResponse; import com.checkout.issuing.testing.responses.CardAuthorizationReversalResponse; +import com.checkout.issuing.disputes.requests.CreateDisputeRequest; +import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; +import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; +import com.checkout.issuing.disputes.responses.DisputeResponse; import com.checkout.payments.VoidResponse; -import io.vavr.concurrent.Task; - import java.io.UnsupportedEncodingException; import java.util.concurrent.CompletableFuture; @@ -104,6 +106,13 @@ public class IssuingClientImpl extends AbstractClient implements IssuingClient { private static final String SCHEDULE_REVOCATION_PATH = "schedule-revocation"; private static final String ACCESS_TOKEN_PATH = "access/connect/token"; + private static final String DISPUTES_PATH = "disputes"; + + private static final String CANCEL_PATH = "cancel"; + + private static final String ESCALATE_PATH = "escalate"; + + private static final String SUBMIT_PATH = "submit"; public IssuingClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); @@ -559,7 +568,7 @@ public CompletableFuture renewCard(final String cardId, final RenewCardResponse.class, renewCardRequest, null - ); + ); } @Override @@ -584,6 +593,62 @@ public CompletableFuture deleteScheduledRevocation(final String ca ); } + public CompletableFuture createDispute(final CreateDisputeRequest createDisputeRequest, String idempotencyKey) { + validateCreateDisputeRequest(createDisputeRequest); + return apiClient.postAsync( + buildPath(ISSUING_PATH, DISPUTES_PATH), + sdkAuthorization(), + DisputeResponse.class, + createDisputeRequest, + idempotencyKey + ); + } + + public CompletableFuture getDispute(final String disputeId) { + validateDisputeId(disputeId); + return apiClient.getAsync( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId), + sdkAuthorization(), + DisputeResponse.class + ); + } + + public CompletableFuture cancelDispute(final String disputeId, String idempotencyKey) { + validateDisputeId(disputeId); + return apiClient.postAsync( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, CANCEL_PATH), + sdkAuthorization(), + VoidResponse.class, + null, + idempotencyKey + ); + } + + public CompletableFuture escalateDispute(final String disputeId, String idempotencyKey, + final EscalateDisputeRequest escalateDisputeRequest) { + validateDisputeIdAndEscalateRequest(disputeId, escalateDisputeRequest); + return apiClient.postAsync( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, ESCALATE_PATH), + sdkAuthorization(), + VoidResponse.class, + escalateDisputeRequest, + idempotencyKey + ); + } + + @Override + public CompletableFuture submitDispute(final String disputeId, String idempotencyKey, + final SubmitDisputeRequest submitDisputeRequest) { + validateDisputeIdAndSubmitRequest(disputeId, submitDisputeRequest); + return apiClient.postAsync( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, SUBMIT_PATH), + sdkAuthorization(), + DisputeResponse.class, + submitDisputeRequest, + idempotencyKey + ); + } + // Synchronous methods @Override public CardholderAccessTokenResponse requestCardholderAccessTokenSync(final CardholderAccessTokenRequest cardholderAccessTokenRequest) { @@ -1058,6 +1123,62 @@ public VoidResponse deleteScheduledRevocationSync(final String cardId) { ); } + public DisputeResponse createDisputeSync(final CreateDisputeRequest createDisputeRequest, String idempotencyKey) { + validateCreateDisputeRequest(createDisputeRequest); + return apiClient.post( + buildPath(ISSUING_PATH, DISPUTES_PATH), + sdkAuthorization(), + DisputeResponse.class, + createDisputeRequest, + idempotencyKey + ); + } + + public DisputeResponse getDisputeSync(final String disputeId) { + validateDisputeId(disputeId); + return apiClient.get( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId), + sdkAuthorization(), + DisputeResponse.class + ); + } + + public VoidResponse cancelDisputeSync(final String disputeId, String idempotencyKey) { + validateDisputeId(disputeId); + return apiClient.post( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, CANCEL_PATH), + sdkAuthorization(), + VoidResponse.class, + null, + idempotencyKey + ); + } + + public VoidResponse escalateDisputeSync(final String disputeId, String idempotencyKey, + final EscalateDisputeRequest escalateDisputeRequest) { + validateDisputeIdAndEscalateRequest(disputeId, escalateDisputeRequest); + return apiClient.post( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, ESCALATE_PATH), + sdkAuthorization(), + VoidResponse.class, + escalateDisputeRequest, + idempotencyKey + ); + } + + @Override + public DisputeResponse submitDisputeSync(final String disputeId, String idempotencyKey, + final SubmitDisputeRequest submitDisputeRequest) { + validateDisputeIdAndSubmitRequest(disputeId, submitDisputeRequest); + return apiClient.post( + buildPath(ISSUING_PATH, DISPUTES_PATH, disputeId, SUBMIT_PATH), + sdkAuthorization(), + DisputeResponse.class, + submitDisputeRequest, + idempotencyKey + ); + } + // Common methods private UrlEncodedFormEntity createFormUrlEncodedContent(final CardholderAccessTokenRequest cardholderAccessTokenRequest) { try { @@ -1138,4 +1259,20 @@ private void validateAuthorizationIdAndRefundRequest(final String authorizationI private void validateAuthorizationIdAndReversalRequest(final String authorizationId, final CardAuthorizationReversalRequest cardAuthorizationReversalRequest) { validateParams("authorizationId", authorizationId, "cardAuthorizationReversalRequest", cardAuthorizationReversalRequest); } + + private void validateCreateDisputeRequest(final CreateDisputeRequest createDisputeRequest) { + validateParams("createDisputeRequest", createDisputeRequest); + } + + private void validateDisputeId(final String disputeId) { + validateParams("disputeId", disputeId); + } + + private void validateDisputeIdAndEscalateRequest(final String disputeId, final EscalateDisputeRequest escalateDisputeRequest) { + validateParams("disputeId", disputeId, "escalateDisputeRequest", escalateDisputeRequest); + } + + private void validateDisputeIdAndSubmitRequest(final String disputeId, final SubmitDisputeRequest submitDisputeRequest) { + validateParams("disputeId", disputeId, "submitDisputeRequest", submitDisputeRequest); + } } diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeAmount.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeAmount.java new file mode 100644 index 00000000..733a6483 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeAmount.java @@ -0,0 +1,28 @@ +package com.checkout.issuing.disputes.entities; + +import com.checkout.common.Currency; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Amount details in dispute responses + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeAmount { + + /** + * The amount is provided in the minor currency unit + */ + private Long amount; + + /** + * The issuing currency, as a three-letter ISO currency code + */ + private Currency currency; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeArbitration.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeArbitration.java new file mode 100644 index 00000000..241a0882 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeArbitration.java @@ -0,0 +1,39 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Arbitration details in dispute response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeArbitration { + + /** + * The date and time when the arbitration was successfully escalated to the card scheme, in UTC + */ + private Instant submittedOn; + + /** + * The disputed amount at the arbitration stage, in the minor currency unit + */ + private DisputeAmount amount; + + /** + * Your justification for escalating the dispute to arbitration + */ + private String justification; + + /** + * The date and time when the card scheme decided the arbitration case, in UTC + */ + private Instant decidedOn; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeChargeback.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeChargeback.java new file mode 100644 index 00000000..a0a54df9 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeChargeback.java @@ -0,0 +1,45 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +/** + * Chargeback details in dispute response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeChargeback { + + /** + * The date and time when the chargeback was successfully submitted to the card scheme, in UTC + */ + private Instant submittedOn; + + /** + * The four-digit scheme-specific reason code you provide for the chargeback + */ + private String reason; + + /** + * The disputed amount, in the minor unit of the transaction currency + */ + private DisputeAmount amount; + + /** + * Your evidence for the chargeback + */ + private List evidence; + + /** + * Your justification for the chargeback + */ + private String justification; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeEvidence.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeEvidence.java new file mode 100644 index 00000000..46d16f0a --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeEvidence.java @@ -0,0 +1,33 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Evidence for creating or updating disputes + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeEvidence { + + /** + * The complete file name, including the extension + */ + private String name; + + /** + * The base64-encoded string that represents a single JPG, PDF, TIFF, or ZIP file. + * ZIP files can contain multiple JPG, PDF, or TIFF files. + */ + private String content; + + /** + * A brief description of the evidence + */ + private String description; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeFileEvidence.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeFileEvidence.java new file mode 100644 index 00000000..7cf9512d --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeFileEvidence.java @@ -0,0 +1,27 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Evidence as returned from dispute responses + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeFileEvidence { + + /** + * The unique identifier for an uploaded file + */ + private String fileId; + + /** + * A brief description of the evidence + */ + private String description; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeMerchant.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeMerchant.java new file mode 100644 index 00000000..08fdced6 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeMerchant.java @@ -0,0 +1,54 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Merchant details in dispute response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeMerchant { + + /** + * The merchant's identifier. This can vary from one acquirer to another. + */ + private String id; + + /** + * The merchant's name + */ + private String name; + + /** + * The city where the merchant is located + */ + private String city; + + /** + * The state where the merchant is located (US only) + */ + private String state; + + /** + * The two-digit country code where the merchant is located + */ + private String countryCode; + + /** + * The merchant's category code for the disputed transaction + */ + private String categoryCode; + + /** + * Any evidence submitted by the merchant during the dispute lifecycle + */ + private List evidence; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputePreArbitration.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputePreArbitration.java new file mode 100644 index 00000000..7a1392ca --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputePreArbitration.java @@ -0,0 +1,54 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +/** + * Pre-arbitration details in dispute response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputePreArbitration { + + /** + * The date and time when the pre-arbitration was successfully escalated to the card scheme, in UTC + */ + private Instant submittedOn; + + /** + * The evidence relating to the Issuing dispute provided at the pre-arbitration stage + */ + private List evidence; + + /** + * The disputed amount at the pre-arbitration stage, in the minor currency unit + */ + private DisputeAmount amount; + + /** + * The change to the dispute reason and your justification for changing it + */ + private DisputeReasonChange reasonChange; + + /** + * Your justification for escalating the dispute to pre-arbitration + */ + private String justification; + + /** + * The date and time when the merchant provided evidence against the pre-arbitration, in UTC + */ + private Instant merchantRespondedOn; + + /** + * Evidence provided by the merchant against the pre-arbitration + */ + private List merchantEvidence; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeReasonChange.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeReasonChange.java new file mode 100644 index 00000000..0639e194 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeReasonChange.java @@ -0,0 +1,26 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Dispute reason change details + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeReasonChange { + + /** + * The updated four-digit scheme-specific reason code for the chargeback + */ + private String reason; + + /** + * Your justification for changing the dispute reason + */ + private String justification; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/DisputeRepresentment.java b/src/main/java/com/checkout/issuing/disputes/entities/DisputeRepresentment.java new file mode 100644 index 00000000..126ebce8 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/DisputeRepresentment.java @@ -0,0 +1,35 @@ +package com.checkout.issuing.disputes.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +/** + * Representment details in dispute response + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DisputeRepresentment { + + /** + * The date and time when you received the representment, in UTC + */ + private Instant receivedOn; + + /** + * The representment amount, in the minor currency unit + */ + private DisputeAmount amount; + + /** + * The evidence provided by the merchant against the chargeback + */ + private List evidence; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatus.java b/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatus.java new file mode 100644 index 00000000..1f132ec2 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatus.java @@ -0,0 +1,27 @@ +package com.checkout.issuing.disputes.entities; + +import com.google.gson.annotations.SerializedName; + +/** + * Dispute status enumeration + */ +public enum IssuingDisputeStatus { + + @SerializedName("created") + CREATED, + + @SerializedName("canceled") + CANCELED, + + @SerializedName("processing") + PROCESSING, + + @SerializedName("action_required") + ACTION_REQUIRED, + + @SerializedName("won") + WON, + + @SerializedName("lost") + LOST +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatusReason.java b/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatusReason.java new file mode 100644 index 00000000..83351aa8 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/entities/IssuingDisputeStatusReason.java @@ -0,0 +1,63 @@ +package com.checkout.issuing.disputes.entities; + +import com.google.gson.annotations.SerializedName; + +/** + * Dispute status reason enumeration + */ +public enum IssuingDisputeStatusReason { + + @SerializedName("expired") + EXPIRED, + + @SerializedName("chargeback_pending") + CHARGEBACK_PENDING, + + @SerializedName("chargeback_evidence_invalid_or_insufficient") + CHARGEBACK_EVIDENCE_INVALID_OR_INSUFFICIENT, + + @SerializedName("chargeback_processed") + CHARGEBACK_PROCESSED, + + @SerializedName("chargeback_rejected") + CHARGEBACK_REJECTED, + + @SerializedName("chargeback_reversal_pending") + CHARGEBACK_REVERSAL_PENDING, + + @SerializedName("chargeback_reversed") + CHARGEBACK_REVERSED, + + @SerializedName("chargeback_response_accepted") + CHARGEBACK_RESPONSE_ACCEPTED, + + @SerializedName("prearbitration_pending") + PREARBITRATION_PENDING, + + @SerializedName("prearbitration_evidence_invalid_or_insufficient") + PREARBITRATION_EVIDENCE_INVALID_OR_INSUFFICIENT, + + @SerializedName("prearbitration_processed") + PREARBITRATION_PROCESSED, + + @SerializedName("prearbitration_rejected") + PREARBITRATION_REJECTED, + + @SerializedName("prearbitration_reversal_pending") + PREARBITRATION_REVERSAL_PENDING, + + @SerializedName("prearbitration_reversed") + PREARBITRATION_REVERSED, + + @SerializedName("prearbitration_response_accepted") + PREARBITRATION_RESPONSE_ACCEPTED, + + @SerializedName("arbitration_pending") + ARBITRATION_PENDING, + + @SerializedName("arbitration_processed") + ARBITRATION_PROCESSED, + + @SerializedName("presentment_reversed") + PRESENTMENT_REVERSED +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java b/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java new file mode 100644 index 00000000..89263383 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/requests/CreateDisputeRequest.java @@ -0,0 +1,71 @@ +package com.checkout.issuing.disputes.requests; + +import com.checkout.common.Resource; +import com.checkout.issuing.disputes.entities.DisputeEvidence; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * Request to create an Issuing dispute + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class CreateDisputeRequest extends Resource { + + /** + * The transaction's unique identifier + */ + @NotBlank + private String transactionId; + + /** + * The four-digit scheme-specific reason code for the chargeback. + * Only provide this if Checkout.com is your issuing processor. + * Checkout.com does not validate this value. + */ + @NotBlank + private String reason; + + /** + * Your evidence for raising the chargeback, in line with the card scheme's requirements + */ + private List evidence; + + /** + * The chargeback amount, in the minor unit of the transaction currency. + * If not provided, Checkout.com uses the full amount of the presentment. + */ + private Long amount; + + /** + * The unique identifier for the disputed presentment message, if the transaction has multiple presentments. + * If the transaction has only one presentment, Checkout.com uses this automatically. + */ + private String presentmentMessageId; + + /** + * Indicates whether to submit the dispute: + * • Immediately – Set to true. + * • Later – Set to false. + * Default: false + */ + @Builder.Default + private Boolean isReadyForSubmission = false; + + /** + * Your justification for the chargeback + */ + @Size(max = 100) + private String justification; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/requests/EscalateDisputeRequest.java b/src/main/java/com/checkout/issuing/disputes/requests/EscalateDisputeRequest.java new file mode 100644 index 00000000..fb115540 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/requests/EscalateDisputeRequest.java @@ -0,0 +1,48 @@ +package com.checkout.issuing.disputes.requests; + +import com.checkout.common.Resource; +import com.checkout.issuing.disputes.entities.DisputeEvidence; +import com.checkout.issuing.disputes.entities.DisputeReasonChange; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + +/** + * Request to escalate an Issuing dispute to pre-arbitration or arbitration + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class EscalateDisputeRequest extends Resource { + + /** + * Justification for escalating the dispute + */ + @NotBlank + @Size(max = 13000) + private String justification; + + /** + * Your evidence for escalating the dispute, in line with the card scheme's requirements. + * If the request goes to arbitration, the card scheme ignores any evidence you provide at this stage using this request. + */ + private List additionalEvidence; + + /** + * The updated disputed amount, in the minor unit of the representment currency + */ + private Long amount; + + /** + * The change to the dispute reason and your justification for changing it + */ + private DisputeReasonChange reasonChange; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/requests/SubmitDisputeRequest.java b/src/main/java/com/checkout/issuing/disputes/requests/SubmitDisputeRequest.java new file mode 100644 index 00000000..a117f04a --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/requests/SubmitDisputeRequest.java @@ -0,0 +1,41 @@ +package com.checkout.issuing.disputes.requests; + +import com.checkout.common.Resource; +import com.checkout.issuing.disputes.entities.DisputeEvidence; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Request to submit an Issuing dispute to the card scheme for processing + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SubmitDisputeRequest extends Resource { + + /** + * The updated four-digit scheme-specific reason code. + * If not provided, Checkout.com uses the existing reason code. + */ + private String reason; + + /** + * Your evidence for the chargeback, if updated since you created the dispute + */ + private List evidence; + + /** + * The updated disputed amount, in the minor unit of the transaction or representment currency. + * If not provided, Checkout.com uses the existing disputed amount. + */ + private Long amount; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/disputes/responses/DisputeResponse.java b/src/main/java/com/checkout/issuing/disputes/responses/DisputeResponse.java new file mode 100644 index 00000000..38575c59 --- /dev/null +++ b/src/main/java/com/checkout/issuing/disputes/responses/DisputeResponse.java @@ -0,0 +1,95 @@ +package com.checkout.issuing.disputes.responses; + +import com.checkout.common.Resource; +import com.checkout.issuing.disputes.entities.DisputeArbitration; +import com.checkout.issuing.disputes.entities.DisputeChargeback; +import com.checkout.issuing.disputes.entities.DisputeAmount; +import com.checkout.issuing.disputes.entities.IssuingDisputeStatus; +import com.checkout.issuing.disputes.entities.IssuingDisputeStatusReason; +import com.checkout.issuing.disputes.entities.DisputeMerchant; +import com.checkout.issuing.disputes.entities.DisputePreArbitration; +import com.checkout.issuing.disputes.entities.DisputeRepresentment; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; + +/** + * Issuing dispute response + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DisputeResponse extends Resource { + + /** + * The unique identifier for the Issuing dispute + */ + private String id; + + /** + * The four-digit scheme-specific reason code you provide for the chargeback + */ + private String reason; + + /** + * The disputed amount, in the minor unit of the transaction currency + */ + private DisputeAmount disputedAmount; + + /** + * The dispute status + */ + private IssuingDisputeStatus status; + + /** + * The status reason, which provides more information about the dispute status + */ + private IssuingDisputeStatusReason statusReason; + + /** + * The unique Checkout.com identifier for the transaction + */ + private String transactionId; + + /** + * The unique identifier for the disputed presentment message + */ + private String presentmentMessageId; + + /** + * The details of the merchant you raised the dispute with + */ + private DisputeMerchant merchant; + + /** + * The date and time when the dispute was created, in UTC + */ + private Instant createdOn; + + /** + * The date and time when the dispute was last modified, in UTC + */ + private Instant modifiedOn; + + /** + * The dispute details at the chargeback stage + */ + private DisputeChargeback chargeback; + + /** + * The information provided by the merchant when they reject the chargeback and send a representment + */ + private DisputeRepresentment representment; + + /** + * The dispute details at the pre-arbitration stage + */ + private DisputePreArbitration preArbitration; + + /** + * The dispute details during the arbitration stage + */ + private DisputeArbitration arbitration; + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java index 22f73cf3..5efec875 100644 --- a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java +++ b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java @@ -46,6 +46,10 @@ import com.checkout.issuing.controls.requests.controlprofile.UpdateControlProfileRequest; import com.checkout.issuing.controls.responses.controlprofile.ControlProfileResponse; import com.checkout.issuing.controls.responses.controlprofile.ControlProfilesQueryResponse; +import com.checkout.issuing.disputes.requests.CreateDisputeRequest; +import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; +import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; +import com.checkout.issuing.disputes.responses.DisputeResponse; import com.checkout.issuing.testing.requests.CardAuthorizationClearingRequest; import com.checkout.issuing.testing.requests.CardAuthorizationIncrementingRequest; import com.checkout.issuing.testing.requests.CardAuthorizationRefundsRequest; @@ -766,7 +770,6 @@ void shouldAddTargetToControlProfile() throws ExecutionException, InterruptedExc )).thenReturn(CompletableFuture.completedFuture(response)); final CompletableFuture future = client.addTargetToControlProfile("profile_id", "target_id"); - validateVoidResponse(response, future.get()); } @@ -788,6 +791,94 @@ void shouldRemoveTargetFromControlProfile() throws ExecutionException, Interrupt } } + @DisplayName("Disputes") + class Disputes { + @Test + void shouldCreateDispute() throws ExecutionException, InterruptedException { + final CreateDisputeRequest request = createCreateDisputeRequest(); + final DisputeResponse response = createDisputeResponse(); + + when(apiClient.postAsync( + "issuing/disputes", + authorization, + DisputeResponse.class, + request, + "idempotencyKey" + )).thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.createDispute(request, "idempotencyKey"); + + validateDisputeResponse(response, future.get()); + } + + @Test + void shouldGetDispute() throws ExecutionException, InterruptedException { + final DisputeResponse response = createDisputeResponse(); + + when(apiClient.getAsync( + "issuing/disputes/dispute_id", + authorization, + DisputeResponse.class)) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.getDispute("dispute_id"); + + validateDisputeResponse(response, future.get()); + } + + @Test + void shouldCancelDispute() throws ExecutionException, InterruptedException { + final VoidResponse response = createVoidResponse(); + + when(apiClient.postAsync( + "issuing/disputes/dispute_id/cancel", + authorization, + VoidResponse.class, + null, + "idempotencyKey" + )).thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.cancelDispute("dispute_id", "idempotencyKey"); + + validateVoidResponse(response, future.get()); + } + + void shouldEscalateDispute() throws ExecutionException, InterruptedException { + final EscalateDisputeRequest request = createEscalateDisputeRequest(); + final VoidResponse response = createVoidResponse(); + + when(apiClient.postAsync( + "issuing/disputes/dispute_id/escalate", + authorization, + VoidResponse.class, + request, + "idempotencyKey" + )).thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.escalateDispute("dispute_id", "idempotencyKey", request); + + validateVoidResponse(response, future.get()); + } + + @Test + void shouldSubmitDispute() throws ExecutionException, InterruptedException { + final SubmitDisputeRequest request = createSubmitDisputeRequest(); + final DisputeResponse response = createDisputeResponse(); + + when(apiClient.postAsync( + "issuing/disputes/dispute_id/submit", + authorization, + DisputeResponse.class, + request, + "idempotencyKey" + )).thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.submitDispute("dispute_id", "idempotencyKey", request); + + validateDisputeResponse(response, future.get()); + } + } + // Synchronous methods @Nested @DisplayName("Cardholders Sync") @@ -1454,7 +1545,6 @@ void shouldAddTargetToControlProfileSync() { )).thenReturn(expectedResponse); final VoidResponse actualResponse = client.addTargetToControlProfileSync("profile_id", "target_id"); - validateVoidResponse(expectedResponse, actualResponse); } @@ -1476,6 +1566,94 @@ void shouldRemoveTargetFromControlProfileSync() { } } + @DisplayName("Disputes Sync") + class DisputesSync { + @Test + void shouldCreateDisputeSync() { + final CreateDisputeRequest request = createCreateDisputeRequest(); + final DisputeResponse expectedResponse = createDisputeResponse(); + + when(apiClient.post( + "issuing/disputes", + authorization, + DisputeResponse.class, + request, + "idempotencyKey" + )).thenReturn(expectedResponse); + + final DisputeResponse actualResponse = client.createDisputeSync(request, "idempotencyKey"); + + validateDisputeResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetDisputeSync() { + final DisputeResponse expectedResponse = createDisputeResponse(); + + when(apiClient.get( + "issuing/disputes/dispute_id", + authorization, + DisputeResponse.class)) + .thenReturn(expectedResponse); + + final DisputeResponse actualResponse = client.getDisputeSync("dispute_id"); + + validateDisputeResponse(expectedResponse, actualResponse); + } + + @Test + void shouldCancelDisputeSync() { + final VoidResponse expectedResponse = createVoidResponse(); + + when(apiClient.post( + "issuing/disputes/dispute_id/cancel", + authorization, + VoidResponse.class, + null, + "idempotencyKey" + )).thenReturn(expectedResponse); + + final VoidResponse actualResponse = client.cancelDisputeSync("dispute_id", "idempotencyKey"); + + validateVoidResponse(expectedResponse, actualResponse); + } + + void shouldEscalateDisputeSync() { + final EscalateDisputeRequest request = createEscalateDisputeRequest(); + final VoidResponse expectedResponse = createVoidResponse(); + + when(apiClient.post( + "issuing/disputes/dispute_id/escalate", + authorization, + VoidResponse.class, + request, + "idempotencyKey" + )).thenReturn(expectedResponse); + + final VoidResponse actualResponse = client.escalateDisputeSync("dispute_id", "idempotencyKey", request); + + validateVoidResponse(expectedResponse, actualResponse); + } + + @Test + void shouldSubmitDisputeSync() { + final SubmitDisputeRequest request = createSubmitDisputeRequest(); + final DisputeResponse expectedResponse = createDisputeResponse(); + + when(apiClient.post( + "issuing/disputes/dispute_id/submit", + authorization, + DisputeResponse.class, + request, + "idempotencyKey" + )).thenReturn(expectedResponse); + + final DisputeResponse actualResponse = client.submitDisputeSync("dispute_id", "idempotencyKey", request); + + validateDisputeResponse(expectedResponse, actualResponse); + } + } + // Common methods private CardholderRequest createCardholderRequest() { return mock(CardholderRequest.class); @@ -1641,6 +1819,22 @@ private CardAuthorizationReversalResponse createCardAuthorizationReversalRespons return mock(CardAuthorizationReversalResponse.class); } + private CreateDisputeRequest createCreateDisputeRequest() { + return mock(CreateDisputeRequest.class); + } + + private DisputeResponse createDisputeResponse() { + return mock(DisputeResponse.class); + } + + private EscalateDisputeRequest createEscalateDisputeRequest() { + return mock(EscalateDisputeRequest.class); + } + + private SubmitDisputeRequest createSubmitDisputeRequest() { + return mock(SubmitDisputeRequest.class); + } + private void validateCardholderResponse(CardholderResponse expected, CardholderResponse actual) { assertNotNull(actual); assertEquals(expected, actual); @@ -1803,4 +1997,9 @@ private void validateControlProfilesQueryResponse(ControlProfilesQueryResponse e assertNotNull(actual); assertEquals(expected, actual); } + + private void validateDisputeResponse(DisputeResponse expected, DisputeResponse actual) { + assertNotNull(actual); + assertEquals(expected, actual); + } } diff --git a/src/test/java/com/checkout/issuing/IssuingDisputesTestIT.java b/src/test/java/com/checkout/issuing/IssuingDisputesTestIT.java new file mode 100644 index 00000000..460f5cf1 --- /dev/null +++ b/src/test/java/com/checkout/issuing/IssuingDisputesTestIT.java @@ -0,0 +1,228 @@ +package com.checkout.issuing; + +import com.checkout.payments.VoidResponse; +import com.checkout.issuing.cardholders.CardholderResponse; +import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.disputes.entities.DisputeEvidence; +import com.checkout.issuing.disputes.entities.DisputeFileEvidence; +import com.checkout.issuing.disputes.entities.IssuingDisputeStatus; +import com.checkout.issuing.disputes.requests.CreateDisputeRequest; +import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; +import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; +import com.checkout.issuing.disputes.responses.DisputeResponse; +import com.checkout.issuing.testing.requests.CardAuthorizationRequest; +import com.checkout.issuing.testing.requests.CardSimulation; +import com.checkout.issuing.testing.requests.TransactionMerchant; +import com.checkout.issuing.testing.requests.TransactionSimulation; +import com.checkout.issuing.testing.requests.TransactionType; +import com.checkout.issuing.testing.responses.CardAuthorizationResponse; +import com.checkout.common.Currency; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Arrays; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Disabled("Avoid creating disputes all the time") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class IssuingDisputesTestIT extends BaseIssuingTestIT { + + private CardResponse card; + private CardAuthorizationResponse transaction; + private DisputeResponse dispute; + + @BeforeAll + void setUp() { + final CardholderResponse cardholder = createCardholder(); + card = createCard(cardholder.getId(), true); + transaction = createTransaction(); + dispute = createInitialDispute(); + } + + @Test + void shouldCreateDispute() { + validateDisputeResponse(dispute); + } + + @Test + void shouldGetDispute() { + final DisputeResponse response = blocking(() -> + issuingApi.issuingClient().getDispute(dispute.getId())); + + validateDisputeResponse(response); + } + + @Test + void shouldCancelDispute() { + final DisputeResponse newDispute = createInitialDispute(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final VoidResponse response = blocking(() -> + issuingApi.issuingClient().cancelDispute(newDispute.getId(), idempotencyKey)); + + validateVoidResponse(response); + } + + @Test + void shouldEscalateDispute() { + final DisputeResponse newDispute = createInitialDispute(); + final EscalateDisputeRequest request = createEscalateDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final VoidResponse response = blocking(() -> + issuingApi.issuingClient().escalateDispute(newDispute.getId(), idempotencyKey, request)); + + validateVoidResponse(response); + } + + @Test + void shouldSubmitDispute() { + final DisputeResponse newDispute = createInitialDispute(); + final SubmitDisputeRequest request = createSubmitDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final DisputeResponse response = blocking(() -> + issuingApi.issuingClient().submitDispute(newDispute.getId(), idempotencyKey, request)); + + validateDisputeResponse(response); + } + + // Synchronous methods + @Test + void shouldCreateDisputeSync() { + final CreateDisputeRequest request = createCreateDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final DisputeResponse response = + issuingApi.issuingClient().createDisputeSync(request, idempotencyKey); + + validateDisputeResponse(response); + } + + @Test + void shouldGetDisputeSync() { + final DisputeResponse response = + issuingApi.issuingClient().getDisputeSync(dispute.getId()); + + validateDisputeResponse(response); + } + + @Test + void shouldCancelDisputeSync() { + final DisputeResponse newDispute = createInitialDispute(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final VoidResponse response = + issuingApi.issuingClient().cancelDisputeSync(newDispute.getId(), idempotencyKey); + + validateVoidResponse(response); + } + + @Test + void shouldEscalateDisputeSync() { + final DisputeResponse newDispute = createInitialDispute(); + final EscalateDisputeRequest request = createEscalateDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final VoidResponse response = + issuingApi.issuingClient().escalateDisputeSync(newDispute.getId(), idempotencyKey, request); + + validateVoidResponse(response); + } + + @Test + void shouldSubmitDisputeSync() { + final DisputeResponse newDispute = createInitialDispute(); + final SubmitDisputeRequest request = createSubmitDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + final DisputeResponse response = + issuingApi.issuingClient().submitDisputeSync(newDispute.getId(), idempotencyKey, request); + + validateDisputeResponse(response); + } + + // Common methods + private CardAuthorizationResponse createTransaction() { + final CardAuthorizationRequest authorizationRequest = CardAuthorizationRequest.builder() + .card(CardSimulation.builder() + .id(card.getId()) + .expiryMonth(card.getExpiryMonth()) + .expiryYear(card.getExpiryYear()) + .build()) + .transaction(TransactionSimulation.builder() + .type(TransactionType.PURCHASE) + .amount(10000) + .currency(Currency.USD) + .merchant(TransactionMerchant.builder() + .categoryCode("5411") + .build()) + .build()) + .build(); + + return blocking(() -> issuingApi.issuingClient().simulateAuthorization(authorizationRequest)); + } + + private DisputeResponse createInitialDispute() { + final CreateDisputeRequest request = createCreateDisputeRequest(); + final String idempotencyKey = UUID.randomUUID().toString(); + + return blocking(() -> issuingApi.issuingClient().createDispute(request, idempotencyKey)); + } + + private CreateDisputeRequest createCreateDisputeRequest() { + return CreateDisputeRequest.builder() + .transactionId(transaction.getId()) + .reason("fraudulent") + .evidence(Arrays.asList( + DisputeEvidence.builder() + .name("evidence.pdf") + .content("base64encodedcontent") + .description("Evidence showing fraudulent transaction") + .build() + )) + .build(); + } + + private EscalateDisputeRequest createEscalateDisputeRequest() { + return EscalateDisputeRequest.builder() + .justification("Additional evidence supports our case") + .additionalEvidence(Arrays.asList( + DisputeEvidence.builder() + .name("additional_evidence.pdf") + .content("base64additionalcontent") + .description("Additional evidence for escalation") + .build() + )) + .build(); + } + + private SubmitDisputeRequest createSubmitDisputeRequest() { + return SubmitDisputeRequest.builder() + .evidence(Arrays.asList( + DisputeEvidence.builder() + .name("final_evidence.pdf") + .content("base64finalcontent") + .description("Final evidence for submission") + .build() + )) + .build(); + } + + private void validateDisputeResponse(DisputeResponse response) { + assertNotNull(response); + assertNotNull(response.getId()); + assertEquals("fraudulent", response.getReason()); + assertEquals(IssuingDisputeStatus.CREATED, response.getStatus()); + } + + private void validateVoidResponse(VoidResponse response) { + assertNotNull(response); + assertEquals(202, response.getHttpStatusCode()); + } +} \ No newline at end of file From afbf787689f8a6ad6da329c18efbb4c99763d581 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 9 Mar 2026 16:15:44 +0100 Subject: [PATCH 05/19] - Transport headers modification - Extended module AccountClient with reserve rule endpoints - urlencoded form and other request bodies support - Etag management (if-match) - UpdatePaymentInstrumentRequest header management change --- .../checkout/ApacheHttpClientTransport.java | 49 +++- src/main/java/com/checkout/HttpMetadata.java | 4 + .../com/checkout/accounts/AccountsClient.java | 30 ++ .../checkout/accounts/AccountsClientImpl.java | 137 +++++++++ .../accounts/EntityMemberResponse.java | 16 ++ .../accounts/EntityMembersResponse.java | 17 ++ .../java/com/checkout/accounts/Headers.java | 9 +- .../UpdatePaymentInstrumentRequest.java | 11 +- .../entities/HoldingDuration.java | 16 ++ .../entities/RollingReservePolicy.java | 19 ++ .../responses/ReserveRuleCreateResponse.java | 16 ++ .../responses/ReserveRuleRequest.java | 28 ++ .../responses/ReserveRuleResponse.java | 24 ++ .../responses/ReserveRulesResponse.java | 17 ++ .../com/checkout/common/CheckoutUtils.java | 13 + .../accounts/AccountsClientImplTest.java | 263 ++++++++++++++++++ .../com/checkout/accounts/AccountsTestIT.java | 234 ++++++++++++++++ 17 files changed, 885 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/checkout/accounts/EntityMemberResponse.java create mode 100644 src/main/java/com/checkout/accounts/EntityMembersResponse.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/entities/HoldingDuration.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleCreateResponse.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java create mode 100644 src/main/java/com/checkout/accounts/reserverules/responses/ReserveRulesResponse.java diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index 31318827..774e01e9 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -1,6 +1,7 @@ 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; @@ -33,6 +34,7 @@ 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; @@ -42,6 +44,7 @@ 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; @@ -52,7 +55,6 @@ import static com.checkout.common.CheckoutUtils.getVersionFromManifest; import static org.apache.http.HttpHeaders.ACCEPT; import static org.apache.http.HttpHeaders.AUTHORIZATION; -import static org.apache.http.HttpHeaders.CONTENT_TYPE; import static org.apache.http.HttpHeaders.USER_AGENT; @Slf4j @@ -277,7 +279,7 @@ private HttpEntity getMultipartFileEntity(final AbstractFileRequest abstractFile } private Response performCall(final SdkAuthorization authorization, - final Object requestBodyOrEntity, + final Object requestBody, final HttpUriRequest request, final ClientOperation clientOperation) { log.info("{}: {}", clientOperation, request.getURI()); @@ -285,6 +287,9 @@ private Response performCall(final SdkAuthorization authorization, request.setHeader(ACCEPT, getAcceptHeader(clientOperation)); request.setHeader(AUTHORIZATION, authorization.getAuthorizationHeader()); + // Check and add headers from the request object if needed + addHeadersFromRequestBody(requestBody, request); + String currentRequestId = UUID.randomUUID().toString(); addTelemetryHeader(request, currentRequestId); @@ -292,16 +297,23 @@ private Response performCall(final SdkAuthorization authorization, long startTime = System.currentTimeMillis(); log.info("Request: " + Arrays.toString(sanitiseHeaders(request.getAllHeaders()))); - if (requestBodyOrEntity != null && request instanceof HttpEntityEnclosingRequest) { - if (requestBodyOrEntity instanceof UrlEncodedFormEntity) { - // Handle form-encoded content - request.setHeader(CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); - ((HttpEntityEnclosingRequestBase) request).setEntity((UrlEncodedFormEntity) requestBodyOrEntity); - } else if (requestBodyOrEntity instanceof String) { - // Handle JSON content - ((HttpEntityEnclosingRequestBase) request).setEntity(new StringEntity((String) requestBodyOrEntity, ContentType.APPLICATION_JSON)); + if (requestBody != null && request instanceof HttpEntityEnclosingRequest) { + HttpEntity httpEntity = null; + + if (requestBody instanceof HttpEntity) { + httpEntity = (HttpEntity) requestBody; + } else if (requestBody instanceof MultipartEntityBuilder) { + httpEntity = ((MultipartEntityBuilder) requestBody).build(); + } else if (requestBody instanceof UrlEncodedFormEntity) { + httpEntity = (UrlEncodedFormEntity) requestBody; + } else { + String json = serializer.toJson(requestBody); + httpEntity = new StringEntity(json, ContentType.APPLICATION_JSON); } + + ((HttpEntityEnclosingRequestBase) request).setEntity(httpEntity); } + try (final CloseableHttpResponse response = httpClient.execute(request)) { long elapsed = System.currentTimeMillis() - startTime; log.info("Response: " + response.getStatusLine().getStatusCode() + " " + Arrays.toString(response.getAllHeaders())); @@ -327,6 +339,23 @@ 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) {} + } + } + } + private void addTelemetryHeader(HttpUriRequest request, String currentRequestId) { if (configuration.isTelemetryEnabled()) { String telemetryHeader = generateTelemetryHeader(currentRequestId); diff --git a/src/main/java/com/checkout/HttpMetadata.java b/src/main/java/com/checkout/HttpMetadata.java index 762902af..ce707212 100644 --- a/src/main/java/com/checkout/HttpMetadata.java +++ b/src/main/java/com/checkout/HttpMetadata.java @@ -18,4 +18,8 @@ public String getRequestId() { return CheckoutUtils.getRequestId(responseHeaders); } + public String getEtag() { + return CheckoutUtils.getEtag(responseHeaders); + } + } diff --git a/src/main/java/com/checkout/accounts/AccountsClient.java b/src/main/java/com/checkout/accounts/AccountsClient.java index 203682e3..36dfa68b 100644 --- a/src/main/java/com/checkout/accounts/AccountsClient.java +++ b/src/main/java/com/checkout/accounts/AccountsClient.java @@ -6,6 +6,10 @@ import com.checkout.accounts.payout.schedule.request.UpdateScheduleRequest; import com.checkout.accounts.payout.schedule.response.GetScheduleResponse; import com.checkout.accounts.payout.schedule.response.VoidResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleCreateResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; +import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; +import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -39,6 +43,19 @@ CompletableFuture updatePaymentInstrumentDetails(String entityId, CompletableFuture updatePayoutSchedule(String entityId, Currency currency, UpdateScheduleRequest updateScheduleRequest); + CompletableFuture getEntityMembers(String entityId); + + CompletableFuture reinviteEntityMember(String entityId, String userId); + + CompletableFuture createReserveRule(String entityId, ReserveRuleRequest reserveRuleRequest); + + CompletableFuture updateReserveRule(String entityId, String reserveRuleId, + ReserveRuleRequest reserveRuleRequest); + + CompletableFuture getReserveRule(String entityId, String reserveRuleId); + + CompletableFuture getReserveRules(String entityId); + // Synchronous methods IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest); @@ -62,4 +79,17 @@ IdResponse updatePaymentInstrumentDetailsSync(final String entityId, GetScheduleResponse retrievePayoutScheduleSync(final String entityId); VoidResponse updatePayoutScheduleSync(final String entityId, final Currency currency, final UpdateScheduleRequest updateScheduleRequest); + + EntityMembersResponse getEntityMembersSync(final String entityId); + + EntityMemberResponse reinviteEntityMemberSync(final String entityId, final String userId); + + ReserveRuleCreateResponse createReserveRuleSync(final String entityId, final ReserveRuleRequest reserveRuleRequest); + + ReserveRuleCreateResponse updateReserveRuleSync(final String entityId, final String reserveRuleId, final ReserveRuleRequest reserveRuleRequest); + + ReserveRuleResponse getReserveRuleSync(final String entityId, final String reserveRuleId); + + ReserveRulesResponse getReserveRulesSync(final String entityId); + } diff --git a/src/main/java/com/checkout/accounts/AccountsClientImpl.java b/src/main/java/com/checkout/accounts/AccountsClientImpl.java index ea3269e9..18eb4fe7 100644 --- a/src/main/java/com/checkout/accounts/AccountsClientImpl.java +++ b/src/main/java/com/checkout/accounts/AccountsClientImpl.java @@ -14,6 +14,10 @@ import com.checkout.accounts.payout.schedule.request.UpdateScheduleRequest; import com.checkout.accounts.payout.schedule.response.GetScheduleResponse; import com.checkout.accounts.payout.schedule.response.VoidResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleCreateResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; +import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; +import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -25,6 +29,8 @@ public class AccountsClientImpl extends AbstractClient implements AccountsClient private static final String FILES_PATH = "files"; private static final String PAYOUT_SCHEDULES_PATH = "payout-schedules"; private static final String PAYMENT_INSTRUMENTS_PATH = "payment-instruments"; + private static final String MEMBERS_PATH = "members"; + private static final String RESERVE_RULES_PATH = "reserve-rules"; private final ApiClient filesClient; @@ -152,6 +158,65 @@ public CompletableFuture updatePayoutSchedule(final String entityI request); } + @Override + public CompletableFuture getEntityMembers(final String entityId) { + validateEntityId(entityId); + return apiClient.getAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, MEMBERS_PATH), + sdkAuthorization(), + EntityMembersResponse.class); + } + + @Override + public CompletableFuture reinviteEntityMember(final String entityId, final String userId) { + validateEntityIdAndUserId(entityId, userId); + return apiClient.putAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, MEMBERS_PATH, userId), + sdkAuthorization(), + EntityMemberResponse.class, + null); + } + + @Override + public CompletableFuture createReserveRule(final String entityId, final ReserveRuleRequest reserveRuleRequest) { + validateEntityIdAndReserveRuleRequest(entityId, reserveRuleRequest); + return apiClient.postAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH), + sdkAuthorization(), + ReserveRuleCreateResponse.class, + reserveRuleRequest, + null); + } + + @Override + public CompletableFuture updateReserveRule(final String entityId, final String reserveRuleId, + final ReserveRuleRequest reserveRuleRequest) { + validateEntityIdReserveRuleIdAndRequest(entityId, reserveRuleId, reserveRuleRequest); + return apiClient.putAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH, reserveRuleId), + sdkAuthorization(), + ReserveRuleCreateResponse.class, + reserveRuleRequest); + } + + @Override + public CompletableFuture getReserveRule(final String entityId, final String reserveRuleId) { + validateEntityIdAndReserveRuleId(entityId, reserveRuleId); + return apiClient.getAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH, reserveRuleId), + sdkAuthorization(), + ReserveRuleResponse.class); + } + + @Override + public CompletableFuture getReserveRules(final String entityId) { + validateEntityId(entityId); + return apiClient.getAsync( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH), + sdkAuthorization(), + ReserveRulesResponse.class); + } + // Synchronous methods @Override public IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest) { @@ -270,6 +335,62 @@ public VoidResponse updatePayoutScheduleSync(final String entityId, final Curren request); } + @Override + public EntityMembersResponse getEntityMembersSync(final String entityId) { + validateEntityId(entityId); + return apiClient.get( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, MEMBERS_PATH), + sdkAuthorization(), + EntityMembersResponse.class); + } + + @Override + public EntityMemberResponse reinviteEntityMemberSync(final String entityId, final String userId) { + validateEntityIdAndUserId(entityId, userId); + return apiClient.put( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, MEMBERS_PATH, userId), + sdkAuthorization(), + EntityMemberResponse.class, + null); + } + @Override + public ReserveRuleCreateResponse createReserveRuleSync(final String entityId, final ReserveRuleRequest reserveRuleRequest) { + validateEntityIdAndReserveRuleRequest(entityId, reserveRuleRequest); + return apiClient.post( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH), + sdkAuthorization(), + ReserveRuleCreateResponse.class, + reserveRuleRequest, + null); + } + @Override + public ReserveRuleCreateResponse updateReserveRuleSync(final String entityId, final String reserveRuleId, final ReserveRuleRequest reserveRuleRequest) { + validateEntityIdReserveRuleIdAndRequest(entityId, reserveRuleId, reserveRuleRequest); + return apiClient.put( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH, reserveRuleId), + sdkAuthorization(), + ReserveRuleCreateResponse.class, + reserveRuleRequest); + } + + @Override + public ReserveRuleResponse getReserveRuleSync(final String entityId, final String reserveRuleId) { + validateEntityIdAndReserveRuleId(entityId, reserveRuleId); + return apiClient.get( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH, reserveRuleId), + sdkAuthorization(), + ReserveRuleResponse.class); + } + + @Override + public ReserveRulesResponse getReserveRulesSync(final String entityId) { + validateEntityId(entityId); + return apiClient.get( + buildPath(ACCOUNTS_PATH, ENTITIES_PATH, entityId, RESERVE_RULES_PATH), + sdkAuthorization(), + ReserveRulesResponse.class); + } + // Common methods private Map buildScheduleRequestMap(final Currency currency, final UpdateScheduleRequest updateScheduleRequest) { final Map request = new EnumMap<>(Currency.class); @@ -317,4 +438,20 @@ private void validateEntityIdCurrencyAndRequest(final String entityId, final Cur validateParams("entityId", entityId, "currency", currency, "updateScheduleRequest", updateScheduleRequest); } + private void validateEntityIdAndUserId(final String entityId, final String userId) { + validateParams("entityId", entityId, "userId", userId); + } + + private void validateEntityIdAndReserveRuleId(final String entityId, final String reserveRuleId) { + validateParams("entityId", entityId, "reserveRuleId", reserveRuleId); + } + + private void validateEntityIdReserveRuleIdAndRequest(final String entityId, final String reserveRuleId, final ReserveRuleRequest reserveRuleRequest) { + validateParams("entityId", entityId, "reserveRuleId", reserveRuleId, "reserveRuleRequest", reserveRuleRequest); + } + + private void validateEntityIdAndReserveRuleRequest(final String entityId, final ReserveRuleRequest reserveRuleRequest) { + validateParams("entityId", entityId, "reserveRuleRequest", reserveRuleRequest); + } + } diff --git a/src/main/java/com/checkout/accounts/EntityMemberResponse.java b/src/main/java/com/checkout/accounts/EntityMemberResponse.java new file mode 100644 index 00000000..bb45b569 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityMemberResponse.java @@ -0,0 +1,16 @@ +package com.checkout.accounts; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import com.checkout.common.Resource; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class EntityMemberResponse extends Resource { + + private String userId; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/EntityMembersResponse.java b/src/main/java/com/checkout/accounts/EntityMembersResponse.java new file mode 100644 index 00000000..80365b83 --- /dev/null +++ b/src/main/java/com/checkout/accounts/EntityMembersResponse.java @@ -0,0 +1,17 @@ +package com.checkout.accounts; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import com.checkout.common.Resource; +import java.util.List; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class EntityMembersResponse extends Resource { + + private List data; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/Headers.java b/src/main/java/com/checkout/accounts/Headers.java index f75f62ca..529d0883 100644 --- a/src/main/java/com/checkout/accounts/Headers.java +++ b/src/main/java/com/checkout/accounts/Headers.java @@ -1,11 +1,16 @@ package com.checkout.accounts; import com.google.gson.annotations.SerializedName; -import lombok.Builder; + +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data -@Builder +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor public class Headers { @SerializedName("if-match") diff --git a/src/main/java/com/checkout/accounts/UpdatePaymentInstrumentRequest.java b/src/main/java/com/checkout/accounts/UpdatePaymentInstrumentRequest.java index 9faf3b51..f6780855 100644 --- a/src/main/java/com/checkout/accounts/UpdatePaymentInstrumentRequest.java +++ b/src/main/java/com/checkout/accounts/UpdatePaymentInstrumentRequest.java @@ -1,18 +1,17 @@ package com.checkout.accounts; import com.google.gson.annotations.SerializedName; -import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; @Data -@Builder -public class UpdatePaymentInstrumentRequest { +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class UpdatePaymentInstrumentRequest extends Headers{ private String label; @SerializedName("default") private Boolean defaultDestination; - - private Headers headers; - } diff --git a/src/main/java/com/checkout/accounts/reserverules/entities/HoldingDuration.java b/src/main/java/com/checkout/accounts/reserverules/entities/HoldingDuration.java new file mode 100644 index 00000000..f6bde4f6 --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/entities/HoldingDuration.java @@ -0,0 +1,16 @@ +package com.checkout.accounts.reserverules.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HoldingDuration { + + private Integer weeks; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java b/src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java new file mode 100644 index 00000000..60f8ecd6 --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java @@ -0,0 +1,19 @@ +package com.checkout.accounts.reserverules.entities; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RollingReservePolicy { + private double percentage; + + @SerializedName("holding_duration") + private HoldingDuration holdingDuration; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleCreateResponse.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleCreateResponse.java new file mode 100644 index 00000000..af9387dc --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleCreateResponse.java @@ -0,0 +1,16 @@ +package com.checkout.accounts.reserverules.responses; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import com.checkout.common.Resource; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class ReserveRuleCreateResponse extends Resource { + + private String id; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java new file mode 100644 index 00000000..6d640244 --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java @@ -0,0 +1,28 @@ +package com.checkout.accounts.reserverules.responses; + +import com.checkout.accounts.Headers; +import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +import java.time.Instant; + +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class ReserveRuleRequest extends Headers { + + private String type; + + private RollingReservePolicy rolling; + + private Instant validFrom; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java new file mode 100644 index 00000000..62b68f60 --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java @@ -0,0 +1,24 @@ +package com.checkout.accounts.reserverules.responses; + +import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import com.checkout.common.Resource; +import java.time.Instant; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class ReserveRuleResponse extends Resource { + + private String id; + + private String type; + + private Instant validFrom; + + private RollingReservePolicy rolling; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRulesResponse.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRulesResponse.java new file mode 100644 index 00000000..c72468be --- /dev/null +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRulesResponse.java @@ -0,0 +1,17 @@ +package com.checkout.accounts.reserverules.responses; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import com.checkout.common.Resource; +import java.util.List; + +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class ReserveRulesResponse extends Resource { + + private List data; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/common/CheckoutUtils.java b/src/main/java/com/checkout/common/CheckoutUtils.java index 0f5d3af1..a9fb1082 100644 --- a/src/main/java/com/checkout/common/CheckoutUtils.java +++ b/src/main/java/com/checkout/common/CheckoutUtils.java @@ -17,6 +17,7 @@ public final class CheckoutUtils { public static final String ACCEPT_JSON = "application/json;charset=UTF-8"; public static final String CONTROL_TYPE = "control_type"; private static final String CKO_REQUEST_ID = "Cko-Request-Id"; + private static final String ETAG = "Etag"; private CheckoutUtils() { } @@ -36,6 +37,18 @@ public static String getRequestId(final Map responseHeaders) { return responseHeaders.get(CKO_REQUEST_ID); } + public static String getEtag(final Map responseHeaders) { + if (responseHeaders == null) { + return null; + } + for (Map.Entry entry : responseHeaders.entrySet()) { + if (entry.getKey().equalsIgnoreCase(ETAG)) { + return entry.getValue(); + } + } + return null; + } + public static void validateParams(final String p1, final Object o1) { validateMultipleRequires(new Object[][]{{p1, o1}}); } diff --git a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java index 228c54ec..07c884a3 100644 --- a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java +++ b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java @@ -32,6 +32,10 @@ import com.checkout.accounts.payout.schedule.request.UpdateScheduleRequest; import com.checkout.accounts.payout.schedule.response.GetScheduleResponse; import com.checkout.accounts.payout.schedule.response.VoidResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleCreateResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; +import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; +import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -438,6 +442,239 @@ void shouldThrowException_whenPaymentInstrumentRequestIsNullSync() { verifyNoInteractions(apiClient); } + @Test + void shouldGetEntityMembers() throws ExecutionException, InterruptedException { + final EntityMembersResponse expectedResponse = createEntityMembersResponse(); + + when(apiClient.getAsync("accounts/entities/entity_id/members", authorization, EntityMembersResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.getEntityMembers("entity_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldReinviteEntityMember() throws ExecutionException, InterruptedException { + final EntityMemberResponse expectedResponse = createEntityMemberResponse(); + + when(apiClient.putAsync(eq("accounts/entities/entity_id/members/user_id"), eq(authorization), eq(EntityMemberResponse.class), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.reinviteEntityMember("entity_id", "user_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldCreateReserveRule() throws ExecutionException, InterruptedException { + final ReserveRuleRequest request = createReserveRuleRequest(); + final ReserveRuleCreateResponse expectedResponse = createReserveRuleCreateResponse(); + + when(apiClient.postAsync(eq("accounts/entities/entity_id/reserve-rules"), eq(authorization), eq(ReserveRuleCreateResponse.class), eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.createReserveRule("entity_id", request); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldUpdateReserveRule() throws ExecutionException, InterruptedException { + final ReserveRuleRequest request = createReserveRuleRequest(); + final ReserveRuleCreateResponse expectedResponse = createReserveRuleCreateResponse(); + + when(apiClient.putAsync(eq("accounts/entities/entity_id/reserve-rules/rule_id"), eq(authorization), eq(ReserveRuleCreateResponse.class), eq(request))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.updateReserveRule("entity_id", "rule_id", request); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldGetReserveRule() throws ExecutionException, InterruptedException { + final ReserveRuleResponse expectedResponse = createReserveRuleResponse(); + + when(apiClient.getAsync("accounts/entities/entity_id/reserve-rules/rule_id", authorization, ReserveRuleResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.getReserveRule("entity_id", "rule_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldGetReserveRules() throws ExecutionException, InterruptedException { + final ReserveRulesResponse expectedResponse = createReserveRulesResponse(); + + when(apiClient.getAsync("accounts/entities/entity_id/reserve-rules", authorization, ReserveRulesResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.getReserveRules("entity_id"); + + validateResponse(expectedResponse, future.get()); + } + + // Synchronous methods + @Test + void shouldGetEntityMembersSync() { + final EntityMembersResponse expectedResponse = createEntityMembersResponse(); + + when(apiClient.get("accounts/entities/entity_id/members", authorization, EntityMembersResponse.class)) + .thenReturn(expectedResponse); + + final EntityMembersResponse actualResponse = accountsClient.getEntityMembersSync("entity_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldReinviteEntityMemberSync() { + final EntityMemberResponse expectedResponse = createEntityMemberResponse(); + + when(apiClient.put(eq("accounts/entities/entity_id/members/user_id"), eq(authorization), eq(EntityMemberResponse.class), isNull())) + .thenReturn(expectedResponse); + + final EntityMemberResponse actualResponse = accountsClient.reinviteEntityMemberSync("entity_id", "user_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldCreateReserveRuleSync() { + final ReserveRuleRequest request = createReserveRuleRequest(); + final ReserveRuleCreateResponse expectedResponse = createReserveRuleCreateResponse(); + + when(apiClient.post(eq("accounts/entities/entity_id/reserve-rules"), eq(authorization), eq(ReserveRuleCreateResponse.class), eq(request), isNull())) + .thenReturn(expectedResponse); + + final ReserveRuleCreateResponse actualResponse = accountsClient.createReserveRuleSync("entity_id", request); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldUpdateReserveRuleSync() { + final ReserveRuleRequest request = createReserveRuleRequest(); + final ReserveRuleCreateResponse expectedResponse = createReserveRuleCreateResponse(); + + when(apiClient.put(eq("accounts/entities/entity_id/reserve-rules/rule_id"), eq(authorization), eq(ReserveRuleCreateResponse.class), eq(request))) + .thenReturn(expectedResponse); + + final ReserveRuleCreateResponse actualResponse = accountsClient.updateReserveRuleSync("entity_id", "rule_id", request); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetReserveRuleSync() { + final ReserveRuleResponse expectedResponse = createReserveRuleResponse(); + + when(apiClient.get("accounts/entities/entity_id/reserve-rules/rule_id", authorization, ReserveRuleResponse.class)) + .thenReturn(expectedResponse); + + final ReserveRuleResponse actualResponse = accountsClient.getReserveRuleSync("entity_id", "rule_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetReserveRulesSync() { + final ReserveRulesResponse expectedResponse = createReserveRulesResponse(); + + when(apiClient.get("accounts/entities/entity_id/reserve-rules", authorization, ReserveRulesResponse.class)) + .thenReturn(expectedResponse); + + final ReserveRulesResponse actualResponse = accountsClient.getReserveRulesSync("entity_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldThrowException_whenEntityIdIsNullGetEntityMembers() { + try { + accountsClient.getEntityMembers(null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenEntityIdIsNullReinviteEntityMember() { + try { + accountsClient.reinviteEntityMember(null, "user_id"); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenUserIdIsNullReinviteEntityMember() { + try { + accountsClient.reinviteEntityMember("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("userId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenEntityIdIsNullGetReserveRule() { + try { + accountsClient.getReserveRule(null, "rule_id"); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenReserveRuleIdIsNullGetReserveRule() { + try { + accountsClient.getReserveRule("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("reserveRuleId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenEntityIdIsNullCreateReserveRule() { + try { + accountsClient.createReserveRule(null, createReserveRuleRequest()); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenReserveRuleRequestIsNullCreateReserveRule() { + try { + accountsClient.createReserveRule("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("reserveRuleRequest cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + // Common methods private OnboardEntityRequest createOnboardEntityRequest() { return OnboardEntityRequest.builder().build(); @@ -504,6 +741,32 @@ private PaymentInstrumentQueryResponse createPaymentInstrumentQueryResponse() { return mock(PaymentInstrumentQueryResponse.class); } + // Entity Members mock objects + private EntityMembersResponse createEntityMembersResponse() { + return mock(EntityMembersResponse.class); + } + + private EntityMemberResponse createEntityMemberResponse() { + return mock(EntityMemberResponse.class); + } + + // Reserve Rules mock objects + private ReserveRuleRequest createReserveRuleRequest() { + return mock(ReserveRuleRequest.class); + } + + private ReserveRuleResponse createReserveRuleResponse() { + return mock(ReserveRuleResponse.class); + } + + private ReserveRulesResponse createReserveRulesResponse() { + return mock(ReserveRulesResponse.class); + } + + private ReserveRuleCreateResponse createReserveRuleCreateResponse() { + return mock(ReserveRuleCreateResponse.class); + } + private void validateResponse(T expectedResponse, T actualResponse) { assertNotNull(actualResponse); assertEquals(expectedResponse, actualResponse); diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index d50978ca..86ad83a7 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -7,6 +7,12 @@ import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; import com.checkout.TestHelper; +import com.checkout.accounts.reserverules.entities.HoldingDuration; +import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import com.checkout.accounts.reserverules.responses.ReserveRuleCreateResponse; +import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; +import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; +import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; import com.checkout.common.Address; import com.checkout.common.CountryCode; import com.checkout.common.Currency; @@ -154,7 +160,179 @@ void shouldCreateAndRetrievePaymentInstrumentSync() throws URISyntaxException { validatePaymentInstrumentDetailsResponse(instrumentDetailsResponse); } + @Test + void shouldGetEntityMembers() { + final String entityId = createTestEntity(); + + final EntityMembersResponse response = blocking(() -> checkoutApi.accountsClient().getEntityMembers(entityId)); + validateEntityMembersResponse(response); + } + + @Test + void shouldReinviteEntityMember() { + final String entityId = createTestEntity(); + + // Get entity members first to find a valid user ID + final EntityMembersResponse membersResponse = blocking(() -> checkoutApi.accountsClient().getEntityMembers(entityId)); + validateEntityMembersResponse(membersResponse); + + // Skip test if no members found + if (membersResponse.getData() == null || membersResponse.getData().isEmpty()) { + System.out.println("Skipping reinvite test - no entity members found"); + return; + } + + final String userId = membersResponse.getData().get(0).getUserId(); + assertNotNull(userId, "First entity member should have a user ID"); + + final EntityMemberResponse response = blocking(() -> checkoutApi.accountsClient().reinviteEntityMember(entityId, userId)); + validateEntityMemberResponse(response); + } + + @Test + void shouldCreateGetAndUpdateReserveRule() { + final String entityId = createTestEntity(); + final ReserveRuleRequest createRequest = buildReserveRuleRequest(); + + final ReserveRuleCreateResponse createResponse = blocking(() -> checkoutApi.accountsClient().createReserveRule(entityId, createRequest)); + validateReserveRuleCreateResponse(createResponse); + + final ReserveRuleResponse getResponse = blocking(() -> checkoutApi.accountsClient().getReserveRule(entityId, createResponse.getId())); + validateReserveRuleResponse(getResponse); + + // Get Etag from the creation response headers + String etag = null; + if (createResponse != null) + { + etag = createResponse.getEtag(); + } + + // Update (with the If-Match header when using the etag) + final ReserveRuleRequest updateRequest = buildReserveRuleRequestWithEtag(etag); // Set the Etag for concurrency control + final ReserveRuleCreateResponse updateResponse = blocking(() -> checkoutApi.accountsClient().updateReserveRule(entityId, createResponse.getId(), updateRequest)); + validateReserveRuleCreateResponse(updateResponse); + } + + @Test + void shouldGetReserveRules() { + final String entityId = createTestEntity(); + + final ReserveRulesResponse response = blocking(() -> checkoutApi.accountsClient().getReserveRules(entityId)); + validateReserveRulesResponse(response); + } + + // Synchronous methods + @Test + void shouldGetEntityMembersSync() { + final String entityId = createTestEntity(); + + final EntityMembersResponse response = checkoutApi.accountsClient().getEntityMembersSync(entityId); + validateEntityMembersResponse(response); + } + + @Test + void shouldReinviteEntityMemberSync() { + final String entityId = createTestEntity(); + + // Get entity members first to find a valid user ID + final EntityMembersResponse membersResponse = checkoutApi.accountsClient().getEntityMembersSync(entityId); + validateEntityMembersResponse(membersResponse); + + // Skip test if no members found + if (membersResponse.getData() == null || membersResponse.getData().isEmpty()) { + System.out.println("Skipping reinvite sync test - no entity members found"); + return; + } + + final String userId = membersResponse.getData().get(0).getUserId(); + assertNotNull(userId, "First entity member should have a user ID"); + + final EntityMemberResponse response = checkoutApi.accountsClient().reinviteEntityMemberSync(entityId, userId); + validateEntityMemberResponse(response); + } + + @Test + void shouldCreateGetAndUpdateReserveRuleSync() { + final String entityId = createTestEntity(); + final ReserveRuleRequest createRequest = buildReserveRuleRequest(); + + final ReserveRuleCreateResponse createResponse = checkoutApi.accountsClient().createReserveRuleSync(entityId, createRequest); + validateReserveRuleCreateResponse(createResponse); + + final ReserveRuleResponse getResponse = checkoutApi.accountsClient().getReserveRuleSync(entityId, createResponse.getId()); + validateReserveRuleResponse(getResponse); + + // Get Etag from the creation response headers + String etag = null; + if (createResponse != null) + { + etag = createResponse.getEtag(); + } + + // Update (with the If-Match header when using the etag) + final ReserveRuleRequest updateRequest = buildReserveRuleRequestWithEtag(etag); // Set the Etag for concurrency control + final ReserveRuleCreateResponse updateResponse = checkoutApi.accountsClient().updateReserveRuleSync(entityId, createResponse.getId(), updateRequest); + validateReserveRuleCreateResponse(updateResponse); + } + + @Test + void shouldGetReserveRulesSync() { + final String entityId = createTestEntity(); + + final ReserveRulesResponse response = checkoutApi.accountsClient().getReserveRulesSync(entityId); + validateReserveRulesResponse(response); + } + // Common methods + private String createTestEntity() { + final String randomReference = RandomStringUtils.random(15, true, true); + final OnboardEntityRequest entityRequest = OnboardEntityRequest.builder() + .reference(randomReference) + .contactDetails(ContactDetails.builder() + .phone(buildAccountPhone()) + .emailAddresses(EntityEmailAddresses.builder() + .primary(generateRandomEmail()) + .build()) + .build()) + .profile(buildProfile()) + .company(Company.builder() + .businessRegistrationNumber("01234567") + .legalName("Reserve Rules Test Inc.") + .tradingName("Reserve Rules Test") + .principalAddress(TestHelper.createAddress()) + .registeredAddress(TestHelper.createAddress()) + .representatives(Collections.singletonList(Representative.builder() + .firstName("John") + .lastName("Doe") + .address(TestHelper.createAddress()) + .identification(Identification.builder() + .nationalIdNumber("AB123456C") + .build()) + .phone(buildAccountPhone()) + .dateOfBirth(DateOfBirth.builder() + .day(5) + .month(6) + .year(1995) + .build()) + .placeOfBirth(PlaceOfBirth.builder() + .country(CountryCode.GB) + .build()) + .roles(Collections.singletonList(EntityRoles.UBO)) + .build())) + .financialDetails(EntityFinancialDetails.builder() + .annualProcessingVolume(120000L) + .averageTransactionValue(10000L) + .highestTransactionValue(2500L) + .build()) + .build()) + .build(); + + final OnboardEntityResponse entityResponse = blocking(() -> checkoutApi.accountsClient().createEntity(entityRequest)); + assertNotNull(entityResponse); + assertNotNull(entityResponse.getId()); + return entityResponse.getId(); + } + private IdResponse uploadFileSync() throws URISyntaxException { final URL resource = getClass().getClassLoader().getResource("checkout.jpeg"); final File file = new File(resource.toURI()); @@ -354,4 +532,60 @@ private CheckoutApi getAccountsCheckoutApi() { .build(); } + private static void validateEntityMembersResponse(final EntityMembersResponse response) { + assertNotNull(response); + assertNotNull(response.getData()); + } + + private static void validateEntityMemberResponse(final EntityMemberResponse response) { + assertNotNull(response); + assertNotNull(response.getUserId()); + } + + // Reserve Rules builders and validators + private static ReserveRuleRequest buildReserveRuleRequest() { + return ReserveRuleRequest.builder() + .type("rolling") + .validFrom(java.time.Instant.now().plusSeconds(3600)) // 1 hour from now + .rolling(RollingReservePolicy.builder() + .percentage(10.0) + .holdingDuration(HoldingDuration.builder() + .weeks(2) + .build()) + .build()) + .build(); + } + + private static ReserveRuleRequest buildReserveRuleRequestWithEtag(String etag) { + return ReserveRuleRequest.builder() + .type("rolling") + .validFrom(java.time.Instant.now().plusSeconds(3600)) // 1 hour from now + .rolling(RollingReservePolicy.builder() + .percentage(10.0) + .holdingDuration(HoldingDuration.builder() + .weeks(2) + .build()) + .build()) + .ifMatch(etag) + .build(); + } + + private static void validateReserveRuleCreateResponse(final ReserveRuleCreateResponse response) { + assertNotNull(response); + assertNotNull(response.getId()); + } + + private static void validateReserveRuleResponse(final ReserveRuleResponse response) { + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getType()); + assertNotNull(response.getValidFrom()); + assertNotNull(response.getRolling()); + } + + private static void validateReserveRulesResponse(final ReserveRulesResponse response) { + assertNotNull(response); + assertNotNull(response.getData()); + } + } \ No newline at end of file From d780ab121863165e8ae80267510c8a856cbfc487 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 9 Mar 2026 16:16:11 +0100 Subject: [PATCH 06/19] RollingReserveRule rename --- .../{RollingReservePolicy.java => RollingReserveRule.java} | 2 +- .../accounts/reserverules/responses/ReserveRuleRequest.java | 4 ++-- .../reserverules/responses/ReserveRuleResponse.java | 4 ++-- src/test/java/com/checkout/accounts/AccountsTestIT.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename src/main/java/com/checkout/accounts/reserverules/entities/{RollingReservePolicy.java => RollingReserveRule.java} (91%) diff --git a/src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java b/src/main/java/com/checkout/accounts/reserverules/entities/RollingReserveRule.java similarity index 91% rename from src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java rename to src/main/java/com/checkout/accounts/reserverules/entities/RollingReserveRule.java index 60f8ecd6..8ef239d0 100644 --- a/src/main/java/com/checkout/accounts/reserverules/entities/RollingReservePolicy.java +++ b/src/main/java/com/checkout/accounts/reserverules/entities/RollingReserveRule.java @@ -10,7 +10,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class RollingReservePolicy { +public class RollingReserveRule { private double percentage; @SerializedName("holding_duration") diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java index 6d640244..2de30434 100644 --- a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleRequest.java @@ -1,7 +1,7 @@ package com.checkout.accounts.reserverules.responses; import com.checkout.accounts.Headers; -import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import com.checkout.accounts.reserverules.entities.RollingReserveRule; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -21,7 +21,7 @@ public class ReserveRuleRequest extends Headers { private String type; - private RollingReservePolicy rolling; + private RollingReserveRule rolling; private Instant validFrom; diff --git a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java index 62b68f60..54990831 100644 --- a/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java +++ b/src/main/java/com/checkout/accounts/reserverules/responses/ReserveRuleResponse.java @@ -1,6 +1,6 @@ package com.checkout.accounts.reserverules.responses; -import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import com.checkout.accounts.reserverules.entities.RollingReserveRule; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @@ -19,6 +19,6 @@ public class ReserveRuleResponse extends Resource { private Instant validFrom; - private RollingReservePolicy rolling; + private RollingReserveRule rolling; } \ No newline at end of file diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index 86ad83a7..9bf402ae 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -8,7 +8,7 @@ import com.checkout.SandboxTestFixture; import com.checkout.TestHelper; import com.checkout.accounts.reserverules.entities.HoldingDuration; -import com.checkout.accounts.reserverules.entities.RollingReservePolicy; +import com.checkout.accounts.reserverules.entities.RollingReserveRule; import com.checkout.accounts.reserverules.responses.ReserveRuleCreateResponse; import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; @@ -547,7 +547,7 @@ private static ReserveRuleRequest buildReserveRuleRequest() { return ReserveRuleRequest.builder() .type("rolling") .validFrom(java.time.Instant.now().plusSeconds(3600)) // 1 hour from now - .rolling(RollingReservePolicy.builder() + .rolling(RollingReserveRule.builder() .percentage(10.0) .holdingDuration(HoldingDuration.builder() .weeks(2) @@ -560,7 +560,7 @@ private static ReserveRuleRequest buildReserveRuleRequestWithEtag(String etag) { return ReserveRuleRequest.builder() .type("rolling") .validFrom(java.time.Instant.now().plusSeconds(3600)) // 1 hour from now - .rolling(RollingReservePolicy.builder() + .rolling(RollingReserveRule.builder() .percentage(10.0) .holdingDuration(HoldingDuration.builder() .weeks(2) From 07d5c14f08213e8f0e58f50924752b43f6853433 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 10 Mar 2026 11:10:03 +0100 Subject: [PATCH 07/19] New endpoints in AccountsClient for entities file management + unit and integration tests --- .../com/checkout/accounts/AccountsClient.java | 11 ++ .../checkout/accounts/AccountsClientImpl.java | 51 ++++++++ .../accounts/files/entities/FilePurpose.java | 36 ++++++ .../files/request/FileUploadRequest.java | 18 +++ .../files/response/FileDetailsResponse.java | 33 +++++ .../files/response/FileUploadResponse.java | 23 ++++ .../accounts/AccountsClientImplTest.java | 117 +++++++++++++++++- .../com/checkout/accounts/AccountsTestIT.java | 69 +++++++++++ 8 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/checkout/accounts/files/entities/FilePurpose.java create mode 100644 src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java create mode 100644 src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java create mode 100644 src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java diff --git a/src/main/java/com/checkout/accounts/AccountsClient.java b/src/main/java/com/checkout/accounts/AccountsClient.java index 36dfa68b..96c79d3d 100644 --- a/src/main/java/com/checkout/accounts/AccountsClient.java +++ b/src/main/java/com/checkout/accounts/AccountsClient.java @@ -10,6 +10,9 @@ import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; +import com.checkout.accounts.files.request.FileUploadRequest; +import com.checkout.accounts.files.response.FileUploadResponse; +import com.checkout.accounts.files.response.FileDetailsResponse; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -17,6 +20,10 @@ public interface AccountsClient { CompletableFuture submitFile(AccountsFileRequest accountsFileRequest); + CompletableFuture uploadFile(String entityId, FileUploadRequest fileUploadRequest); + + CompletableFuture retrieveFile(String entityId, String fileId); + CompletableFuture createEntity(OnboardEntityRequest entityRequest); CompletableFuture retrievePaymentInstrumentDetails(String entityId, String paymentInstrumentId); @@ -58,6 +65,10 @@ CompletableFuture updateReserveRule(String entityId, // Synchronous methods IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest); + + FileUploadResponse uploadFileSync(final String entityId, final FileUploadRequest fileUploadRequest); + + FileDetailsResponse retrieveFileSync(final String entityId, final String fileId); OnboardEntityResponse createEntitySync(final OnboardEntityRequest entityRequest); diff --git a/src/main/java/com/checkout/accounts/AccountsClientImpl.java b/src/main/java/com/checkout/accounts/AccountsClientImpl.java index 18eb4fe7..92e3ee03 100644 --- a/src/main/java/com/checkout/accounts/AccountsClientImpl.java +++ b/src/main/java/com/checkout/accounts/AccountsClientImpl.java @@ -18,6 +18,9 @@ import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; +import com.checkout.accounts.files.request.FileUploadRequest; +import com.checkout.accounts.files.response.FileUploadResponse; +import com.checkout.accounts.files.response.FileDetailsResponse; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -51,6 +54,26 @@ public CompletableFuture submitFile(final AccountsFileRequest accoun IdResponse.class); } + @Override + public CompletableFuture uploadFile(final String entityId, final FileUploadRequest fileUploadRequest) { + validateEntityIdAndFileUploadRequest(entityId, fileUploadRequest); + return filesClient.postAsync( + buildPath(ENTITIES_PATH, entityId, FILES_PATH), + sdkAuthorization(), + FileUploadResponse.class, + fileUploadRequest, + null); + } + + @Override + public CompletableFuture retrieveFile(final String entityId, final String fileId) { + validateEntityIdAndFileId(entityId, fileId); + return filesClient.getAsync( + buildPath(ENTITIES_PATH, entityId, FILES_PATH, fileId), + sdkAuthorization(), + FileDetailsResponse.class); + } + @Override public CompletableFuture createEntity(final OnboardEntityRequest entityRequest) { validateEntityRequest(entityRequest); @@ -228,6 +251,26 @@ public IdResponse submitFileSync(final AccountsFileRequest accountsFileRequest) IdResponse.class); } + @Override + public FileUploadResponse uploadFileSync(final String entityId, final FileUploadRequest fileUploadRequest) { + validateEntityIdAndFileUploadRequest(entityId, fileUploadRequest); + return filesClient.post( + buildPath(ENTITIES_PATH, entityId, FILES_PATH), + sdkAuthorization(), + FileUploadResponse.class, + fileUploadRequest, + null); + } + + @Override + public FileDetailsResponse retrieveFileSync(final String entityId, final String fileId) { + validateEntityIdAndFileId(entityId, fileId); + return filesClient.get( + buildPath(ENTITIES_PATH, entityId, FILES_PATH, fileId), + sdkAuthorization(), + FileDetailsResponse.class); + } + @Override public OnboardEntityResponse createEntitySync(final OnboardEntityRequest entityRequest) { validateEntityRequest(entityRequest); @@ -454,4 +497,12 @@ private void validateEntityIdAndReserveRuleRequest(final String entityId, final validateParams("entityId", entityId, "reserveRuleRequest", reserveRuleRequest); } + private void validateEntityIdAndFileUploadRequest(final String entityId, final FileUploadRequest fileUploadRequest) { + validateParams("entityId", entityId, "fileUploadRequest", fileUploadRequest); + } + + private void validateEntityIdAndFileId(final String entityId, final String fileId) { + validateParams("entityId", entityId, "fileId", fileId); + } + } diff --git a/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java b/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java new file mode 100644 index 00000000..cb030fd0 --- /dev/null +++ b/src/main/java/com/checkout/accounts/files/entities/FilePurpose.java @@ -0,0 +1,36 @@ +package com.checkout.accounts.files.entities; + +import com.google.gson.annotations.SerializedName; + +public enum FilePurpose { + @SerializedName("additional_document") + ADDITIONAL_DOCUMENT, + @SerializedName("articles_of_association") + ARTICLES_OF_ASSOCIATION, + @SerializedName("bank_verification") + BANK_VERIFICATION, + @SerializedName("certified_authorised_signatory") + CERTIFIED_AUTHORISED_SIGNATORY, + @SerializedName("company_ownership") + COMPANY_OWNERSHIP, + @SerializedName("company_verification") + COMPANY_VERIFICATION, + @SerializedName("financial_verification") + FINANCIAL_VERIFICATION, + @SerializedName("identity_verification") + IDENTITY_VERIFICATION, + @SerializedName("proof_of_legality") + PROOF_OF_LEGALITY, + @SerializedName("proof_of_principal_address") + PROOF_OF_PRINCIPAL_ADDRESS, + @SerializedName("shareholder_structure") + SHAREHOLDER_STRUCTURE, + @SerializedName("tax_verification") + TAX_VERIFICATION, + @SerializedName("proof_of_residential_address") + PROOF_OF_RESIDENTIAL_ADDRESS, + @SerializedName("proof_of_registration") + PROOF_OF_REGISTRATION, + @SerializedName("dispute_evidence") + DISPUTE_EVIDENCE +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java b/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java new file mode 100644 index 00000000..86cf0a81 --- /dev/null +++ b/src/main/java/com/checkout/accounts/files/request/FileUploadRequest.java @@ -0,0 +1,18 @@ +package com.checkout.accounts.files.request; + + +import com.checkout.accounts.files.entities.FilePurpose; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileUploadRequest { + + private FilePurpose purpose; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java b/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java new file mode 100644 index 00000000..58a73664 --- /dev/null +++ b/src/main/java/com/checkout/accounts/files/response/FileDetailsResponse.java @@ -0,0 +1,33 @@ +package com.checkout.accounts.files.response; + +import com.checkout.common.Resource; +import com.checkout.accounts.files.entities.FilePurpose; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileDetailsResponse extends Resource { + + private String id; + + private String status; + + private List statusReasons; + + private Long size; + + private String mimeType; + + private Instant uploadedOn; + + private FilePurpose purpose; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java b/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java new file mode 100644 index 00000000..7e355453 --- /dev/null +++ b/src/main/java/com/checkout/accounts/files/response/FileUploadResponse.java @@ -0,0 +1,23 @@ +package com.checkout.accounts.files.response; + +import com.checkout.common.Resource; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class FileUploadResponse extends Resource { + + private String id; + + private Long maximumSizeInBytes; + + private List documentTypesForPurpose; +} \ No newline at end of file diff --git a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java index 07c884a3..8015673c 100644 --- a/src/test/java/com/checkout/accounts/AccountsClientImplTest.java +++ b/src/test/java/com/checkout/accounts/AccountsClientImplTest.java @@ -36,6 +36,10 @@ import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; +import com.checkout.accounts.files.request.FileUploadRequest; +import com.checkout.accounts.files.response.FileUploadResponse; +import com.checkout.accounts.files.response.FileDetailsResponse; +import com.checkout.accounts.files.entities.FilePurpose; import com.checkout.common.Currency; import com.checkout.common.IdResponse; @@ -102,7 +106,104 @@ void shouldThrowException_whenEntityIdIsNull() { } @Test - void shouldCreateEntity() throws ExecutionException, InterruptedException { + void shouldThrowException_whenEntityIdIsNullForUploadFile() { + try { + accountsClient.uploadFile(null, createFileUploadRequest()); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenFileUploadRequestIsNull() { + try { + accountsClient.uploadFile("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("fileUploadRequest cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenEntityIdIsNullForGetFileDetails() { + try { + accountsClient.retrieveFile(null, "file_id"); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("entityId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldThrowException_whenFileIdIsNull() { + try { + accountsClient.retrieveFile("entity_id", null); + fail(); + } catch (final CheckoutArgumentException checkoutArgumentException) { + assertEquals("fileId cannot be null", checkoutArgumentException.getMessage()); + } + + verifyNoInteractions(apiClient); + } + + @Test + void shouldUploadFile() throws ExecutionException, InterruptedException { + final FileUploadRequest request = createFileUploadRequest(); + final FileUploadResponse expectedResponse = createFileUploadResponse(); + + when(apiClient.postAsync(eq("entities/entity_id/files"), eq(authorization), eq(FileUploadResponse.class), eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.uploadFile("entity_id", request); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldGetFileDetails() throws ExecutionException, InterruptedException { + final FileDetailsResponse expectedResponse = createFileDetailsResponse(); + + when(apiClient.getAsync("entities/entity_id/files/file_id", authorization, FileDetailsResponse.class)) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = accountsClient.retrieveFile("entity_id", "file_id"); + + validateResponse(expectedResponse, future.get()); + } + + @Test + void shouldUploadFileSync() { + final FileUploadRequest request = createFileUploadRequest(); + final FileUploadResponse expectedResponse = createFileUploadResponse(); + + when(apiClient.post(eq("entities/entity_id/files"), eq(authorization), eq(FileUploadResponse.class), eq(request), isNull())) + .thenReturn(expectedResponse); + + final FileUploadResponse actualResponse = accountsClient.uploadFileSync("entity_id", request); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetFileDetailsSync() { + final FileDetailsResponse expectedResponse = createFileDetailsResponse(); + + when(apiClient.get("entities/entity_id/files/file_id", authorization, FileDetailsResponse.class)) + .thenReturn(expectedResponse); + + final FileDetailsResponse actualResponse = accountsClient.retrieveFileSync("entity_id", "file_id"); + + validateResponse(expectedResponse, actualResponse); + } + + @Test void shouldCreateEntity() throws ExecutionException, InterruptedException { final OnboardEntityRequest request = createOnboardEntityRequest(); final OnboardEntityResponse expectedResponse = createOnboardEntityResponse(); @@ -701,6 +802,20 @@ private AccountsFileRequest createAccountsFileRequest() { return AccountsFileRequest.builder().build(); } + private FileUploadRequest createFileUploadRequest() { + return FileUploadRequest.builder() + .purpose(FilePurpose.IDENTITY_VERIFICATION) + .build(); + } + + private FileUploadResponse createFileUploadResponse() { + return mock(FileUploadResponse.class); + } + + private FileDetailsResponse createFileDetailsResponse() { + return mock(FileDetailsResponse.class); + } + private UpdateScheduleRequest createUpdateScheduleRequest() { return mock(UpdateScheduleRequest.class); } diff --git a/src/test/java/com/checkout/accounts/AccountsTestIT.java b/src/test/java/com/checkout/accounts/AccountsTestIT.java index 9bf402ae..0348fdfd 100644 --- a/src/test/java/com/checkout/accounts/AccountsTestIT.java +++ b/src/test/java/com/checkout/accounts/AccountsTestIT.java @@ -13,6 +13,10 @@ import com.checkout.accounts.reserverules.responses.ReserveRuleRequest; import com.checkout.accounts.reserverules.responses.ReserveRuleResponse; import com.checkout.accounts.reserverules.responses.ReserveRulesResponse; +import com.checkout.accounts.files.request.FileUploadRequest; +import com.checkout.accounts.files.response.FileUploadResponse; +import com.checkout.accounts.files.response.FileDetailsResponse; +import com.checkout.accounts.files.entities.FilePurpose; import com.checkout.common.Address; import com.checkout.common.CountryCode; import com.checkout.common.Currency; @@ -79,6 +83,31 @@ void shouldUploadAccountsFile() throws URISyntaxException { } @Disabled("Recently giving a 503 with 'no healthy upstream' description from the API, disabled") + @Test + void shouldUploadFileForEntity() { + final String entityId = createTestEntity(); + final FileUploadRequest fileUploadRequest = FileUploadRequest.builder() + .purpose(FilePurpose.IDENTITY_VERIFICATION) + .build(); + + final FileUploadResponse response = blocking(() -> checkoutApi.accountsClient().uploadFile(entityId, fileUploadRequest)); + validateFileUploadResponseForEntity(response); + } + + @Test + void shouldRetrieveFileForEntity() { + final String entityId = createTestEntity(); + final FileUploadRequest fileUploadRequest = FileUploadRequest.builder() + .purpose(FilePurpose.IDENTITY_VERIFICATION) + .build(); + + final FileUploadResponse uploadResponse = blocking(() -> checkoutApi.accountsClient().uploadFile(entityId, fileUploadRequest)); + validateFileUploadResponseForEntity(uploadResponse); + + final FileDetailsResponse detailsResponse = blocking(() -> checkoutApi.accountsClient().retrieveFile(entityId, uploadResponse.getId())); + validateFileDetailsResponseForEntity(detailsResponse, uploadResponse.getId()); + } + @Test void shouldCreateAndRetrievePaymentInstrument() throws URISyntaxException { final CheckoutApi checkoutApi = getAccountsCheckoutApi(); @@ -160,6 +189,31 @@ void shouldCreateAndRetrievePaymentInstrumentSync() throws URISyntaxException { validatePaymentInstrumentDetailsResponse(instrumentDetailsResponse); } + @Test + void shouldUploadFileForEntitySync() { + final String entityId = createTestEntity(); + final FileUploadRequest fileUploadRequest = FileUploadRequest.builder() + .purpose(FilePurpose.IDENTITY_VERIFICATION) + .build(); + + final FileUploadResponse response = checkoutApi.accountsClient().uploadFileSync(entityId, fileUploadRequest); + validateFileUploadResponseForEntity(response); + } + + @Test + void shouldRetrieveFileForEntitySync() { + final String entityId = createTestEntity(); + final FileUploadRequest fileUploadRequest = FileUploadRequest.builder() + .purpose(FilePurpose.IDENTITY_VERIFICATION) + .build(); + + final FileUploadResponse uploadResponse = checkoutApi.accountsClient().uploadFileSync(entityId, fileUploadRequest); + validateFileUploadResponseForEntity(uploadResponse); + + final FileDetailsResponse detailsResponse = checkoutApi.accountsClient().retrieveFileSync(entityId, uploadResponse.getId()); + validateFileDetailsResponseForEntity(detailsResponse, uploadResponse.getId()); + } + @Test void shouldGetEntityMembers() { final String entityId = createTestEntity(); @@ -435,6 +489,21 @@ private void validateFileUploadResponse(final IdResponse fileResponse) { assertNotNull(fileResponse.getId()); } + private void validateFileUploadResponseForEntity(final FileUploadResponse fileResponse) { + assertNotNull(fileResponse); + assertNotNull(fileResponse.getId()); + assertNotNull(fileResponse.getMaximumSizeInBytes()); + assertNotNull(fileResponse.getDocumentTypesForPurpose()); + assertNotNull(fileResponse.getLinks()); + } + + private void validateFileDetailsResponseForEntity(final FileDetailsResponse fileDetailsResponse, final String expectedFileId) { + assertNotNull(fileDetailsResponse); + assertEquals(expectedFileId, fileDetailsResponse.getId()); + assertNotNull(fileDetailsResponse.getStatus()); + assertNotNull(fileDetailsResponse.getPurpose()); + } + private void validatePaymentInstrumentDetailsResponse(final PaymentInstrumentDetailsResponse instrumentDetailsResponse) { assertNotNull(instrumentDetailsResponse); assertNotNull(instrumentDetailsResponse.getId()); From f27c2d7052c616ec6230ca106aec0f879281345a Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 10 Mar 2026 16:09:28 +0100 Subject: [PATCH 08/19] Issuing/transactions read endpoints support + unit and integration tests --- .../com/checkout/issuing/IssuingClient.java | 11 ++ .../checkout/issuing/IssuingClientImpl.java | 56 ++++++++++ .../transactions/entities/DigitalCard.java | 11 ++ .../transactions/entities/Merchant.java | 20 ++++ .../entities/MessageIndicator.java | 35 ++++++ .../entities/MessageInitiator.java | 20 ++++ .../transactions/entities/MessageResult.java | 14 +++ .../transactions/entities/MessageType.java | 32 ++++++ .../entities/ReferenceTransaction.java | 11 ++ .../transactions/entities/ReferenceType.java | 11 ++ .../entities/TransactionAmount.java | 11 ++ .../entities/TransactionAmounts.java | 17 +++ .../entities/TransactionCard.java | 11 ++ .../entities/TransactionCardholder.java | 9 ++ .../entities/TransactionClient.java | 9 ++ .../entities/TransactionEntity.java | 9 ++ .../entities/TransactionMessage.java | 32 ++++++ .../entities/TransactionStatus.java | 23 ++++ .../entities/TransactionType.java | 71 ++++++++++++ .../transactions/entities/WalletType.java | 14 +++ .../requests/TransactionsQuery.java | 28 +++++ .../responses/TransactionResponse.java | 40 +++++++ .../responses/TransactionsListResponse.java | 20 ++++ .../responses/TransactionsSingleResponse.java | 40 +++++++ .../checkout/issuing/BaseIssuingTestIT.java | 3 +- .../issuing/IssuingClientImplTest.java | 103 ++++++++++++++++++ .../issuing/IssuingTransactionsTestIT.java | 102 +++++++++++++++++ 27 files changed, 762 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/DigitalCard.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/Merchant.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/MessageIndicator.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/MessageInitiator.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/MessageResult.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/MessageType.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/ReferenceTransaction.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/ReferenceType.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionAmount.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionAmounts.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionCard.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionCardholder.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionClient.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionEntity.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionMessage.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionStatus.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/TransactionType.java create mode 100644 src/main/java/com/checkout/issuing/transactions/entities/WalletType.java create mode 100644 src/main/java/com/checkout/issuing/transactions/requests/TransactionsQuery.java create mode 100644 src/main/java/com/checkout/issuing/transactions/responses/TransactionResponse.java create mode 100644 src/main/java/com/checkout/issuing/transactions/responses/TransactionsListResponse.java create mode 100644 src/main/java/com/checkout/issuing/transactions/responses/TransactionsSingleResponse.java create mode 100644 src/test/java/com/checkout/issuing/IssuingTransactionsTestIT.java diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index f75d38f6..222c363c 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -53,6 +53,9 @@ import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; import com.checkout.issuing.disputes.responses.DisputeResponse; +import com.checkout.issuing.transactions.requests.TransactionsQuery; +import com.checkout.issuing.transactions.responses.TransactionsSingleResponse; +import com.checkout.issuing.transactions.responses.TransactionsListResponse; import com.checkout.payments.VoidResponse; import java.util.concurrent.CompletableFuture; @@ -160,6 +163,10 @@ CompletableFuture simulateReversal( CompletableFuture submitDispute(final String disputeId, String idempotencyKey, final SubmitDisputeRequest submitDisputeRequest); + CompletableFuture getListTransactions(final TransactionsQuery queryFilter); + + CompletableFuture getSingleTransaction(final String transactionId); + // Synchronous methods CardholderAccessTokenResponse requestCardholderAccessTokenSync(CardholderAccessTokenRequest cardholderAccessTokenRequest); @@ -261,4 +268,8 @@ CardAuthorizationReversalResponse simulateReversalSync( VoidResponse escalateDisputeSync(String disputeId, String idempotencyKey, EscalateDisputeRequest escalateDisputeRequest); DisputeResponse submitDisputeSync(String disputeId, String idempotencyKey, SubmitDisputeRequest submitDisputeRequest); + + TransactionsListResponse getListTransactionsSync(TransactionsQuery queryFilter); + + TransactionsSingleResponse getSingleTransactionSync(String transactionId); } \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/IssuingClientImpl.java b/src/main/java/com/checkout/issuing/IssuingClientImpl.java index 601b12bf..e9bd53a7 100644 --- a/src/main/java/com/checkout/issuing/IssuingClientImpl.java +++ b/src/main/java/com/checkout/issuing/IssuingClientImpl.java @@ -58,6 +58,9 @@ import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; import com.checkout.issuing.disputes.responses.DisputeResponse; +import com.checkout.issuing.transactions.requests.TransactionsQuery; +import com.checkout.issuing.transactions.responses.TransactionsSingleResponse; +import com.checkout.issuing.transactions.responses.TransactionsListResponse; import com.checkout.payments.VoidResponse; import java.io.UnsupportedEncodingException; @@ -108,6 +111,8 @@ public class IssuingClientImpl extends AbstractClient implements IssuingClient { private static final String ACCESS_TOKEN_PATH = "access/connect/token"; private static final String DISPUTES_PATH = "disputes"; + private static final String TRANSACTIONS_PATH = "transactions"; + private static final String CANCEL_PATH = "cancel"; private static final String ESCALATE_PATH = "escalate"; @@ -649,6 +654,28 @@ public CompletableFuture submitDispute(final String disputeId, ); } + @Override + public CompletableFuture getListTransactions(final TransactionsQuery queryFilter) { + validateTransactionsQuery(queryFilter); + return apiClient.queryAsync( + buildPath(ISSUING_PATH, TRANSACTIONS_PATH), + sdkAuthorization(), + queryFilter, + TransactionsListResponse.class + ); + } + + @Override + public CompletableFuture getSingleTransaction(final String transactionId) { + validateTransactionId(transactionId); + return apiClient.getAsync( + buildPath(ISSUING_PATH, TRANSACTIONS_PATH, transactionId), + sdkAuthorization(), + TransactionsSingleResponse.class + ); + } + + // Synchronous methods @Override public CardholderAccessTokenResponse requestCardholderAccessTokenSync(final CardholderAccessTokenRequest cardholderAccessTokenRequest) { @@ -1179,6 +1206,27 @@ public DisputeResponse submitDisputeSync(final String disputeId, String idempote ); } + @Override + public TransactionsListResponse getListTransactionsSync(final TransactionsQuery queryFilter) { + validateTransactionsQuery(queryFilter); + return apiClient.query( + buildPath(ISSUING_PATH, TRANSACTIONS_PATH), + sdkAuthorization(), + queryFilter, + TransactionsListResponse.class + ); + } + + @Override + public TransactionsSingleResponse getSingleTransactionSync(final String transactionId) { + validateTransactionId(transactionId); + return apiClient.get( + buildPath(ISSUING_PATH, TRANSACTIONS_PATH, transactionId), + sdkAuthorization(), + TransactionsSingleResponse.class + ); + } + // Common methods private UrlEncodedFormEntity createFormUrlEncodedContent(final CardholderAccessTokenRequest cardholderAccessTokenRequest) { try { @@ -1275,4 +1323,12 @@ private void validateDisputeIdAndEscalateRequest(final String disputeId, final E private void validateDisputeIdAndSubmitRequest(final String disputeId, final SubmitDisputeRequest submitDisputeRequest) { validateParams("disputeId", disputeId, "submitDisputeRequest", submitDisputeRequest); } + + private void validateTransactionsQuery(final TransactionsQuery query) { + validateParams("query", query); + } + + private void validateTransactionId(final String transactionId) { + validateParams("transactionId", transactionId); + } } diff --git a/src/main/java/com/checkout/issuing/transactions/entities/DigitalCard.java b/src/main/java/com/checkout/issuing/transactions/entities/DigitalCard.java new file mode 100644 index 00000000..b923057b --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/DigitalCard.java @@ -0,0 +1,11 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class DigitalCard { + + private String id; + + private WalletType walletType; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/Merchant.java b/src/main/java/com/checkout/issuing/transactions/entities/Merchant.java new file mode 100644 index 00000000..cdfbd04f --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/Merchant.java @@ -0,0 +1,20 @@ +package com.checkout.issuing.transactions.entities; + +import com.checkout.common.CountryCode; +import lombok.Data; + +@Data +public class Merchant { + + private String id; + + private String name; + + private String city; + + private String state; + + private CountryCode countryCode; + + private String categoryCode; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/MessageIndicator.java b/src/main/java/com/checkout/issuing/transactions/entities/MessageIndicator.java new file mode 100644 index 00000000..b6a756e4 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/MessageIndicator.java @@ -0,0 +1,35 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum MessageIndicator { + @SerializedName("incremental_preauthorization") + INCREMENTAL_PREAUTHORIZATION, + + @SerializedName("deferred_authorization") + DEFERRED_AUTHORIZATION, + + @SerializedName("preauthorization") + PREAUTHORIZATION, + + @SerializedName("normal_authorization") + NORMAL_AUTHORIZATION, + + @SerializedName("final_authorization") + FINAL_AUTHORIZATION, + + @SerializedName("partial_reversal") + PARTIAL_REVERSAL, + + @SerializedName("full_reversal") + FULL_REVERSAL, + + @SerializedName("partial_presentment") + PARTIAL_PRESENTMENT, + + @SerializedName("final_presentment") + FINAL_PRESENTMENT, + + @SerializedName("unknown") + UNKNOWN +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/MessageInitiator.java b/src/main/java/com/checkout/issuing/transactions/entities/MessageInitiator.java new file mode 100644 index 00000000..d855e939 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/MessageInitiator.java @@ -0,0 +1,20 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum MessageInitiator { + @SerializedName("cardholder") + CARDHOLDER, + + @SerializedName("merchant") + MERCHANT, + + @SerializedName("acquirer") + ACQUIRER, + + @SerializedName("card_network") + CARD_NETWORK, + + @SerializedName("issuer") + ISSUER +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/MessageResult.java b/src/main/java/com/checkout/issuing/transactions/entities/MessageResult.java new file mode 100644 index 00000000..6f076278 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/MessageResult.java @@ -0,0 +1,14 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum MessageResult { + @SerializedName("approved") + APPROVED, + + @SerializedName("declined") + DECLINED, + + @SerializedName("processed") + PROCESSED +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/MessageType.java b/src/main/java/com/checkout/issuing/transactions/entities/MessageType.java new file mode 100644 index 00000000..e37eeb9f --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/MessageType.java @@ -0,0 +1,32 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum MessageType { + @SerializedName("authorization") + AUTHORIZATION, + + @SerializedName("reversal") + REVERSAL, + + @SerializedName("authorization_advice") + AUTHORIZATION_ADVICE, + + @SerializedName("reversal_advice") + REVERSAL_ADVICE, + + @SerializedName("presentment") + PRESENTMENT, + + @SerializedName("authorization_refund") + AUTHORIZATION_REFUND, + + @SerializedName("presentment_refund") + PRESENTMENT_REFUND, + + @SerializedName("presentment_reversed") + PRESENTMENT_REVERSED, + + @SerializedName("presentment_reversed_refund") + PRESENTMENT_REVERSED_REFUND +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/ReferenceTransaction.java b/src/main/java/com/checkout/issuing/transactions/entities/ReferenceTransaction.java new file mode 100644 index 00000000..dc9752d0 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/ReferenceTransaction.java @@ -0,0 +1,11 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class ReferenceTransaction { + + private String transactionId; + + private ReferenceType referenceType; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/ReferenceType.java b/src/main/java/com/checkout/issuing/transactions/entities/ReferenceType.java new file mode 100644 index 00000000..338a5a70 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/ReferenceType.java @@ -0,0 +1,11 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum ReferenceType { + @SerializedName("original_mit") + ORIGINAL_MIT, + + @SerializedName("original_recurring") + ORIGINAL_RECURRING; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmount.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmount.java new file mode 100644 index 00000000..a3b0217a --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmount.java @@ -0,0 +1,11 @@ +package com.checkout.issuing.transactions.entities; + +import java.util.Currency; + +import lombok.Data; + +@Data +public class TransactionAmount { + private Long amount; + private Currency currency; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmounts.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmounts.java new file mode 100644 index 00000000..6b2254ed --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionAmounts.java @@ -0,0 +1,17 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class TransactionAmounts { + + private TransactionAmount totalHeld; + + private TransactionAmount totalAuthorized; + + private TransactionAmount totalReversed; + + private TransactionAmount totalCleared; + + private TransactionAmount totalRefunded; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionCard.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionCard.java new file mode 100644 index 00000000..aa9fa6ef --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionCard.java @@ -0,0 +1,11 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class TransactionCard { + + private String id; + + private String network; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionCardholder.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionCardholder.java new file mode 100644 index 00000000..84ea4ef8 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionCardholder.java @@ -0,0 +1,9 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class TransactionCardholder { + + private String id; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionClient.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionClient.java new file mode 100644 index 00000000..cf661db8 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionClient.java @@ -0,0 +1,9 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class TransactionClient { + + private String id; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionEntity.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionEntity.java new file mode 100644 index 00000000..a7ef2ce5 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionEntity.java @@ -0,0 +1,9 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +@Data +public class TransactionEntity { + + private String id; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionMessage.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionMessage.java new file mode 100644 index 00000000..b37970c8 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionMessage.java @@ -0,0 +1,32 @@ +package com.checkout.issuing.transactions.entities; + +import lombok.Data; + +import java.time.Instant; +import java.util.Currency; + +@Data +public class TransactionMessage { + + private String id; + + private MessageInitiator initiator; + + private MessageType type; + + private MessageResult result; + + private Boolean isRelayed; + + private MessageIndicator indicator; + + private String declineReason; + + private String authorizationCode; + + private Long billingAmount; + + private Currency billingCurrency; + + private Instant createdOn; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionStatus.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionStatus.java new file mode 100644 index 00000000..b9ebc425 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionStatus.java @@ -0,0 +1,23 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum TransactionStatus { + @SerializedName("authorized") + AUTHORIZED, + + @SerializedName("declined") + DECLINED, + + @SerializedName("canceled") + CANCELED, + + @SerializedName("cleared") + CLEARED, + + @SerializedName("refunded") + REFUNDED, + + @SerializedName("disputed") + DISPUTED +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/TransactionType.java b/src/main/java/com/checkout/issuing/transactions/entities/TransactionType.java new file mode 100644 index 00000000..fb1d00a1 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/TransactionType.java @@ -0,0 +1,71 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum TransactionType { + @SerializedName("account_funding") + ACCOUNT_FUNDING, + + @SerializedName("account_transfer") + ACCOUNT_TRANSFER, + + @SerializedName("atm_installment") + ATM_INSTALLMENT, + + @SerializedName("balance_inquiry") + BALANCE_INQUIRY, + + @SerializedName("bill_payment") + BILL_PAYMENT, + + @SerializedName("cash_advance") + CASH_ADVANCE, + + @SerializedName("cashback") + CASHBACK, + + @SerializedName("credit_adjustment") + CREDIT_ADJUSTMENT, + + @SerializedName("debit_adjustment") + DEBIT_ADJUSTMENT, + + @SerializedName("original_credit") + ORIGINAL_CREDIT, + + @SerializedName("payment_account_inquiry") + PAYMENT_ACCOUNT_INQUIRY, + + @SerializedName("payment") + PAYMENT, + + @SerializedName("pin_change") + PIN_CHANGE, + + @SerializedName("pin_unblock") + PIN_UNBLOCK, + + @SerializedName("purchase_account_inquiry") + PURCHASE_ACCOUNT_INQUIRY, + + @SerializedName("purchase") + PURCHASE, + + @SerializedName("quasi_cash") + QUASI_CASH, + + @SerializedName("remittance_funding") + REMITTANCE_FUNDING, + + @SerializedName("remittance_payment") + REMITTANCE_PAYMENT, + + @SerializedName("unknown") + UNKNOWN, + + @SerializedName("withdrawal") + WITHDRAWAL, + + @SerializedName("refund") + REFUND +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/entities/WalletType.java b/src/main/java/com/checkout/issuing/transactions/entities/WalletType.java new file mode 100644 index 00000000..9d3be959 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/entities/WalletType.java @@ -0,0 +1,14 @@ +package com.checkout.issuing.transactions.entities; + +import com.google.gson.annotations.SerializedName; + +public enum WalletType { + @SerializedName("googlepay") + GOOGLEPAY, + + @SerializedName("applepay") + APPLEPAY, + + @SerializedName("remote_commerce_programs") + REMOTE_COMMERCE_PROGRAMS; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/requests/TransactionsQuery.java b/src/main/java/com/checkout/issuing/transactions/requests/TransactionsQuery.java new file mode 100644 index 00000000..dc545d26 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/requests/TransactionsQuery.java @@ -0,0 +1,28 @@ +package com.checkout.issuing.transactions.requests; + +import com.checkout.issuing.transactions.entities.TransactionStatus; +import lombok.Builder; +import lombok.Data; + +import java.time.Instant; + +@Data +@Builder +public class TransactionsQuery { + + private Integer limit; + + private Integer skip; + + private String cardholderId; + + private String cardId; + + private String entityId; + + private TransactionStatus status; + + private Instant from; + + private Instant to; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/responses/TransactionResponse.java b/src/main/java/com/checkout/issuing/transactions/responses/TransactionResponse.java new file mode 100644 index 00000000..33c206a3 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/responses/TransactionResponse.java @@ -0,0 +1,40 @@ +package com.checkout.issuing.transactions.responses; + +import com.checkout.common.Resource; +import com.checkout.issuing.transactions.entities.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TransactionResponse extends Resource { + + private String id; + + private Instant createdOn; + + private TransactionStatus status; + + private TransactionType transactionType; + + private TransactionClient client; + + private TransactionEntity entity; + + private TransactionCard card; + + private DigitalCard digitalCard; + + private TransactionCardholder cardholder; + + private TransactionAmounts amounts; + + private Merchant merchant; + + private ReferenceTransaction referenceTransaction; + + private List messages; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/responses/TransactionsListResponse.java b/src/main/java/com/checkout/issuing/transactions/responses/TransactionsListResponse.java new file mode 100644 index 00000000..356ef9bc --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/responses/TransactionsListResponse.java @@ -0,0 +1,20 @@ +package com.checkout.issuing.transactions.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TransactionsListResponse extends Resource { + + private Integer limit; + + private Integer skip; + + private Integer totalCount; + + private List data; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/transactions/responses/TransactionsSingleResponse.java b/src/main/java/com/checkout/issuing/transactions/responses/TransactionsSingleResponse.java new file mode 100644 index 00000000..b4301f93 --- /dev/null +++ b/src/main/java/com/checkout/issuing/transactions/responses/TransactionsSingleResponse.java @@ -0,0 +1,40 @@ +package com.checkout.issuing.transactions.responses; + +import com.checkout.common.Resource; +import com.checkout.issuing.transactions.entities.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +public class TransactionsSingleResponse extends Resource { + + private String id; + + private Instant createdOn; + + private TransactionStatus status; + + private TransactionType transactionType; + + private TransactionClient client; + + private TransactionEntity entity; + + private TransactionCard card; + + private DigitalCard digitalCard; + + private TransactionCardholder cardholder; + + private TransactionAmounts amounts; + + private Merchant merchant; + + private ReferenceTransaction referenceTransaction; + + private List messages; +} \ No newline at end of file diff --git a/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java b/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java index 4c25a819..81e064c3 100644 --- a/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java +++ b/src/test/java/com/checkout/issuing/BaseIssuingTestIT.java @@ -36,7 +36,8 @@ private CheckoutApi getIssuingCheckoutApi() { requireNonNull(System.getenv("CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID")), requireNonNull(System.getenv("CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET"))) .scopes(OAuthScope.VAULT, OAuthScope.ISSUING_CLIENT, OAuthScope.ISSUING_CARD_MGMT, - OAuthScope.ISSUING_CONTROLS_READ, OAuthScope.ISSUING_CONTROLS_WRITE) + OAuthScope.ISSUING_CONTROLS_READ, OAuthScope.ISSUING_CONTROLS_WRITE, + OAuthScope.ISSUING_TRANSACTIONS_READ, OAuthScope.ISSUING_TRANSACTIONS_WRITE) .environment(Environment.SANDBOX) .build(); } diff --git a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java index 5efec875..26f672a1 100644 --- a/src/test/java/com/checkout/issuing/IssuingClientImplTest.java +++ b/src/test/java/com/checkout/issuing/IssuingClientImplTest.java @@ -50,6 +50,12 @@ import com.checkout.issuing.disputes.requests.EscalateDisputeRequest; import com.checkout.issuing.disputes.requests.SubmitDisputeRequest; import com.checkout.issuing.disputes.responses.DisputeResponse; +import com.checkout.issuing.transactions.requests.TransactionsQuery; +import com.checkout.issuing.transactions.responses.TransactionsListResponse; +import com.checkout.issuing.transactions.responses.TransactionsSingleResponse; +import com.checkout.issuing.transactions.requests.TransactionsQuery; +import com.checkout.issuing.transactions.responses.TransactionsListResponse; +import com.checkout.issuing.transactions.responses.TransactionsSingleResponse; import com.checkout.issuing.testing.requests.CardAuthorizationClearingRequest; import com.checkout.issuing.testing.requests.CardAuthorizationIncrementingRequest; import com.checkout.issuing.testing.requests.CardAuthorizationRefundsRequest; @@ -1654,6 +1660,81 @@ void shouldSubmitDisputeSync() { } } + @Nested + @DisplayName("Transactions") + class Transactions { + @Test + void shouldGetListTransactions() throws ExecutionException, InterruptedException { + final TransactionsQuery query = createTransactionsQuery(); + final TransactionsListResponse expectedResponse = createTransactionsListResponse(); + + when(apiClient.queryAsync( + "issuing/transactions", + authorization, + query, + TransactionsListResponse.class + )).thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = client.getListTransactions(query); + final TransactionsListResponse actualResponse = future.get(); + + validateTransactionsListResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetSingleTransaction() throws ExecutionException, InterruptedException { + final TransactionsSingleResponse expectedResponse = createTransactionsSingleResponse(); + + when(apiClient.getAsync( + "issuing/transactions/transaction_id", + authorization, + TransactionsSingleResponse.class + )).thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = client.getSingleTransaction("transaction_id"); + final TransactionsSingleResponse actualResponse = future.get(); + + validateTransactionsSingleResponse(expectedResponse, actualResponse); + } + } + + // Synchronous methods + @Nested + @DisplayName("Transactions Sync") + class TransactionsSync { + @Test + void shouldGetListTransactionsSync() { + final TransactionsQuery query = createTransactionsQuery(); + final TransactionsListResponse expectedResponse = createTransactionsListResponse(); + + when(apiClient.query( + "issuing/transactions", + authorization, + query, + TransactionsListResponse.class + )).thenReturn(expectedResponse); + + final TransactionsListResponse actualResponse = client.getListTransactionsSync(query); + + validateTransactionsListResponse(expectedResponse, actualResponse); + } + + @Test + void shouldGetSingleTransactionSync() { + final TransactionsSingleResponse expectedResponse = createTransactionsSingleResponse(); + + when(apiClient.get( + "issuing/transactions/transaction_id", + authorization, + TransactionsSingleResponse.class + )).thenReturn(expectedResponse); + + final TransactionsSingleResponse actualResponse = client.getSingleTransactionSync("transaction_id"); + + validateTransactionsSingleResponse(expectedResponse, actualResponse); + } + } + // Common methods private CardholderRequest createCardholderRequest() { return mock(CardholderRequest.class); @@ -2002,4 +2083,26 @@ private void validateDisputeResponse(DisputeResponse expected, DisputeResponse a assertNotNull(actual); assertEquals(expected, actual); } + + private TransactionsQuery createTransactionsQuery() { + return mock(TransactionsQuery.class); + } + + private TransactionsListResponse createTransactionsListResponse() { + return mock(TransactionsListResponse.class); + } + + private TransactionsSingleResponse createTransactionsSingleResponse() { + return mock(TransactionsSingleResponse.class); + } + + private void validateTransactionsListResponse(TransactionsListResponse expected, TransactionsListResponse actual) { + assertNotNull(actual); + assertEquals(expected, actual); + } + + private void validateTransactionsSingleResponse(TransactionsSingleResponse expected, TransactionsSingleResponse actual) { + assertNotNull(actual); + assertEquals(expected, actual); + } } diff --git a/src/test/java/com/checkout/issuing/IssuingTransactionsTestIT.java b/src/test/java/com/checkout/issuing/IssuingTransactionsTestIT.java new file mode 100644 index 00000000..699bfb88 --- /dev/null +++ b/src/test/java/com/checkout/issuing/IssuingTransactionsTestIT.java @@ -0,0 +1,102 @@ +package com.checkout.issuing; + +import com.checkout.issuing.cardholders.CardholderResponse; +import com.checkout.issuing.cards.responses.CardResponse; +import com.checkout.issuing.transactions.requests.TransactionsQuery; +import com.checkout.issuing.transactions.responses.TransactionsListResponse; +import com.checkout.issuing.transactions.responses.TransactionsSingleResponse; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class IssuingTransactionsTestIT extends BaseIssuingTestIT { + + private CardholderResponse cardholder; + private CardResponse card; + + @BeforeAll + void setUp() { + cardholder = createCardholder(); + card = createCard(cardholder.getId()); + } + + @Test + void shouldGetListTransactions() { + final TransactionsQuery query = createTransactionsQuery(); + + final TransactionsListResponse response = blocking(() -> + issuingApi.issuingClient().getListTransactions(query)); + + validateTransactionsListResponse(response); + } + + @Test + void shouldGetSingleTransaction() { + // First get transactions to have a valid transaction ID + final TransactionsQuery query = createTransactionsQuery(); + final TransactionsListResponse listResponse = blocking(() -> + issuingApi.issuingClient().getListTransactions(query)); + + if (listResponse.getData() != null && !listResponse.getData().isEmpty()) { + final String transactionId = listResponse.getData().get(0).getId(); + + final TransactionsSingleResponse response = blocking(() -> + issuingApi.issuingClient().getSingleTransaction(transactionId)); + + validateTransactionsSingleResponse(response, transactionId); + } + } + + // Synchronous methods + @Test + void shouldGetListTransactionsSync() { + final TransactionsQuery query = createTransactionsQuery(); + + final TransactionsListResponse response = issuingApi.issuingClient().getListTransactionsSync(query); + + validateTransactionsListResponse(response); + } + + @Test + void shouldGetSingleTransactionSync() { + // First get transactions to have a valid transaction ID + final TransactionsQuery query = createTransactionsQuery(); + final TransactionsListResponse listResponse = issuingApi.issuingClient().getListTransactionsSync(query); + + if (listResponse.getData() != null && !listResponse.getData().isEmpty()) { + final String transactionId = listResponse.getData().get(0).getId(); + + final TransactionsSingleResponse response = issuingApi.issuingClient().getSingleTransactionSync(transactionId); + + validateTransactionsSingleResponse(response, transactionId); + } + } + + // Common methods + private TransactionsQuery createTransactionsQuery() { + return TransactionsQuery.builder() + .cardId(card.getId()) + .limit(10) + .build(); + } + + private void validateTransactionsListResponse(TransactionsListResponse response) { + assertNotNull(response); + assertNotNull(response.getLimit()); + assertNotNull(response.getSkip()); + assertNotNull(response.getTotalCount()); + assertNotNull(response.getData()); + } + + private void validateTransactionsSingleResponse(TransactionsSingleResponse response, String expectedTransactionId) { + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getTransactionType()); + assertNotNull(response.getStatus()); + assertNotNull(response.getAmounts()); + assertNotNull(response.getCreatedOn()); + } +} \ No newline at end of file From 052c5b44b7bc398d5a66475481fe1c71b3180bcf Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 11 Mar 2026 10:52:28 +0100 Subject: [PATCH 09/19] New payments/search endpoint + unit and integration tests --- src/main/java/com/checkout/OAuthScope.java | 1 + .../com/checkout/payments/PaymentStatus.java | 2 + .../com/checkout/payments/PaymentsClient.java | 6 ++ .../checkout/payments/PaymentsClientImpl.java | 19 ++++ .../request/PaymentSearchRequest.java | 40 ++++++++ .../payments/response/GetPaymentResponse.java | 9 ++ .../response/PaymentSearchResponse.java | 21 +++++ .../response/source/CardResponseSource.java | 8 +- .../java/com/checkout/SandboxTestFixture.java | 2 +- .../payments/PaymentSearchTestIT.java | 91 +++++++++++++++++++ .../payments/PaymentsClientImplTest.java | 40 ++++++++ .../payments/RequestPaymentsTestIT.java | 4 +- 12 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/checkout/payments/request/PaymentSearchRequest.java create mode 100644 src/main/java/com/checkout/payments/response/PaymentSearchResponse.java create mode 100644 src/test/java/com/checkout/payments/PaymentSearchTestIT.java diff --git a/src/main/java/com/checkout/OAuthScope.java b/src/main/java/com/checkout/OAuthScope.java index 53911ef9..3c53ca69 100644 --- a/src/main/java/com/checkout/OAuthScope.java +++ b/src/main/java/com/checkout/OAuthScope.java @@ -42,6 +42,7 @@ public enum OAuthScope { MIDDLEWARE_MERCHANTS_SECRET("middleware:merchants-secret"), PAYMENT_CONTEXTS("gateway:payment-contexts"), PAYMENT_SESSIONS("payment-sessions"), + PAYMENTS_SEARCH("payments:search"), PAYOUTS_BANK_DETAILS("payouts:bank-details"), REPORTS("reports"), REPORTS_VIEW("reports:view"), diff --git a/src/main/java/com/checkout/payments/PaymentStatus.java b/src/main/java/com/checkout/payments/PaymentStatus.java index 0e2ab500..7ac63c94 100644 --- a/src/main/java/com/checkout/payments/PaymentStatus.java +++ b/src/main/java/com/checkout/payments/PaymentStatus.java @@ -32,6 +32,8 @@ public enum PaymentStatus { PARTIALLY_REFUNDED, @SerializedName("Refunded") REFUNDED, + @SerializedName("Authentication Requested") + AUTHENTICATION_REQUESTED, } diff --git a/src/main/java/com/checkout/payments/PaymentsClient.java b/src/main/java/com/checkout/payments/PaymentsClient.java index 135631aa..27291ecd 100644 --- a/src/main/java/com/checkout/payments/PaymentsClient.java +++ b/src/main/java/com/checkout/payments/PaymentsClient.java @@ -7,10 +7,12 @@ import com.checkout.handlepaymentsandpayouts.payments.postpayments.responses.RequestAPaymentOrPayoutResponse; import com.checkout.payments.request.AuthorizationRequest; import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.request.PaymentSearchRequest; import com.checkout.payments.request.PayoutRequest; import com.checkout.payments.response.AuthorizationResponse; import com.checkout.payments.response.GetPaymentResponse; import com.checkout.payments.response.PaymentResponse; +import com.checkout.payments.response.PaymentSearchResponse; import com.checkout.payments.response.PaymentsQueryResponse; import com.checkout.payments.response.PayoutResponse; @@ -70,6 +72,8 @@ public interface PaymentsClient { CompletableFuture voidPayment(String paymentId, VoidRequest voidRequest, String idempotencyKey); + CompletableFuture searchPayments(PaymentSearchRequest paymentSearchRequest); + // Synchronous methods PaymentResponse requestPaymentSync(PaymentRequest paymentRequest); @@ -125,4 +129,6 @@ public interface PaymentsClient { VoidResponse voidPaymentSync(String paymentId, VoidRequest voidRequest, String idempotencyKey); + PaymentSearchResponse searchPaymentsSync(PaymentSearchRequest paymentSearchRequest); + } \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/PaymentsClientImpl.java b/src/main/java/com/checkout/payments/PaymentsClientImpl.java index 790dbce4..dce7aae3 100644 --- a/src/main/java/com/checkout/payments/PaymentsClientImpl.java +++ b/src/main/java/com/checkout/payments/PaymentsClientImpl.java @@ -20,10 +20,12 @@ import com.checkout.handlepaymentsandpayouts.payments.postpayments.responses.requestapaymentorpayoutresponsecreated.RequestAPaymentOrPayoutResponseCreated; import com.checkout.payments.request.AuthorizationRequest; import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.request.PaymentSearchRequest; import com.checkout.payments.request.PayoutRequest; import com.checkout.payments.response.AuthorizationResponse; import com.checkout.payments.response.GetPaymentResponse; import com.checkout.payments.response.PaymentResponse; +import com.checkout.payments.response.PaymentSearchResponse; import com.checkout.payments.response.PaymentsQueryResponse; import com.checkout.payments.response.PayoutResponse; import com.google.gson.reflect.TypeToken; @@ -31,6 +33,7 @@ public final class PaymentsClientImpl extends AbstractClient implements PaymentsClient { private static final String PAYMENTS_PATH = "payments"; + private static final String SEARCH_PATH = "search"; private static final String ACTIONS_PATH = "actions"; private static final String CAPTURES_PATH = "captures"; private static final String AUTHORIZATIONS_PATH = "authorizations"; @@ -226,6 +229,12 @@ public CompletableFuture voidPayment(final String paymentId, final return apiClient.postAsync(buildPath(PAYMENTS_PATH, paymentId, VOIDS_PATH), sdkAuthorization(), VoidResponse.class, voidRequest, idempotencyKey); } + @Override + public CompletableFuture searchPayments(final PaymentSearchRequest paymentSearchRequest) { + validatePaymentSearchRequest(paymentSearchRequest); + return apiClient.postAsync(buildPath(PAYMENTS_PATH, SEARCH_PATH), sdkAuthorization(), PaymentSearchResponse.class, paymentSearchRequest, null); + } + // Synchronous methods @Override public PaymentResponse requestPaymentSync(final PaymentRequest paymentRequest) { @@ -400,6 +409,12 @@ public VoidResponse voidPaymentSync(final String paymentId, final VoidRequest vo return apiClient.post(buildPath(PAYMENTS_PATH, paymentId, VOIDS_PATH), sdkAuthorization(), VoidResponse.class, voidRequest, idempotencyKey); } + @Override + public PaymentSearchResponse searchPaymentsSync(final PaymentSearchRequest paymentSearchRequest) { + validatePaymentSearchRequest(paymentSearchRequest); + return apiClient.post(buildPath(PAYMENTS_PATH, SEARCH_PATH), sdkAuthorization(), PaymentSearchResponse.class, paymentSearchRequest, null); + } + // Common methods protected void validatePaymentRequest(final PaymentRequest paymentRequest) { validateParams("paymentRequest", paymentRequest); @@ -477,4 +492,8 @@ protected void validatePaymentIdVoidRequestAndIdempotencyKey(final String paymen validateParams("paymentId", paymentId, "voidRequest", voidRequest, "idempotencyKey", idempotencyKey); } + protected void validatePaymentSearchRequest(final PaymentSearchRequest paymentSearchRequest) { + validateParams("paymentSearchRequest", paymentSearchRequest); + } + } diff --git a/src/main/java/com/checkout/payments/request/PaymentSearchRequest.java b/src/main/java/com/checkout/payments/request/PaymentSearchRequest.java new file mode 100644 index 00000000..8904706e --- /dev/null +++ b/src/main/java/com/checkout/payments/request/PaymentSearchRequest.java @@ -0,0 +1,40 @@ +package com.checkout.payments.request; + +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.Size; +import java.time.Instant; + +@Data +@Builder +public final class PaymentSearchRequest { + + /** + * The query string. + * For more information on how to build out your query, see the Search and filter payments documentation. + */ + @Size(max = 1024) + private String query; + + /** + * The number of results to return per page. + */ + @Min(1) + @Max(1000) + private Integer limit; + + /** + * The UTC date and time for the query start in ISO 8601 format. + * Required if to is provided. + */ + private Instant from; + + /** + * The UTC date and time for the query end in ISO 8601 format. + * Required if from is provided. + */ + private Instant to; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/response/GetPaymentResponse.java b/src/main/java/com/checkout/payments/response/GetPaymentResponse.java index ffe89e2e..4c0fc57a 100644 --- a/src/main/java/com/checkout/payments/response/GetPaymentResponse.java +++ b/src/main/java/com/checkout/payments/response/GetPaymentResponse.java @@ -36,6 +36,12 @@ public final class GetPaymentResponse extends Resource { private String id; + @SerializedName("authentication_id") + private String authenticationId; + + @SerializedName("processing_channel_id") + private String processingChannelId; + @SerializedName("requested_on") private Instant requestedOn; @@ -47,6 +53,9 @@ public final class GetPaymentResponse extends Resource { private Long amount; + @SerializedName("amount_requested") + private Long amountRequested; + private Currency currency; @SerializedName("payment_type") diff --git a/src/main/java/com/checkout/payments/response/PaymentSearchResponse.java b/src/main/java/com/checkout/payments/response/PaymentSearchResponse.java new file mode 100644 index 00000000..e35c8af5 --- /dev/null +++ b/src/main/java/com/checkout/payments/response/PaymentSearchResponse.java @@ -0,0 +1,21 @@ +package com.checkout.payments.response; + +import com.checkout.common.Resource; + +import lombok.Data; +import lombok.Builder; +import lombok.EqualsAndHashCode; + +import java.util.List; + +@Data +@Builder +@EqualsAndHashCode(callSuper = true) +public final class PaymentSearchResponse extends Resource { + + /** + * Array of payment objects matching the search query. + * Can contain Payin, BankPayout, or CardPayout responses. + */ + private List data; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java index 7d107a89..57c18d2b 100644 --- a/src/main/java/com/checkout/payments/response/source/CardResponseSource.java +++ b/src/main/java/com/checkout/payments/response/source/CardResponseSource.java @@ -26,11 +26,15 @@ public final class CardResponseSource extends AbstractResponseSource implements private Phone phone; + // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization + // issues if it is set to Instant @SerializedName("expiry_month") - private Integer expiryMonth; + private String expiryMonth; + // This is set explicitly to String because the API mask the response with "****" and this will cause deserialization + // issues if it is set to Instant @SerializedName("expiry_year") - private Integer expiryYear; + private String expiryYear; private String name; diff --git a/src/test/java/com/checkout/SandboxTestFixture.java b/src/test/java/com/checkout/SandboxTestFixture.java index b7eee6f0..a88588b4 100644 --- a/src/test/java/com/checkout/SandboxTestFixture.java +++ b/src/test/java/com/checkout/SandboxTestFixture.java @@ -90,7 +90,7 @@ public SandboxTestFixture(final PlatformType platformType) { OAuthScope.VAULT, OAuthScope.PAYOUTS_BANK_DETAILS, OAuthScope.DISPUTES, OAuthScope.TRANSFERS_CREATE, OAuthScope.TRANSFERS_VIEW, OAuthScope.BALANCES_VIEW, OAuthScope.VAULT_CARD_METADATA, OAuthScope.FINANCIAL_ACTIONS, OAuthScope.FORWARD, - OAuthScope.FORWARD_SECRETS) + OAuthScope.FORWARD_SECRETS, OAuthScope.PAYMENTS_SEARCH) .environment(Environment.SANDBOX) .executor(CUSTOM_EXECUTOR) .build(); diff --git a/src/test/java/com/checkout/payments/PaymentSearchTestIT.java b/src/test/java/com/checkout/payments/PaymentSearchTestIT.java new file mode 100644 index 00000000..d63a4ba4 --- /dev/null +++ b/src/test/java/com/checkout/payments/PaymentSearchTestIT.java @@ -0,0 +1,91 @@ +package com.checkout.payments; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.checkout.PlatformType; +import com.checkout.payments.request.PaymentSearchRequest; +import com.checkout.payments.response.PaymentResponse; +import com.checkout.payments.response.PaymentSearchResponse; + +public class PaymentSearchTestIT extends AbstractPaymentsTestIT { + + public PaymentSearchTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Disabled("Avoid because can create timeout in the pipeline, activate when needed") + @Test + void shouldSearchPayments() throws InterruptedException { + final PaymentResponse payment = makeCardPayment(true); + final PaymentSearchRequest request = createPaymentSearchRequest(payment.getId()); + + final PaymentSearchResponse response = retryUntilSearchHasResults(() -> + blocking(() -> checkoutApi.paymentsClient().searchPayments(request))); + + validatePaymentSearchResponse(response, payment); + } + + // Synchronous methods + @Disabled("Avoid because can create timeout in the pipeline, activate when needed") + @Test + void shouldSearchPaymentsSync() throws InterruptedException { + final PaymentResponse payment = makeCardPayment(true); + final PaymentSearchRequest request = createPaymentSearchRequest(payment.getId()); + + final PaymentSearchResponse response = retryUntilSearchHasResults(() -> + checkoutApi.paymentsClient().searchPaymentsSync(request)); + + validatePaymentSearchResponse(response, payment); + } + + // Common methods + private PaymentSearchRequest createPaymentSearchRequest(String paymentId) { + final Instant now = Instant.now(); + return PaymentSearchRequest + .builder() + .query("id:'" + paymentId + "'") + .limit(10) + .from(now.minus(5, ChronoUnit.MINUTES)) + .to(now.plus(5, ChronoUnit.MINUTES)) + .build(); + } + + private PaymentSearchResponse retryUntilSearchHasResults(java.util.function.Supplier searchCall) + throws InterruptedException { + PaymentSearchResponse response = null; + int attempts = 0; + final int maxAttempts = 10; + final long delayMs = 2000; + + while (attempts < maxAttempts) { + Thread.sleep(delayMs); + response = searchCall.get(); + if (searchHasResults(response)) { + return response; + } + attempts++; + } + + return response; // return last response even if no results + } + + private static boolean searchHasResults(PaymentSearchResponse response) { + return response != null && response.getData() != null && !response.getData().isEmpty(); + } + + private void validatePaymentSearchResponse(PaymentSearchResponse response, PaymentResponse expectedPayment) { + assertNotNull(response); + assertNotNull(response.getData()); + assertTrue(response.getData().size() > 0); + assertEquals(expectedPayment.getId(), response.getData().get(0).getId()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/payments/PaymentsClientImplTest.java b/src/test/java/com/checkout/payments/PaymentsClientImplTest.java index 79f21268..d442a90f 100644 --- a/src/test/java/com/checkout/payments/PaymentsClientImplTest.java +++ b/src/test/java/com/checkout/payments/PaymentsClientImplTest.java @@ -33,6 +33,7 @@ import com.checkout.common.Phone; import com.checkout.payments.request.AuthorizationRequest; import com.checkout.payments.request.PaymentRequest; +import com.checkout.payments.request.PaymentSearchRequest; import com.checkout.payments.request.PayoutRequest; import com.checkout.payments.request.source.AbstractRequestSource; import com.checkout.payments.request.source.PayoutRequestCurrencyAccountSource; @@ -45,6 +46,7 @@ import com.checkout.payments.response.AuthorizationResponse; import com.checkout.payments.response.GetPaymentResponse; import com.checkout.payments.response.PaymentResponse; +import com.checkout.payments.response.PaymentSearchResponse; import com.checkout.payments.response.PaymentsQueryResponse; import com.checkout.payments.response.PayoutResponse; import com.checkout.payments.sender.PaymentInstrumentSender; @@ -496,6 +498,20 @@ void shouldRequestBankAccountPayment() throws ExecutionException, InterruptedExc validateResponse(response, actualResponse); } + @Test + void shouldSearchPayments() throws ExecutionException, InterruptedException { + final PaymentSearchRequest request = createPaymentSearchRequest(); + final PaymentSearchResponse response = createPaymentSearchResponse(); + + when(apiClient.postAsync(eq("payments/search"), any(SdkAuthorization.class), eq(PaymentSearchResponse.class), eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = paymentsClient.searchPayments(request); + final PaymentSearchResponse actualResponse = future.get(); + + validateResponse(response, actualResponse); + } + // Synchronous methods @Test void shouldRequestPaymentSync() { @@ -870,6 +886,19 @@ void shouldRequestBankAccountPaymentSync() { validateResponse(expectedResponse, actualResponse); } + @Test + void shouldSearchPaymentsSync() { + final PaymentSearchRequest request = createPaymentSearchRequest(); + final PaymentSearchResponse expectedResponse = createPaymentSearchResponse(); + + when(apiClient.post(eq("payments/search"), any(SdkAuthorization.class), eq(PaymentSearchResponse.class), eq(request), isNull())) + .thenReturn(expectedResponse); + + final PaymentSearchResponse actualResponse = paymentsClient.searchPaymentsSync(request); + + validateResponse(expectedResponse, actualResponse); + } + // Common methods private RequestIdSource createIdSource() { return mock(RequestIdSource.class); @@ -1020,6 +1049,17 @@ private ItemsResponse createPaymentActionsResponse() { return (ItemsResponse) mock(ItemsResponse.class); } + private PaymentSearchRequest createPaymentSearchRequest() { + return PaymentSearchRequest.builder() + .query("amount:100") + .limit(10) + .build(); + } + + private PaymentSearchResponse createPaymentSearchResponse() { + return mock(PaymentSearchResponse.class); + } + private void validateResponse(T expectedResponse, T actualResponse) { assertEquals(expectedResponse, actualResponse); assertNotNull(actualResponse); diff --git a/src/test/java/com/checkout/payments/RequestPaymentsTestIT.java b/src/test/java/com/checkout/payments/RequestPaymentsTestIT.java index 04525718..de1a58c6 100644 --- a/src/test/java/com/checkout/payments/RequestPaymentsTestIT.java +++ b/src/test/java/com/checkout/payments/RequestPaymentsTestIT.java @@ -506,8 +506,8 @@ private void validateCardSource(PaymentResponse paymentResponse) { final CardResponseSource responseCardSource = (CardResponseSource) paymentResponse.getSource(); assertNotNull(responseCardSource); assertEquals(PaymentSourceType.CARD, responseCardSource.getType()); - assertEquals(CardSourceHelper.Visa.EXPIRY_MONTH, (int) responseCardSource.getExpiryMonth()); - assertEquals(CardSourceHelper.Visa.EXPIRY_YEAR, (int) responseCardSource.getExpiryYear()); + assertEquals(String.valueOf(CardSourceHelper.Visa.EXPIRY_MONTH), responseCardSource.getExpiryMonth()); + assertEquals(String.valueOf(CardSourceHelper.Visa.EXPIRY_YEAR), responseCardSource.getExpiryYear()); assertEquals("Visa", responseCardSource.getScheme()); } From c6c5cef627567ce7c43394341a107e623681a21c Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 11 Mar 2026 12:38:40 +0100 Subject: [PATCH 10/19] - PaymentMethodType generic enum in common. - New paymentsmethods client with the new endpoint + unit and integration tests --- src/main/java/com/checkout/CheckoutApi.java | 3 + .../java/com/checkout/CheckoutApiImpl.java | 9 ++ .../checkout/common/PaymentMethodType.java | 129 ++++++++++++++++ .../flow/entities/PaymentMethod.java | 111 -------------- .../paymentmethods/PaymentMethodsClient.java | 23 +++ .../PaymentMethodsClientImpl.java | 43 ++++++ .../entities/PaymentMethod.java | 25 ++++ .../requests/PaymentMethodsQuery.java | 16 ++ .../responses/PaymentMethodsResponse.java | 21 +++ .../PaymentMethodsClientImplTest.java | 141 ++++++++++++++++++ .../paymentmethods/PaymentMethodsTestIT.java | 56 +++++++ 11 files changed, 466 insertions(+), 111 deletions(-) create mode 100644 src/main/java/com/checkout/common/PaymentMethodType.java delete mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/flow/entities/PaymentMethod.java create mode 100644 src/main/java/com/checkout/paymentmethods/PaymentMethodsClient.java create mode 100644 src/main/java/com/checkout/paymentmethods/PaymentMethodsClientImpl.java create mode 100644 src/main/java/com/checkout/paymentmethods/entities/PaymentMethod.java create mode 100644 src/main/java/com/checkout/paymentmethods/requests/PaymentMethodsQuery.java create mode 100644 src/main/java/com/checkout/paymentmethods/responses/PaymentMethodsResponse.java create mode 100644 src/test/java/com/checkout/paymentmethods/PaymentMethodsClientImplTest.java create mode 100644 src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index aadbbebd..b9cc8d72 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -18,6 +18,7 @@ import com.checkout.issuing.IssuingClient; import com.checkout.metadata.MetadataClient; import com.checkout.networktokens.NetworkTokensClient; +import com.checkout.paymentmethods.PaymentMethodsClient; import com.checkout.payments.PaymentsClient; import com.checkout.payments.contexts.PaymentContextsClient; import com.checkout.payments.hosted.HostedPaymentsClient; @@ -53,6 +54,8 @@ public interface CheckoutApi extends CheckoutApmApi { PaymentLinksClient paymentLinksClient(); + PaymentMethodsClient paymentMethodsClient(); + HostedPaymentsClient hostedPaymentsClient(); BalancesClient balancesClient(); diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index b966206c..f3deea04 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -36,6 +36,8 @@ import com.checkout.metadata.MetadataClientImpl; import com.checkout.networktokens.NetworkTokensClient; import com.checkout.networktokens.NetworkTokensClientImpl; +import com.checkout.paymentmethods.PaymentMethodsClient; +import com.checkout.paymentmethods.PaymentMethodsClientImpl; import com.checkout.payments.PaymentsClient; import com.checkout.payments.PaymentsClientImpl; import com.checkout.payments.contexts.PaymentContextsClient; @@ -72,6 +74,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final SessionsClient sessionsClient; private final ForexClient forexClient; private final PaymentLinksClient paymentLinksClient; + private final PaymentMethodsClient paymentMethodsClient; private final HostedPaymentsClient hostedPaymentsClient; private final TransfersClient transfersClient; private final BalancesClient balancesClient; @@ -102,6 +105,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.sessionsClient = new SessionsClientImpl(this.apiClient, configuration); this.forexClient = new ForexClientImpl(this.apiClient, configuration); this.paymentLinksClient = new PaymentLinksClientImpl(this.apiClient, configuration); + this.paymentMethodsClient = new PaymentMethodsClientImpl(this.apiClient, configuration); this.hostedPaymentsClient = new HostedPaymentsClientImpl(this.apiClient, configuration); this.transfersClient = new TransfersClientImpl(getTransfersClient(configuration), configuration); this.balancesClient = new BalancesClientImpl(getBalancesClient(configuration), configuration); @@ -179,6 +183,11 @@ public PaymentLinksClient paymentLinksClient() { return paymentLinksClient; } + @Override + public PaymentMethodsClient paymentMethodsClient() { + return paymentMethodsClient; + } + @Override public HostedPaymentsClient hostedPaymentsClient() { return hostedPaymentsClient; diff --git a/src/main/java/com/checkout/common/PaymentMethodType.java b/src/main/java/com/checkout/common/PaymentMethodType.java new file mode 100644 index 00000000..5299aa3e --- /dev/null +++ b/src/main/java/com/checkout/common/PaymentMethodType.java @@ -0,0 +1,129 @@ +package com.checkout.common; + +import com.google.gson.annotations.SerializedName; + +/** + * Comprehensive enum for all payment method types across different APIs + * Consolidates values from payment methods, flow APIs, and other payment contexts + */ +public enum PaymentMethodType { + + @SerializedName("accel") + ACCEL, + @SerializedName("ach") + ACH, + @SerializedName("alipay_cn") + ALIPAY_CN, + @SerializedName("alipay_hk") + ALIPAY_HK, + @SerializedName("alipay_plus") + ALIPAY_PLUS, + @SerializedName("alma") + ALMA, + @SerializedName("amex") + AMEX, + @SerializedName("applepay") + APPLEPAY, + @SerializedName("bancontact") + BANCONTACT, + @SerializedName("benefit") + BENEFIT, + @SerializedName("boost") + BOOST, + @SerializedName("bpi") + BPI, + @SerializedName("card") + CARD, + @SerializedName("cartes_bancaires") + CARTES_BANCAIRES, + @SerializedName("china_union_pay") + CHINA_UNION_PAY, + @SerializedName("connect_wallet") + CONNECT_WALLET, + @SerializedName("dana") + DANA, + @SerializedName("dci") + DCI, + @SerializedName("diners") + DINERS, + @SerializedName("discover") + DISCOVER, + @SerializedName("eps") + EPS, + @SerializedName("gcash") + GCASH, + @SerializedName("googlepay") + GOOGLEPAY, + @SerializedName("ideal") + IDEAL, + @SerializedName("jcb") + JCB, + @SerializedName("kakaopay") + KAKAOPAY, + @SerializedName("klarna") + KLARNA, + @SerializedName("knet") + KNET, + @SerializedName("mada") + MADA, + @SerializedName("mastercard") + MASTERCARD, + @SerializedName("mbway") + MBWAY, + @SerializedName("multibanco") + MULTIBANCO, + @SerializedName("nyce") + NYCE, + @SerializedName("omannet") + OMANNET, + @SerializedName("p24") + P24, + @SerializedName("paypal") + PAYPAL, + @SerializedName("paypay") + PAYPAY, + @SerializedName("pulse") + PULSE, + @SerializedName("qpay") + QPAY, + @SerializedName("rabbit_line_pay") + RABBIT_LINE_PAY, + @SerializedName("sepa") + SEPA, + @SerializedName("sequra") + SEQURA, + @SerializedName("shazam") + SHAZAM, + @SerializedName("sofort") + SOFORT, + @SerializedName("star") + STAR, + @SerializedName("stcpay") + STCPAY, + @SerializedName("swish") + SWISH, + @SerializedName("tabby") + TABBY, + @SerializedName("tamara") + TAMARA, + @SerializedName("tng") + TNG, + @SerializedName("truemoney") + TRUEMONEY, + @SerializedName("upi") + UPI, + @SerializedName("visa") + VISA, + @SerializedName("wechatpay") + WECHATPAY, + + // Payment method categories + @SerializedName("card_scheme") + CARD_SCHEME, + @SerializedName("bank_redirects") + BANK_REDIRECTS, + @SerializedName("wallet") + WALLET, + @SerializedName("bnpl") + BNPL +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/entities/PaymentMethod.java b/src/main/java/com/checkout/handlepaymentsandpayouts/flow/entities/PaymentMethod.java deleted file mode 100644 index 28356354..00000000 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/entities/PaymentMethod.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.checkout.handlepaymentsandpayouts.flow.entities; - -import com.google.gson.annotations.SerializedName; - -public enum PaymentMethod { - @SerializedName("alipay_cn") - ALIPAY_CN, - - @SerializedName("alipay_hk") - ALIPAY_HK, - - @SerializedName("alma") - ALMA, - - @SerializedName("applepay") - APPLEPAY, - - @SerializedName("bancontact") - BANCONTACT, - - @SerializedName("benefit") - BENEFIT, - - @SerializedName("bizum") - BIZUM, - - @SerializedName("card") - CARD, - - @SerializedName("dana") - DANA, - - @SerializedName("eps") - EPS, - - @SerializedName("gcash") - GCASH, - - @SerializedName("googlepay") - GOOGLEPAY, - - @SerializedName("ideal") - IDEAL, - - @SerializedName("kakaopay") - KAKAOPAY, - - @SerializedName("klarna") - KLARNA, - - @SerializedName("knet") - KNET, - - @SerializedName("mbway") - MBWAY, - - @SerializedName("mobilepay") - MOBILEPAY, - - @SerializedName("multibanco") - MULTIBANCO, - - @SerializedName("octopus") - OCTOPUS, - - @SerializedName("p24") - P24, - - @SerializedName("paynow") - PAYNOW, - - @SerializedName("paypal") - PAYPAL, - - @SerializedName("plaid") - PLAID, - - @SerializedName("qpay") - QPAY, - - REMEMBER_ME, - - @SerializedName("sepa") - SEPA, - - @SerializedName("stcpay") - STCPAY, - - STORED_CARD, - - @SerializedName("tabby") - TABBY, - - @SerializedName("tamara") - TAMARA, - - @SerializedName("tng") - TNG, - - @SerializedName("truemoney") - TRUEMONEY, - - @SerializedName("twint") - TWINT, - - @SerializedName("vipps") - VIPPS, - - @SerializedName("wechatpay") - WECHATPAY -} \ No newline at end of file diff --git a/src/main/java/com/checkout/paymentmethods/PaymentMethodsClient.java b/src/main/java/com/checkout/paymentmethods/PaymentMethodsClient.java new file mode 100644 index 00000000..9af6284d --- /dev/null +++ b/src/main/java/com/checkout/paymentmethods/PaymentMethodsClient.java @@ -0,0 +1,23 @@ +package com.checkout.paymentmethods; + +import java.util.concurrent.CompletableFuture; + +import com.checkout.paymentmethods.requests.PaymentMethodsQuery; +import com.checkout.paymentmethods.responses.PaymentMethodsResponse; + +public interface PaymentMethodsClient { + + /** + * Get available payment methods + * Beta + * Get a list of all available payment methods for a specific Processing Channel ID. + * + * @param paymentMethodsQuery The query parameters including processing_channel_id + * @return CompletableFuture containing the payment methods response + */ + CompletableFuture getPaymentMethods(PaymentMethodsQuery paymentMethodsQuery); + + // Synchronous methods + PaymentMethodsResponse getPaymentMethodsSync(PaymentMethodsQuery paymentMethodsQuery); + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/paymentmethods/PaymentMethodsClientImpl.java b/src/main/java/com/checkout/paymentmethods/PaymentMethodsClientImpl.java new file mode 100644 index 00000000..88cf038c --- /dev/null +++ b/src/main/java/com/checkout/paymentmethods/PaymentMethodsClientImpl.java @@ -0,0 +1,43 @@ +package com.checkout.paymentmethods; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorizationType; +import com.checkout.common.CheckoutUtils; +import com.checkout.paymentmethods.requests.PaymentMethodsQuery; +import com.checkout.paymentmethods.responses.PaymentMethodsResponse; + +import java.util.concurrent.CompletableFuture; + +public class PaymentMethodsClientImpl extends AbstractClient implements PaymentMethodsClient { + + private static final String PAYMENT_METHODS_PATH = "payment-methods"; + + public PaymentMethodsClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); + } + + @Override + public CompletableFuture getPaymentMethods(final PaymentMethodsQuery paymentMethodsQuery) { + validatePaymentMethodsQuery(paymentMethodsQuery); + return apiClient.queryAsync(PAYMENT_METHODS_PATH, sdkAuthorization(), paymentMethodsQuery, PaymentMethodsResponse.class); + } + + // Synchronous methods + @Override + public PaymentMethodsResponse getPaymentMethodsSync(final PaymentMethodsQuery paymentMethodsQuery) { + validatePaymentMethodsQuery(paymentMethodsQuery); + return apiClient.query(PAYMENT_METHODS_PATH, sdkAuthorization(), paymentMethodsQuery, PaymentMethodsResponse.class); + } + + // Common methods + private void validatePaymentMethodsQuery(final PaymentMethodsQuery paymentMethodsQuery) { + CheckoutUtils.validateParams("paymentMethodsQuery", paymentMethodsQuery); + CheckoutUtils.validateParams("processingChannelId", paymentMethodsQuery.getProcessingChannelId()); + + if (!paymentMethodsQuery.getProcessingChannelId().matches("^(pc)_(\\w{26})$")) { + throw new IllegalArgumentException("processingChannelId must match pattern ^(pc)_(\\w{26})$"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/paymentmethods/entities/PaymentMethod.java b/src/main/java/com/checkout/paymentmethods/entities/PaymentMethod.java new file mode 100644 index 00000000..aa370e54 --- /dev/null +++ b/src/main/java/com/checkout/paymentmethods/entities/PaymentMethod.java @@ -0,0 +1,25 @@ +package com.checkout.paymentmethods.entities; + +import com.checkout.common.PaymentMethodType; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Data +@EqualsAndHashCode +@ToString +@NoArgsConstructor +public final class PaymentMethod { + + private PaymentMethodType type; + + private String name; + + /** + * The unique identifier for the merchant, provided by the partner. + */ + @SerializedName("partner_merchant_id") + private String partnerMerchantId; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/paymentmethods/requests/PaymentMethodsQuery.java b/src/main/java/com/checkout/paymentmethods/requests/PaymentMethodsQuery.java new file mode 100644 index 00000000..389a1218 --- /dev/null +++ b/src/main/java/com/checkout/paymentmethods/requests/PaymentMethodsQuery.java @@ -0,0 +1,16 @@ +package com.checkout.paymentmethods.requests; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public final class PaymentMethodsQuery { + + private String processingChannelId; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/paymentmethods/responses/PaymentMethodsResponse.java b/src/main/java/com/checkout/paymentmethods/responses/PaymentMethodsResponse.java new file mode 100644 index 00000000..44fc4082 --- /dev/null +++ b/src/main/java/com/checkout/paymentmethods/responses/PaymentMethodsResponse.java @@ -0,0 +1,21 @@ +package com.checkout.paymentmethods.responses; + +import com.checkout.common.Resource; +import com.checkout.paymentmethods.entities.PaymentMethod; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class PaymentMethodsResponse extends Resource { + + /** + * The enabled payment methods for the processing channel. + */ + private List methods; +} \ No newline at end of file diff --git a/src/test/java/com/checkout/paymentmethods/PaymentMethodsClientImplTest.java b/src/test/java/com/checkout/paymentmethods/PaymentMethodsClientImplTest.java new file mode 100644 index 00000000..a91eeadc --- /dev/null +++ b/src/test/java/com/checkout/paymentmethods/PaymentMethodsClientImplTest.java @@ -0,0 +1,141 @@ +package com.checkout.paymentmethods; + +import com.checkout.ApiClient; +import com.checkout.CheckoutArgumentException; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.paymentmethods.requests.PaymentMethodsQuery; +import com.checkout.paymentmethods.responses.PaymentMethodsResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PaymentMethodsClientImplTest { + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration checkoutConfiguration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + private PaymentMethodsClient paymentMethodsClient; + + @BeforeEach + void setUp() { + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + lenient().when(checkoutConfiguration.getSdkCredentials()).thenReturn(sdkCredentials); + this.paymentMethodsClient = new PaymentMethodsClientImpl(apiClient, checkoutConfiguration); + } + + @Test + void shouldGetPaymentMethods() throws ExecutionException, InterruptedException { + final PaymentMethodsQuery query = createPaymentMethodsQuery(); + final PaymentMethodsResponse expectedResponse = mock(PaymentMethodsResponse.class); + + when(apiClient.queryAsync(eq("payment-methods"), any(SdkAuthorization.class), eq(query), eq(PaymentMethodsResponse.class))) + .thenReturn(CompletableFuture.completedFuture(expectedResponse)); + + final CompletableFuture future = paymentMethodsClient.getPaymentMethods(query); + + final PaymentMethodsResponse actualResponse = future.get(); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldThrowExceptionWhenQueryIsNull() { + assertThrows(CheckoutArgumentException.class, + () -> paymentMethodsClient.getPaymentMethods(null)); + } + + @Test + void shouldThrowExceptionWhenProcessingChannelIdIsNull() { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder().build(); + + assertThrows(CheckoutArgumentException.class, + () -> paymentMethodsClient.getPaymentMethods(query)); + } + + @Test + void shouldThrowExceptionWhenProcessingChannelIdIsInvalid() { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder() + .processingChannelId("invalid_id") + .build(); + + assertThrows(IllegalArgumentException.class, + () -> paymentMethodsClient.getPaymentMethods(query)); + } + + // Synchronous methods + @Test + void shouldGetPaymentMethodsSync() { + final PaymentMethodsQuery query = createPaymentMethodsQuery(); + final PaymentMethodsResponse expectedResponse = mock(PaymentMethodsResponse.class); + + when(apiClient.query(eq("payment-methods"), any(SdkAuthorization.class), eq(query), eq(PaymentMethodsResponse.class))) + .thenReturn(expectedResponse); + + final PaymentMethodsResponse actualResponse = paymentMethodsClient.getPaymentMethodsSync(query); + + validateResponse(expectedResponse, actualResponse); + } + + @Test + void shouldThrowExceptionWhenQueryIsNullSync() { + assertThrows(CheckoutArgumentException.class, + () -> paymentMethodsClient.getPaymentMethodsSync(null)); + } + + @Test + void shouldThrowExceptionWhenProcessingChannelIdIsNullSync() { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder().build(); + + assertThrows(CheckoutArgumentException.class, + () -> paymentMethodsClient.getPaymentMethodsSync(query)); + } + + @Test + void shouldThrowExceptionWhenProcessingChannelIdIsInvalidSync() { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder() + .processingChannelId("invalid_id") + .build(); + + assertThrows(IllegalArgumentException.class, + () -> paymentMethodsClient.getPaymentMethodsSync(query)); + } + + // Common methods + private PaymentMethodsQuery createPaymentMethodsQuery() { + return PaymentMethodsQuery.builder() + .processingChannelId("pc_12345678901234567890123456") + .build(); + } + + private void validateResponse(final T expectedResponse, final T actualResponse) { + assertEquals(expectedResponse, actualResponse); + assertNotNull(actualResponse); + } +} \ No newline at end of file diff --git a/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java b/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java new file mode 100644 index 00000000..4ee35fa8 --- /dev/null +++ b/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java @@ -0,0 +1,56 @@ +package com.checkout.paymentmethods; + +import com.checkout.CheckoutApiException; +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.paymentmethods.requests.PaymentMethodsQuery; +import com.checkout.paymentmethods.entities.PaymentMethod; +import com.checkout.paymentmethods.responses.PaymentMethodsResponse; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PaymentMethodsTestIT extends SandboxTestFixture { + + private static final String VALID_PROCESSING_CHANNEL_ID = "pc_5jp2az55l3cuths25t5p3xhwru"; + + PaymentMethodsTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Test + void shouldGetPaymentMethods() throws Exception { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder() + .processingChannelId(VALID_PROCESSING_CHANNEL_ID) + .build(); + + final PaymentMethodsResponse response = blocking(() -> checkoutApi.paymentMethodsClient() + .getPaymentMethods(query)); + + validateGetAvailablePaymentMethodsResponse(response); + } + + @Test + void shouldGetPaymentMethodsSync() { + final PaymentMethodsQuery query = PaymentMethodsQuery.builder() + .processingChannelId(VALID_PROCESSING_CHANNEL_ID) + .build(); + + final PaymentMethodsResponse response = checkoutApi.paymentMethodsClient().getPaymentMethodsSync(query); + + validateGetAvailablePaymentMethodsResponse(response); + } + + // Common methods + private void validateGetAvailablePaymentMethodsResponse(final PaymentMethodsResponse response) { + assertNotNull(response, "Response should not be null"); + assertNotNull(response.getMethods(), "Methods should not be null"); + assertTrue(response.getMethods().size() > 0, "Methods count should be greater than 0"); + + for (PaymentMethod method : response.getMethods()) { + assertNotNull(method, "Method should not be null"); + assertNotNull(method.getType(), "Method type should not be null"); + } + } +} \ No newline at end of file From 6141fc6c12b0f80b1e68531ee5a1919ff204d76d Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 11 Mar 2026 12:39:05 +0100 Subject: [PATCH 11/19] Unused import deletion --- .../java/com/checkout/paymentmethods/PaymentMethodsTestIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java b/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java index 4ee35fa8..76d250d1 100644 --- a/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java +++ b/src/test/java/com/checkout/paymentmethods/PaymentMethodsTestIT.java @@ -1,6 +1,5 @@ package com.checkout.paymentmethods; -import com.checkout.CheckoutApiException; import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; import com.checkout.paymentmethods.requests.PaymentMethodsQuery; From ffcd8911dcde932eb4d346b3d3615966c6d98017 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 12 Mar 2026 12:12:59 +0100 Subject: [PATCH 12/19] StandaloneAccountUpdaterClient initial development (must review) --- .../StandaloneAccountUpdaterClient.java | 31 +++++++ .../StandaloneAccountUpdaterClientImpl.java | 85 +++++++++++++++++++ .../entities/AccountUpdateFailureCode.java | 17 ++++ .../entities/AccountUpdateStatus.java | 17 ++++ .../entities/CardBase.java | 28 ++++++ .../entities/CardDetails.java | 21 +++++ .../entities/CardUpdated.java | 35 ++++++++ .../entities/InstrumentReference.java | 19 +++++ .../entities/SourceOptions.java | 23 +++++ .../entities/UpdatedCardDetails.java | 20 +++++ .../GetUpdatedCardCredentialsRequest.java | 20 +++++ .../GetUpdatedCardCredentialsResponse.java | 30 +++++++ 12 files changed, 346 insertions(+) create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClient.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImpl.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateFailureCode.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateStatus.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/CardBase.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/CardDetails.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/CardUpdated.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/InstrumentReference.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/SourceOptions.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/entities/UpdatedCardDetails.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/requests/GetUpdatedCardCredentialsRequest.java create mode 100644 src/main/java/com/checkout/standaloneaccountupdater/responses/GetUpdatedCardCredentialsResponse.java diff --git a/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClient.java b/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClient.java new file mode 100644 index 00000000..fc30e0ad --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClient.java @@ -0,0 +1,31 @@ +package com.checkout.standaloneaccountupdater; + +import com.checkout.standaloneaccountupdater.requests.GetUpdatedCardCredentialsRequest; +import com.checkout.standaloneaccountupdater.responses.GetUpdatedCardCredentialsResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * Client interface for standalone account updater operations. + */ +public interface StandaloneAccountUpdaterClient { + + /** + * Get updated card credentials. + * Retrieve updated card credentials. The following card schemes are supported: Mastercard, Visa, American Express. + * + * @param getUpdatedCardCredentialsRequest The get updated card credentials request + * @return CompletableFuture containing the get updated card credentials response + */ + CompletableFuture getUpdatedCardCredentials(GetUpdatedCardCredentialsRequest getUpdatedCardCredentialsRequest); + + // Synchronous methods + /** + * Get updated card credentials (synchronous). + * Retrieve updated card credentials. The following card schemes are supported: Mastercard, Visa, American Express. + * + * @param getUpdatedCardCredentialsRequest The get updated card credentials request + * @return The get updated card credentials response + */ + GetUpdatedCardCredentialsResponse getUpdatedCardCredentialsSync(GetUpdatedCardCredentialsRequest getUpdatedCardCredentialsRequest); +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImpl.java b/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImpl.java new file mode 100644 index 00000000..93b964ef --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImpl.java @@ -0,0 +1,85 @@ +package com.checkout.standaloneaccountupdater; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorizationType; +import com.checkout.common.CheckoutUtils; +import com.checkout.standaloneaccountupdater.requests.GetUpdatedCardCredentialsRequest; +import com.checkout.standaloneaccountupdater.responses.GetUpdatedCardCredentialsResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * Implementation of the standalone account updater client. + */ +public class StandaloneAccountUpdaterClientImpl extends AbstractClient implements StandaloneAccountUpdaterClient { + + private static final String ACCOUNT_UPDATER_PATH = "account-updater"; + private static final String CARDS_PATH = "cards"; + + public StandaloneAccountUpdaterClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); + } + + @Override + public CompletableFuture getUpdatedCardCredentials( + final GetUpdatedCardCredentialsRequest getUpdatedCardCredentialsRequest) { + validateGetUpdatedCardCredentialsRequest(getUpdatedCardCredentialsRequest); + return apiClient.postAsync(buildPath(ACCOUNT_UPDATER_PATH, CARDS_PATH), sdkAuthorization(), + GetUpdatedCardCredentialsResponse.class, getUpdatedCardCredentialsRequest, null); + } + + // Synchronous methods + @Override + public GetUpdatedCardCredentialsResponse getUpdatedCardCredentialsSync( + final GetUpdatedCardCredentialsRequest getUpdatedCardCredentialsRequest) { + validateGetUpdatedCardCredentialsRequest(getUpdatedCardCredentialsRequest); + return apiClient.post(buildPath(ACCOUNT_UPDATER_PATH, CARDS_PATH), sdkAuthorization(), + GetUpdatedCardCredentialsResponse.class, getUpdatedCardCredentialsRequest, null); + } + + // Common methods + private void validateGetUpdatedCardCredentialsRequest(final GetUpdatedCardCredentialsRequest request) { + CheckoutUtils.validateParams("getUpdatedCardCredentialsRequest", request); + if (request.getSourceOptions() == null) { + throw new IllegalArgumentException("sourceOptions cannot be null"); + } + if (request.getSourceOptions().getCard() == null && request.getSourceOptions().getInstrument() == null) { + throw new IllegalArgumentException("Either card or instrument must be provided in sourceOptions"); + } + if (request.getSourceOptions().getCard() != null && request.getSourceOptions().getInstrument() != null) { + throw new IllegalArgumentException("You must provide either card or instrument, but not both"); + } + if (request.getSourceOptions().getCard() != null) { + validateCardDetails(request.getSourceOptions().getCard()); + } + if (request.getSourceOptions().getInstrument() != null) { + validateInstrumentReference(request.getSourceOptions().getInstrument()); + } + } + + private void validateCardDetails(final com.checkout.standaloneaccountupdater.entities.CardDetails cardDetails) { + if (cardDetails.getNumber() == null || cardDetails.getNumber().trim().isEmpty()) { + throw new IllegalArgumentException("Card number cannot be null or empty"); + } + if (!cardDetails.getNumber().matches("^[0-9]+$")) { + throw new IllegalArgumentException("Card number must contain only digits"); + } + if (cardDetails.getExpiryMonth() == null) { + throw new IllegalArgumentException("Card expiry month cannot be null"); + } + if (cardDetails.getExpiryMonth() < 1 || cardDetails.getExpiryMonth() > 12) { + throw new IllegalArgumentException("Card expiry month must be between 1 and 12"); + } + if (cardDetails.getExpiryYear() == null) { + throw new IllegalArgumentException("Card expiry year cannot be null"); + } + } + + private void validateInstrumentReference(final com.checkout.standaloneaccountupdater.entities.InstrumentReference instrumentReference) { + if (instrumentReference.getId() == null || instrumentReference.getId().trim().isEmpty()) { + throw new IllegalArgumentException("Instrument ID cannot be null or empty"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateFailureCode.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateFailureCode.java new file mode 100644 index 00000000..0f590cfd --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateFailureCode.java @@ -0,0 +1,17 @@ +package com.checkout.standaloneaccountupdater.entities; + +import com.google.gson.annotations.SerializedName; + +public enum AccountUpdateFailureCode { + @SerializedName("CARDHOLDER_OPT_OUT") + CARDHOLDER_OPT_OUT, + + @SerializedName("UP_TO_DATE") + UP_TO_DATE, + + @SerializedName("NON_PARTICIPATING_BIN") + NON_PARTICIPATING_BIN, + + @SerializedName("UNKNOWN") + UNKNOWN +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateStatus.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateStatus.java new file mode 100644 index 00000000..40114522 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/AccountUpdateStatus.java @@ -0,0 +1,17 @@ +package com.checkout.standaloneaccountupdater.entities; + +import com.google.gson.annotations.SerializedName; + +public enum AccountUpdateStatus { + @SerializedName("CARD_UPDATED") + CARD_UPDATED, + + @SerializedName("CARD_EXPIRY_UPDATED") + CARD_EXPIRY_UPDATED, + + @SerializedName("CARD_CLOSED") + CARD_CLOSED, + + @SerializedName("UPDATE_FAILED") + UPDATE_FAILED +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/CardBase.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardBase.java new file mode 100644 index 00000000..cc127232 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardBase.java @@ -0,0 +1,28 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +/** + * Base class for card expiry date information + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public abstract class CardBase { + + /** + * The expiry month of the card + * [Required] + */ + private Integer expiryMonth; + + /** + * The four-digit expiry year of the card + * [Required] + */ + private Integer expiryYear; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/CardDetails.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardDetails.java new file mode 100644 index 00000000..371dc937 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardDetails.java @@ -0,0 +1,21 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class CardDetails extends CardBase { + + /** + * The card number + * [Required] + */ + private String number; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/CardUpdated.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardUpdated.java new file mode 100644 index 00000000..3e001881 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/CardUpdated.java @@ -0,0 +1,35 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@EqualsAndHashCode(callSuper = true) +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +public class CardUpdated extends CardBase { + + /** + * The encrypted full Primary Account Number (PAN). Returned only for PCI SAQ D merchants. + */ + private String encryptedCardNumber; + + /** + * The first 6 digits of the PAN. + */ + private String bin; + + /** + * Last 4 digits of the PAN. + */ + private String last4; + + /** + * Unique identifier for the card + */ + private String fingerprint; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/InstrumentReference.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/InstrumentReference.java new file mode 100644 index 00000000..5fd9f70d --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/InstrumentReference.java @@ -0,0 +1,19 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InstrumentReference { + + /** + * Unique instrument identifier + * [Required] + */ + private String id; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/SourceOptions.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/SourceOptions.java new file mode 100644 index 00000000..2f334b5a --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/SourceOptions.java @@ -0,0 +1,23 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SourceOptions { + + /** + * The card details + */ + private CardDetails card; + + /** + * Instrument reference + */ + private InstrumentReference instrument; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/entities/UpdatedCardDetails.java b/src/main/java/com/checkout/standaloneaccountupdater/entities/UpdatedCardDetails.java new file mode 100644 index 00000000..34e477a5 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/entities/UpdatedCardDetails.java @@ -0,0 +1,20 @@ +package com.checkout.standaloneaccountupdater.entities; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UpdatedCardDetails { + + private Integer expiryMonth; + private Integer expiryYear; + private String encryptedCardNumber; + private String bin; + private String last4; + private String fingerprint; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/requests/GetUpdatedCardCredentialsRequest.java b/src/main/java/com/checkout/standaloneaccountupdater/requests/GetUpdatedCardCredentialsRequest.java new file mode 100644 index 00000000..90d59b50 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/requests/GetUpdatedCardCredentialsRequest.java @@ -0,0 +1,20 @@ +package com.checkout.standaloneaccountupdater.requests; + +import com.checkout.standaloneaccountupdater.entities.SourceOptions; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GetUpdatedCardCredentialsRequest { + + /** + * The source to update. You must provide either card or instrument object, but not both. + */ + private SourceOptions sourceOptions; +} \ No newline at end of file diff --git a/src/main/java/com/checkout/standaloneaccountupdater/responses/GetUpdatedCardCredentialsResponse.java b/src/main/java/com/checkout/standaloneaccountupdater/responses/GetUpdatedCardCredentialsResponse.java new file mode 100644 index 00000000..e88f9180 --- /dev/null +++ b/src/main/java/com/checkout/standaloneaccountupdater/responses/GetUpdatedCardCredentialsResponse.java @@ -0,0 +1,30 @@ +package com.checkout.standaloneaccountupdater.responses; + +import com.checkout.common.Resource; +import com.checkout.standaloneaccountupdater.entities.AccountUpdateStatus; +import com.checkout.standaloneaccountupdater.entities.AccountUpdateFailureCode; +import com.checkout.standaloneaccountupdater.entities.CardUpdated; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GetUpdatedCardCredentialsResponse extends Resource { + + /** + * Result of the update operation. + * [Required] + */ + private AccountUpdateStatus accountUpdateStatus; + + /** + * This field is returned when the update fails and the scheme returns an appropriate reason code. + * For more information, see Standalone Account Updater + */ + private AccountUpdateFailureCode accountUpdateFailureCode; + + /** + * Updated card details. Fields vary depending on PCI compliance level. + */ + private CardUpdated card; +} \ No newline at end of file From 5f3a05728f757463c9f72e9b6a46b386810df68e Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 16 Mar 2026 16:23:05 +0100 Subject: [PATCH 13/19] Recovered explicit content type header --- src/main/java/com/checkout/ApacheHttpClientTransport.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index 774e01e9..e243cffe 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -55,6 +55,7 @@ import static com.checkout.common.CheckoutUtils.getVersionFromManifest; import static org.apache.http.HttpHeaders.ACCEPT; import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.apache.http.HttpHeaders.CONTENT_TYPE; import static org.apache.http.HttpHeaders.USER_AGENT; @Slf4j @@ -305,6 +306,8 @@ private Response performCall(final SdkAuthorization authorization, } else if (requestBody instanceof MultipartEntityBuilder) { httpEntity = ((MultipartEntityBuilder) requestBody).build(); } else if (requestBody instanceof UrlEncodedFormEntity) { + // Ensure proper Content-Type header for form-encoded content + request.setHeader(CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.getMimeType()); httpEntity = (UrlEncodedFormEntity) requestBody; } else { String json = serializer.toJson(requestBody); From 1ab1260f33805496e8a44107229f59db8b256970 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 16 Mar 2026 16:39:48 +0100 Subject: [PATCH 14/19] Final transport conflict fix --- .../checkout/ApacheHttpClientTransport.java | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index e243cffe..64628ed5 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -102,7 +102,6 @@ public CompletableFuture invoke(final ClientOperation clientOperation, final String idempotencyKey, final Map queryParams) { - final Object requestBodyOrEntity = prepareRequestContent(requestObject); return CompletableFuture.supplyAsync(() -> { final HttpUriRequest request; switch (clientOperation) { @@ -138,7 +137,7 @@ public CompletableFuture invoke(final ClientOperation clientOperation, if (idempotencyKey != null) { request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey); } - return performCall(authorization, requestBodyOrEntity, request, clientOperation); + return performCall(authorization, requestObject, request, clientOperation); }, executor); } @@ -158,7 +157,6 @@ public Response invokeSync(final ClientOperation clientOperation, final Object requestObject, final String idempotencyKey, final Map queryParams) { - final Object requestBodyOrEntity = prepareRequestContent(requestObject); final HttpUriRequest request; switch (clientOperation) { case GET: @@ -194,7 +192,7 @@ public Response invokeSync(final ClientOperation clientOperation, request.setHeader(CKO_IDEMPOTENCY_KEY, idempotencyKey); } - final Supplier callSupplier = () -> performCall(authorization, requestBodyOrEntity, request, clientOperation); + final Supplier callSupplier = () -> performCall(authorization, requestObject, request, clientOperation); return executeWithResilience4j(callSupplier); } @@ -238,29 +236,6 @@ private Response executeWithResilience4j(final Supplier supplier) { return decoratedSupplier.get(); } - /** - * Prepares the request content based on the type of request object. - * If the request object is a UrlEncodedFormEntity, it returns it directly. - * Otherwise, it serializes the object to JSON. - */ - private Object prepareRequestContent(final Object requestObject) { - Object request = null; - - if (requestObject != null) { - if (requestObject instanceof UrlEncodedFormEntity) { - // Return the form entity directly for form-encoded requests - request = requestObject; - } - else - { - // Default behavior: serialize to JSON - request = serializer.toJson(requestObject); - } - } - - return request; - } - private HttpEntity getMultipartFileEntity(final AbstractFileRequest abstractFileRequest) { final MultipartEntityBuilder builder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE); if (abstractFileRequest instanceof FileRequest) { From 826816257e991518c87a2af971e8b6fcb7ec5025 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 16 Mar 2026 17:02:36 +0100 Subject: [PATCH 15/19] StandaloneAccountUpdater + unit + integrations tests --- src/main/java/com/checkout/CheckoutApi.java | 3 + .../java/com/checkout/CheckoutApiImpl.java | 9 ++ ...tandaloneAccountUpdaterClientImplTest.java | 119 +++++++++++++++++ .../StandaloneAccountUpdaterTestIT.java | 124 ++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImplTest.java create mode 100644 src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterTestIT.java diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index b9cc8d72..b21351a0 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -26,6 +26,7 @@ import com.checkout.reports.ReportsClient; import com.checkout.risk.RiskClient; import com.checkout.sessions.SessionsClient; +import com.checkout.standaloneaccountupdater.StandaloneAccountUpdaterClient; import com.checkout.tokens.TokensClient; import com.checkout.transfers.TransfersClient; import com.checkout.workflows.WorkflowsClient; @@ -90,4 +91,6 @@ public interface CheckoutApi extends CheckoutApmApi { NetworkTokensClient networkTokensClient(); + StandaloneAccountUpdaterClient standaloneAccountUpdaterClient(); + } diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index f3deea04..8c357585 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -52,6 +52,8 @@ import com.checkout.risk.RiskClientImpl; import com.checkout.sessions.SessionsClient; import com.checkout.sessions.SessionsClientImpl; +import com.checkout.standaloneaccountupdater.StandaloneAccountUpdaterClient; +import com.checkout.standaloneaccountupdater.StandaloneAccountUpdaterClientImpl; import com.checkout.tokens.TokensClient; import com.checkout.tokens.TokensClientImpl; import com.checkout.transfers.TransfersClient; @@ -92,6 +94,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final IdDocumentVerificationClient idDocumentVerificationClient; private final AmlScreeningClient amlScreeningClient; private final NetworkTokensClient networkTokensClient; + private final StandaloneAccountUpdaterClient standaloneAccountUpdaterClient; public CheckoutApiImpl(final CheckoutConfiguration configuration) { super(configuration); @@ -126,6 +129,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(this.apiClient, configuration); this.amlScreeningClient = new AmlScreeningClientImpl(this.apiClient, configuration); this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); + this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); } @Override @@ -245,8 +249,13 @@ public MetadataClient metadataClient() { @Override public AmlScreeningClient amlScreeningClient() { return amlScreeningClient; } + + @Override public NetworkTokensClient networkTokensClient() { return networkTokensClient; } + @Override + public StandaloneAccountUpdaterClient standaloneAccountUpdaterClient() { return standaloneAccountUpdaterClient; } + private ApiClient getFilesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new FilesApiUriStrategy(configuration)); } diff --git a/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImplTest.java b/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImplTest.java new file mode 100644 index 00000000..416c0123 --- /dev/null +++ b/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterClientImplTest.java @@ -0,0 +1,119 @@ +package com.checkout.standaloneaccountupdater; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.standaloneaccountupdater.entities.CardDetails; +import com.checkout.standaloneaccountupdater.entities.InstrumentReference; +import com.checkout.standaloneaccountupdater.entities.SourceOptions; +import com.checkout.standaloneaccountupdater.requests.GetUpdatedCardCredentialsRequest; +import com.checkout.standaloneaccountupdater.responses.GetUpdatedCardCredentialsResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.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 StandaloneAccountUpdaterClientImplTest { + + private StandaloneAccountUpdaterClient 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 StandaloneAccountUpdaterClientImpl(apiClient, configuration); + } + + @Test + void shouldGetUpdatedCardCredentials() throws ExecutionException, InterruptedException { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithCard(); + final GetUpdatedCardCredentialsResponse response = mock(GetUpdatedCardCredentialsResponse.class); + + when(apiClient.postAsync(eq("account-updater/cards"), eq(authorization), eq(GetUpdatedCardCredentialsResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.getUpdatedCardCredentials(request); + + validateGetUpdatedCardCredentialsResponse(response, future.get()); + } + + // Synchronous methods + @Test + void shouldGetUpdatedCardCredentialsSync() { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithCard(); + final GetUpdatedCardCredentialsResponse response = mock(GetUpdatedCardCredentialsResponse.class); + + when(apiClient.post(eq("account-updater/cards"), eq(authorization), eq(GetUpdatedCardCredentialsResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final GetUpdatedCardCredentialsResponse result = client.getUpdatedCardCredentialsSync(request); + + validateGetUpdatedCardCredentialsResponse(response, result); + } + + // Common methods + private void validateGetUpdatedCardCredentialsResponse(final GetUpdatedCardCredentialsResponse response, final GetUpdatedCardCredentialsResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private GetUpdatedCardCredentialsRequest createGetUpdatedCardCredentialsRequestWithCard() { + final CardDetails cardDetails = CardDetails.builder() + .number("5436424242424242") + .expiryMonth(5) + .expiryYear(2025) + .build(); + + final SourceOptions sourceOptions = SourceOptions.builder() + .card(cardDetails) + .build(); + + return GetUpdatedCardCredentialsRequest.builder() + .sourceOptions(sourceOptions) + .build(); + } + + private GetUpdatedCardCredentialsRequest createGetUpdatedCardCredentialsRequestWithInstrument() { + final InstrumentReference instrumentReference = InstrumentReference.builder() + .id("src_nmukohhu7vbe5f55ndwqzwv2c4") + .build(); + + final SourceOptions sourceOptions = SourceOptions.builder() + .instrument(instrumentReference) + .build(); + + return GetUpdatedCardCredentialsRequest.builder() + .sourceOptions(sourceOptions) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterTestIT.java b/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterTestIT.java new file mode 100644 index 00000000..9ae62d69 --- /dev/null +++ b/src/test/java/com/checkout/standaloneaccountupdater/StandaloneAccountUpdaterTestIT.java @@ -0,0 +1,124 @@ +package com.checkout.standaloneaccountupdater; + +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.standaloneaccountupdater.entities.AccountUpdateStatus; +import com.checkout.standaloneaccountupdater.entities.CardDetails; +import com.checkout.standaloneaccountupdater.entities.InstrumentReference; +import com.checkout.standaloneaccountupdater.entities.SourceOptions; +import com.checkout.standaloneaccountupdater.requests.GetUpdatedCardCredentialsRequest; +import com.checkout.standaloneaccountupdater.responses.GetUpdatedCardCredentialsResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StandaloneAccountUpdaterTestIT extends SandboxTestFixture { + + public StandaloneAccountUpdaterTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Disabled("This test requires valid card data and OAuth scope vault:real-time-account-updater") + @Test + void shouldGetUpdatedCardCredentialsWithCard() { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithCard(); + + final GetUpdatedCardCredentialsResponse response = blocking(() -> checkoutApi.standaloneAccountUpdaterClient().getUpdatedCardCredentials(request)); + + validateGetUpdatedCardCredentialsResponse(response); + + } + + @Disabled("This test requires valid instrument ID and OAuth scope vault:real-time-account-updater") + @Test + void shouldGetUpdatedCardCredentialsWithInstrument() { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithInstrument(); + + final GetUpdatedCardCredentialsResponse response = blocking(() -> checkoutApi.standaloneAccountUpdaterClient().getUpdatedCardCredentials(request)); + + validateGetUpdatedCardCredentialsResponse(response); + + } + + // Sync methods + @Disabled("This test requires valid card data and OAuth scope vault:real-time-account-updater") + @Test + void shouldGetUpdatedCardCredentialsWithCardSync() { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithCard(); + + final GetUpdatedCardCredentialsResponse response = checkoutApi.standaloneAccountUpdaterClient().getUpdatedCardCredentialsSync(request); + + validateGetUpdatedCardCredentialsResponse(response); + + } + + @Disabled("This test requires valid instrument ID and OAuth scope vault:real-time-account-updater") + @Test + void shouldGetUpdatedCardCredentialsWithInstrumentSync() { + + final GetUpdatedCardCredentialsRequest request = createGetUpdatedCardCredentialsRequestWithInstrument(); + + final GetUpdatedCardCredentialsResponse response = checkoutApi.standaloneAccountUpdaterClient().getUpdatedCardCredentialsSync(request); + + validateGetUpdatedCardCredentialsResponse(response); + + } + + // Common methods + private GetUpdatedCardCredentialsRequest createGetUpdatedCardCredentialsRequestWithCard() { + final CardDetails cardDetails = CardDetails.builder() + .number("5436424242424242") + .expiryMonth(5) + .expiryYear(2025) + .build(); + + final SourceOptions sourceOptions = SourceOptions.builder() + .card(cardDetails) + .build(); + + return GetUpdatedCardCredentialsRequest.builder() + .sourceOptions(sourceOptions) + .build(); + } + + private GetUpdatedCardCredentialsRequest createGetUpdatedCardCredentialsRequestWithInstrument() { + final InstrumentReference instrumentReference = InstrumentReference.builder() + .id("src_nmukohhu7vbe5f55ndwqzwv2c4") + .build(); + + final SourceOptions sourceOptions = SourceOptions.builder() + .instrument(instrumentReference) + .build(); + + return GetUpdatedCardCredentialsRequest.builder() + .sourceOptions(sourceOptions) + .build(); + } + + private void validateGetUpdatedCardCredentialsResponse(final GetUpdatedCardCredentialsResponse response) { + assertNotNull(response); + assertNotNull(response.getAccountUpdateStatus()); + assertTrue(response.getAccountUpdateStatus() == AccountUpdateStatus.CARD_UPDATED + || response.getAccountUpdateStatus() == AccountUpdateStatus.CARD_EXPIRY_UPDATED + || response.getAccountUpdateStatus() == AccountUpdateStatus.CARD_CLOSED + || response.getAccountUpdateStatus() == AccountUpdateStatus.UPDATE_FAILED); + + if (response.getAccountUpdateStatus() == AccountUpdateStatus.CARD_UPDATED + || response.getAccountUpdateStatus() == AccountUpdateStatus.CARD_EXPIRY_UPDATED) { + assertNotNull(response.getCard()); + assertNotNull(response.getCard().getExpiryMonth()); + assertNotNull(response.getCard().getExpiryYear()); + // Card details like bin, last4, fingerprint may or may not be present depending on PCI compliance level + } + + if (response.getAccountUpdateStatus() == AccountUpdateStatus.UPDATE_FAILED) { + // accountUpdateFailureCode may be present when update fails + } + } + +} \ No newline at end of file From 7e4617ace90a07cfb195b6052618f0c40f573aea Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 17 Mar 2026 10:03:52 +0100 Subject: [PATCH 16/19] Applepay module + unit and integration tests --- src/main/java/com/checkout/CheckoutApi.java | 3 + .../java/com/checkout/CheckoutApiImpl.java | 7 + .../applepay/ApplePayClient.java | 72 +++++++ .../applepay/ApplePayClientImpl.java | 98 +++++++++ .../applepay/entities/ProtocolVersions.java | 13 ++ .../requests/EnrollDomainRequest.java | 19 ++ .../GenerateSigningRequestRequest.java | 22 ++ .../requests/UploadCertificateRequest.java | 19 ++ .../GenerateSigningRequestResponse.java | 16 ++ .../responses/UploadCertificateResponse.java | 37 ++++ .../applepay/ApplePayClientImplTest.java | 201 ++++++++++++++++++ .../applepay/ApplePayTestIT.java | 200 +++++++++++++++++ 12 files changed, 707 insertions(+) create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClient.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImpl.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/entities/ProtocolVersions.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/EnrollDomainRequest.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/GenerateSigningRequestRequest.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/UploadCertificateRequest.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/GenerateSigningRequestResponse.java create mode 100644 src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/UploadCertificateResponse.java create mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImplTest.java create mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayTestIT.java diff --git a/src/main/java/com/checkout/CheckoutApi.java b/src/main/java/com/checkout/CheckoutApi.java index b21351a0..fd4c473a 100644 --- a/src/main/java/com/checkout/CheckoutApi.java +++ b/src/main/java/com/checkout/CheckoutApi.java @@ -9,6 +9,7 @@ import com.checkout.forward.ForwardClient; import com.checkout.handlepaymentsandpayouts.flow.FlowClient; import com.checkout.handlepaymentsandpayouts.setups.PaymentSetupsClient; +import com.checkout.handlepaymentsandpayouts.applepay.ApplePayClient; import com.checkout.identities.faceauthentications.FaceAuthenticationClient; import com.checkout.identities.applicants.ApplicantClient; import com.checkout.identities.identityverification.IdentityVerificationClient; @@ -77,6 +78,8 @@ public interface CheckoutApi extends CheckoutApmApi { PaymentSetupsClient paymentSetupsClient(); + ApplePayClient applePayClient(); + ForwardClient forwardClient(); FaceAuthenticationClient faceAuthenticationClient(); diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 8c357585..06875880 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -18,6 +18,8 @@ import com.checkout.handlepaymentsandpayouts.flow.FlowClientImpl; import com.checkout.handlepaymentsandpayouts.setups.PaymentSetupsClient; import com.checkout.handlepaymentsandpayouts.setups.PaymentSetupsClientImpl; +import com.checkout.handlepaymentsandpayouts.applepay.ApplePayClient; +import com.checkout.handlepaymentsandpayouts.applepay.ApplePayClientImpl; import com.checkout.identities.faceauthentications.FaceAuthenticationClient; import com.checkout.identities.faceauthentications.FaceAuthenticationClientImpl; import com.checkout.identities.applicants.ApplicantClient; @@ -87,6 +89,7 @@ public class CheckoutApiImpl extends AbstractCheckoutApmApi implements CheckoutA private final PaymentContextsClient paymentContextsClient; private final FlowClient flowClient; private final PaymentSetupsClient paymentSetupsClient; + private final ApplePayClient applePayClient; private final ForwardClient forwardClient; private final FaceAuthenticationClient faceAuthenticationClient; private final ApplicantClient applicantClient; @@ -122,6 +125,7 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.paymentContextsClient = new PaymentContextsClientImpl(this.apiClient, configuration); this.flowClient = new FlowClientImpl(this.apiClient, configuration); this.paymentSetupsClient = new PaymentSetupsClientImpl(this.apiClient, configuration); + this.applePayClient = new ApplePayClientImpl(this.apiClient, configuration); this.forwardClient = new ForwardClientImpl(this.apiClient, configuration); this.faceAuthenticationClient = new FaceAuthenticationClientImpl(this.apiClient, configuration); this.applicantClient = new ApplicantClientImpl(this.apiClient, configuration); @@ -232,6 +236,9 @@ public MetadataClient metadataClient() { @Override public PaymentSetupsClient paymentSetupsClient() { return paymentSetupsClient; } + @Override + public ApplePayClient applePayClient() { return applePayClient; } + @Override public ForwardClient forwardClient() { return forwardClient; } diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClient.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClient.java new file mode 100644 index 00000000..3041e4fd --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClient.java @@ -0,0 +1,72 @@ +package com.checkout.handlepaymentsandpayouts.applepay; + +import com.checkout.EmptyResponse; +import com.checkout.handlepaymentsandpayouts.applepay.requests.EnrollDomainRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.GenerateSigningRequestRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.UploadCertificateRequest; +import com.checkout.handlepaymentsandpayouts.applepay.responses.GenerateSigningRequestResponse; +import com.checkout.handlepaymentsandpayouts.applepay.responses.UploadCertificateResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * Client interface for Apple Pay operations. + */ +public interface ApplePayClient { + + /** + * Upload a payment processing certificate. + * Upload a payment processing certificate. This will allow you to start processing payments via Apple Pay. + * + * @param uploadCertificateRequest The upload certificate request + * @return CompletableFuture containing the upload certificate response + */ + CompletableFuture uploadPaymentProcessingCertificate(UploadCertificateRequest uploadCertificateRequest); + + /** + * Enroll a domain to the Apple Pay Service. + * Enroll a domain to the Apple Pay Service. + * + * @param enrollDomainRequest The enroll domain request + * @return CompletableFuture containing the empty response + */ + CompletableFuture enrollDomain(EnrollDomainRequest enrollDomainRequest); + + /** + * Generate a certificate signing request. + * Generate a certificate signing request. You'll need to upload this to your Apple Developer account to download a payment processing certificate. + * + * @param generateSigningRequestRequest The generate signing request + * @return CompletableFuture containing the generate signing request response + */ + CompletableFuture generateCertificateSigningRequest(GenerateSigningRequestRequest generateSigningRequestRequest); + + // Synchronous methods + /** + * Upload a payment processing certificate (synchronous). + * Upload a payment processing certificate. This will allow you to start processing payments via Apple Pay. + * + * @param uploadCertificateRequest The upload certificate request + * @return The upload certificate response + */ + UploadCertificateResponse uploadPaymentProcessingCertificateSync(UploadCertificateRequest uploadCertificateRequest); + + /** + * Enroll a domain to the Apple Pay Service (synchronous). + * Enroll a domain to the Apple Pay Service. + * + * @param enrollDomainRequest The enroll domain request + * @return The empty response + */ + EmptyResponse enrollDomainSync(EnrollDomainRequest enrollDomainRequest); + + /** + * Generate a certificate signing request (synchronous). + * Generate a certificate signing request. You'll need to upload this to your Apple Developer account to download a payment processing certificate. + * + * @param generateSigningRequestRequest The generate signing request + * @return The generate signing request response + */ + GenerateSigningRequestResponse generateCertificateSigningRequestSync(GenerateSigningRequestRequest generateSigningRequestRequest); + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImpl.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImpl.java new file mode 100644 index 00000000..75c77750 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImpl.java @@ -0,0 +1,98 @@ +package com.checkout.handlepaymentsandpayouts.applepay; + +import com.checkout.AbstractClient; +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; +import com.checkout.SdkAuthorizationType; +import com.checkout.common.CheckoutUtils; +import com.checkout.handlepaymentsandpayouts.applepay.requests.EnrollDomainRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.GenerateSigningRequestRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.UploadCertificateRequest; +import com.checkout.handlepaymentsandpayouts.applepay.responses.GenerateSigningRequestResponse; +import com.checkout.handlepaymentsandpayouts.applepay.responses.UploadCertificateResponse; + +import java.util.concurrent.CompletableFuture; + +/** + * Implementation of the Apple Pay client. + */ +public class ApplePayClientImpl extends AbstractClient implements ApplePayClient { + + private static final String APPLEPAY_PATH = "applepay"; + private static final String CERTIFICATES_PATH = "certificates"; + private static final String ENROLLMENTS_PATH = "enrollments"; + private static final String SIGNING_REQUESTS_PATH = "signing-requests"; + + public ApplePayClientImpl(final ApiClient apiClient, final CheckoutConfiguration configuration) { + super(apiClient, configuration, SdkAuthorizationType.SECRET_KEY_OR_OAUTH); + } + + @Override + public CompletableFuture uploadPaymentProcessingCertificate( + final UploadCertificateRequest uploadCertificateRequest) { + validateUploadCertificateRequest(uploadCertificateRequest); + return apiClient.postAsync(buildPath(APPLEPAY_PATH, CERTIFICATES_PATH), sdkAuthorization(), + UploadCertificateResponse.class, uploadCertificateRequest, null); + } + + @Override + public CompletableFuture enrollDomain(final EnrollDomainRequest enrollDomainRequest) { + validateEnrollDomainRequest(enrollDomainRequest); + return apiClient.postAsync(buildPath(APPLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(SdkAuthorizationType.OAUTH), + EmptyResponse.class, enrollDomainRequest, null); + } + + @Override + public CompletableFuture generateCertificateSigningRequest( + final GenerateSigningRequestRequest generateSigningRequestRequest) { + validateGenerateSigningRequestRequest(generateSigningRequestRequest); + return apiClient.postAsync(buildPath(APPLEPAY_PATH, SIGNING_REQUESTS_PATH), sdkAuthorization(), + GenerateSigningRequestResponse.class, generateSigningRequestRequest, null); + } + + // Synchronous methods + @Override + public UploadCertificateResponse uploadPaymentProcessingCertificateSync( + final UploadCertificateRequest uploadCertificateRequest) { + validateUploadCertificateRequest(uploadCertificateRequest); + return apiClient.post(buildPath(APPLEPAY_PATH, CERTIFICATES_PATH), sdkAuthorization(), + UploadCertificateResponse.class, uploadCertificateRequest, null); + } + + @Override + public EmptyResponse enrollDomainSync(final EnrollDomainRequest enrollDomainRequest) { + validateEnrollDomainRequest(enrollDomainRequest); + return apiClient.post(buildPath(APPLEPAY_PATH, ENROLLMENTS_PATH), sdkAuthorization(SdkAuthorizationType.OAUTH), + EmptyResponse.class, enrollDomainRequest, null); + } + + @Override + public GenerateSigningRequestResponse generateCertificateSigningRequestSync( + final GenerateSigningRequestRequest generateSigningRequestRequest) { + validateGenerateSigningRequestRequest(generateSigningRequestRequest); + return apiClient.post(buildPath(APPLEPAY_PATH, SIGNING_REQUESTS_PATH), sdkAuthorization(), + GenerateSigningRequestResponse.class, generateSigningRequestRequest, null); + } + + // Common methods + private void validateUploadCertificateRequest(final UploadCertificateRequest request) { + CheckoutUtils.validateParams("uploadCertificateRequest", request); + if (request.getContent() == null || request.getContent().trim().isEmpty()) { + throw new IllegalArgumentException("Certificate content cannot be null or empty"); + } + } + + private void validateEnrollDomainRequest(final EnrollDomainRequest request) { + CheckoutUtils.validateParams("enrollDomainRequest", request); + if (request.getDomain() == null || request.getDomain().trim().isEmpty()) { + throw new IllegalArgumentException("Domain cannot be null or empty"); + } + } + + private void validateGenerateSigningRequestRequest(final GenerateSigningRequestRequest request) { + CheckoutUtils.validateParams("generateSigningRequestRequest", request); + // protocolVersion has a default value, so no null check needed + } + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/entities/ProtocolVersions.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/entities/ProtocolVersions.java new file mode 100644 index 00000000..38ca7da2 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/entities/ProtocolVersions.java @@ -0,0 +1,13 @@ +package com.checkout.handlepaymentsandpayouts.applepay.entities; + +import com.google.gson.annotations.SerializedName; + +public enum ProtocolVersions { + + @SerializedName("ec_v1") + EC_V1, + + @SerializedName("rsa_v1") + RSA_V1 + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/EnrollDomainRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/EnrollDomainRequest.java new file mode 100644 index 00000000..226d85f1 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/EnrollDomainRequest.java @@ -0,0 +1,19 @@ +package com.checkout.handlepaymentsandpayouts.applepay.requests; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EnrollDomainRequest { + + /** + * The domain to enroll + */ + private String domain; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/GenerateSigningRequestRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/GenerateSigningRequestRequest.java new file mode 100644 index 00000000..eba004df --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/GenerateSigningRequestRequest.java @@ -0,0 +1,22 @@ +package com.checkout.handlepaymentsandpayouts.applepay.requests; + +import com.checkout.handlepaymentsandpayouts.applepay.entities.ProtocolVersions; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GenerateSigningRequestRequest { + + /** + * The protocol version of the encryption type used. + * Default: "ec_v1" + */ + @Builder.Default + private ProtocolVersions protocolVersion = ProtocolVersions.EC_V1; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/UploadCertificateRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/UploadCertificateRequest.java new file mode 100644 index 00000000..0e8aab04 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/requests/UploadCertificateRequest.java @@ -0,0 +1,19 @@ +package com.checkout.handlepaymentsandpayouts.applepay.requests; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UploadCertificateRequest { + + /** + * The certificate content + */ + private String content; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/GenerateSigningRequestResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/GenerateSigningRequestResponse.java new file mode 100644 index 00000000..d5898fc5 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/GenerateSigningRequestResponse.java @@ -0,0 +1,16 @@ +package com.checkout.handlepaymentsandpayouts.applepay.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GenerateSigningRequestResponse extends Resource { + + /** + * The signing request content + */ + private String content; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/UploadCertificateResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/UploadCertificateResponse.java new file mode 100644 index 00000000..532c7f15 --- /dev/null +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/applepay/responses/UploadCertificateResponse.java @@ -0,0 +1,37 @@ +package com.checkout.handlepaymentsandpayouts.applepay.responses; + +import com.checkout.common.Resource; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.Instant; + +@Data +@EqualsAndHashCode(callSuper = true) +public class UploadCertificateResponse extends Resource { + + /** + * The identifier of the account domain + * [Required] + */ + private String id; + + /** + * Hash of the certificate public key + * [Required] + */ + private String publicKeyHash; + + /** + * When the certificate is valid from + * [Required] + */ + private Instant validFrom; + + /** + * When the certificate is valid until + * [Required] + */ + private Instant validUntil; + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImplTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImplTest.java new file mode 100644 index 00000000..f1c3b8d6 --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayClientImplTest.java @@ -0,0 +1,201 @@ +package com.checkout.handlepaymentsandpayouts.applepay; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.EmptyResponse; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.handlepaymentsandpayouts.applepay.entities.ProtocolVersions; +import com.checkout.handlepaymentsandpayouts.applepay.requests.EnrollDomainRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.GenerateSigningRequestRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.UploadCertificateRequest; +import com.checkout.handlepaymentsandpayouts.applepay.responses.GenerateSigningRequestResponse; +import com.checkout.handlepaymentsandpayouts.applepay.responses.UploadCertificateResponse; +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.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.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ApplePayClientImplTest { + + private ApplePayClient client; + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + @Mock + private SdkAuthorization oAuthAuthorization; + + @BeforeEach + void setUp() { + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); + lenient().when(sdkCredentials.getAuthorization(SdkAuthorizationType.OAUTH)).thenReturn(oAuthAuthorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new ApplePayClientImpl(apiClient, configuration); + } + + @Test + void shouldUploadPaymentProcessingCertificate() throws ExecutionException, InterruptedException { + + final UploadCertificateRequest request = createUploadCertificateRequest(); + final UploadCertificateResponse response = mock(UploadCertificateResponse.class); + + when(apiClient.postAsync(eq("applepay/certificates"), eq(authorization), eq(UploadCertificateResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.uploadPaymentProcessingCertificate(request); + + validateUploadCertificateResponse(response, future.get()); + } + + @Test + void shouldEnrollDomain() throws ExecutionException, InterruptedException { + + final EnrollDomainRequest request = createEnrollDomainRequest(); + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.postAsync(eq("applepay/enrollments"), eq(oAuthAuthorization), eq(EmptyResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.enrollDomain(request); + + validateEmptyResponse(response, future.get()); + } + + @Test + void shouldGenerateCertificateSigningRequest() throws ExecutionException, InterruptedException { + + final GenerateSigningRequestRequest request = createGenerateSigningRequestRequest(); + final GenerateSigningRequestResponse response = mock(GenerateSigningRequestResponse.class); + + when(apiClient.postAsync(eq("applepay/signing-requests"), eq(authorization), eq(GenerateSigningRequestResponse.class), + eq(request), isNull())) + .thenReturn(CompletableFuture.completedFuture(response)); + + final CompletableFuture future = client.generateCertificateSigningRequest(request); + + validateGenerateSigningRequestResponse(response, future.get()); + } + + // Synchronous methods + @Test + void shouldUploadPaymentProcessingCertificateSync() { + + final UploadCertificateRequest request = createUploadCertificateRequest(); + final UploadCertificateResponse response = mock(UploadCertificateResponse.class); + + when(apiClient.post(eq("applepay/certificates"), eq(authorization), eq(UploadCertificateResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final UploadCertificateResponse result = client.uploadPaymentProcessingCertificateSync(request); + + validateUploadCertificateResponse(response, result); + } + + @Test + void shouldEnrollDomainSync() { + + final EnrollDomainRequest request = createEnrollDomainRequest(); + final EmptyResponse response = mock(EmptyResponse.class); + + when(apiClient.post(eq("applepay/enrollments"), eq(oAuthAuthorization), eq(EmptyResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final EmptyResponse result = client.enrollDomainSync(request); + + validateEmptyResponse(response, result); + } + + @Test + void shouldGenerateCertificateSigningRequestSync() { + + final GenerateSigningRequestRequest request = createGenerateSigningRequestRequest(); + final GenerateSigningRequestResponse response = mock(GenerateSigningRequestResponse.class); + + when(apiClient.post(eq("applepay/signing-requests"), eq(authorization), eq(GenerateSigningRequestResponse.class), + eq(request), isNull())) + .thenReturn(response); + + final GenerateSigningRequestResponse result = client.generateCertificateSigningRequestSync(request); + + validateGenerateSigningRequestResponse(response, result); + } + + // Common methods + private void validateUploadCertificateResponse(final UploadCertificateResponse response, final UploadCertificateResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateEmptyResponse(final EmptyResponse response, final EmptyResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private void validateGenerateSigningRequestResponse(final GenerateSigningRequestResponse response, final GenerateSigningRequestResponse result) { + assertNotNull(result); + assertEquals(response, result); + } + + private UploadCertificateRequest createUploadCertificateRequest() { + return UploadCertificateRequest.builder() + .content("-----BEGIN CERTIFICATE-----\nMIIDFjCCAf4CAQAwDQYJKoZIhvcNAQEFBQAwUzELMAkGA1UEBhMCVUsxEDAOBgNV\n-----END CERTIFICATE-----") + .build(); + } + + private EnrollDomainRequest createEnrollDomainRequest() { + return EnrollDomainRequest.builder() + .domain("example.com") + .build(); + } + + private GenerateSigningRequestRequest createGenerateSigningRequestRequest() { + return GenerateSigningRequestRequest.builder() + .protocolVersion(ProtocolVersions.EC_V1) + .build(); + } + + private UploadCertificateResponse createMockUploadCertificateResponse() { + final UploadCertificateResponse response = new UploadCertificateResponse(); + response.setId("aplc_hefptsiydvkexnzzb35zrlqgfq"); + response.setPublicKeyHash("tqYV+tmG9aMh+l/K6cicUnPqkb1gUiLjSTM9gEz6Nl0="); + response.setValidFrom(Instant.parse("2021-01-01T17:32:28Z")); + response.setValidUntil(Instant.parse("2025-01-01T17:32:28Z")); + return response; + } + + private GenerateSigningRequestResponse createMockGenerateSigningRequestResponse() { + final GenerateSigningRequestResponse response = new GenerateSigningRequestResponse(); + response.setContent("-----BEGIN CERTIFICATE REQUEST-----\nMIICWjCCAUICAQAwFTETMBEGA1UEAwwKaXNzdWluZy5jb20wggEiMA0GCSqGSIb3\n-----END CERTIFICATE REQUEST-----"); + return response; + } + +} \ No newline at end of file diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayTestIT.java new file mode 100644 index 00000000..e88db879 --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/applepay/ApplePayTestIT.java @@ -0,0 +1,200 @@ +package com.checkout.handlepaymentsandpayouts.applepay; + +import com.checkout.EmptyResponse; +import com.checkout.PlatformType; +import com.checkout.SandboxTestFixture; +import com.checkout.handlepaymentsandpayouts.applepay.entities.ProtocolVersions; +import com.checkout.handlepaymentsandpayouts.applepay.requests.EnrollDomainRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.GenerateSigningRequestRequest; +import com.checkout.handlepaymentsandpayouts.applepay.requests.UploadCertificateRequest; +import com.checkout.handlepaymentsandpayouts.applepay.responses.GenerateSigningRequestResponse; +import com.checkout.handlepaymentsandpayouts.applepay.responses.UploadCertificateResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ApplePayTestIT extends SandboxTestFixture { + + public ApplePayTestIT() { + super(PlatformType.DEFAULT_OAUTH); + } + + @Disabled("This test requires a valid payment processing certificate") + @Test + void shouldUploadPaymentProcessingCertificate() { + + final UploadCertificateRequest request = createUploadCertificateRequest(); + + final UploadCertificateResponse response = blocking(() -> checkoutApi.applePayClient().uploadPaymentProcessingCertificate(request)); + + validateUploadCertificateResponse(response); + + } + + @Disabled("This test requires OAuth credentials and domain verification") + @Test + void shouldEnrollDomain() { + + final EnrollDomainRequest request = createEnrollDomainRequest(); + + final EmptyResponse response = blocking(() -> checkoutApi.applePayClient().enrollDomain(request)); + + validateEmptyResponse(response); + + } + + @Test + void shouldGenerateCertificateSigningRequest() { + + final GenerateSigningRequestRequest request = createGenerateSigningRequestRequest(); + + final GenerateSigningRequestResponse response = blocking(() -> checkoutApi.applePayClient().generateCertificateSigningRequest(request)); + + validateGenerateSigningRequestResponse(response); + + } + + @Test + void shouldGenerateCertificateSigningRequestWithRsaProtocol() { + + final GenerateSigningRequestRequest request = GenerateSigningRequestRequest.builder() + .protocolVersion(ProtocolVersions.RSA_V1) + .build(); + + final GenerateSigningRequestResponse response = blocking(() -> checkoutApi.applePayClient().generateCertificateSigningRequest(request)); + + validateGenerateSigningRequestResponse(response); + + } + + @Test + void shouldApplePayWorkflowGenerateSigningRequestAndUploadCertificate() { + + // Arrange - Generate signing request first + final GenerateSigningRequestRequest signingRequest = createGenerateSigningRequestRequest(); + + // Act - Generate signing request + final GenerateSigningRequestResponse signingResponse = blocking(() -> checkoutApi.applePayClient().generateCertificateSigningRequest(signingRequest)); + + // Assert signing request response + validateGenerateSigningRequestResponse(signingResponse); + + // Note: In real scenario, you would: + // 1. Take the signingResponse.getContent() + // 2. Submit it to Apple Developer Portal + // 3. Download the certificate from Apple + // 4. Upload it using uploadPaymentProcessingCertificate + // + // For integration testing, we skip the Apple Developer Portal steps + // as they require manual intervention and valid Apple Developer account + + } + + // Sync methods + @Disabled("This test requires a valid payment processing certificate") + @Test + void shouldUploadPaymentProcessingCertificateSync() { + + final UploadCertificateRequest request = createUploadCertificateRequest(); + + final UploadCertificateResponse response = checkoutApi.applePayClient().uploadPaymentProcessingCertificateSync(request); + + validateUploadCertificateResponse(response); + + } + + @Disabled("This test requires OAuth credentials and domain verification") + @Test + void shouldEnrollDomainSync() { + + final EnrollDomainRequest request = createEnrollDomainRequest(); + + final EmptyResponse response = checkoutApi.applePayClient().enrollDomainSync(request); + + validateEmptyResponse(response); + + } + + @Test + void shouldGenerateCertificateSigningRequestSync() { + + final GenerateSigningRequestRequest request = createGenerateSigningRequestRequest(); + + final GenerateSigningRequestResponse response = checkoutApi.applePayClient().generateCertificateSigningRequestSync(request); + + validateGenerateSigningRequestResponse(response); + + } + + @Test + void shouldGenerateCertificateSigningRequestWithRsaProtocolSync() { + + final GenerateSigningRequestRequest request = GenerateSigningRequestRequest.builder() + .protocolVersion(ProtocolVersions.RSA_V1) + .build(); + + final GenerateSigningRequestResponse response = checkoutApi.applePayClient().generateCertificateSigningRequestSync(request); + + validateGenerateSigningRequestResponse(response); + + } + + // Common methods + private UploadCertificateRequest createUploadCertificateRequest() { + // Note: This is a sample certificate format + // In real scenarios, this would be a certificate obtained from Apple Developer Portal + return UploadCertificateRequest.builder() + .content("-----BEGIN CERTIFICATE-----\n" + + "MIIFjTCCBHWgAwIBAgIIAqVJ3DZvutkwDQYJKoZIhvcNAQEFBQAwgZYxCzAJBgNV\n" + + "BAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3Js\n" + + "ZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3\n" + + "aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkw\n" + + "HhcNMjQwMTEwMTUzNjQ1WhcNMjUwMTA5MTUzNjQ1WjCBjDEaMBgGCgmSJomT8ixk\n" + + "ARkWCnRlc3QtZG9tYWluMS0wKwYDVQQDDCRtZXJjaGFudC50ZXN0LWRvbWFpbiAo\n" + + "U2FuZGJveCkgLSBBUE1QMRMwEQYDVQQIDApDYWxpZm9ybmlhMQswCQYDVQQGEwJV\n" + + "UzEXMBUGA1UECgwOVGVzdCBNZXJjaGFudDEXMBUGA1UECwwOVGVzdCBNZXJjaGFu\n" + + "dDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGvWZDKkf8rkJ4V1sdf9Wt1iBZvD\n" + + "l9dEJkY/CJFVYNvK5qgWUzjbGKKcLFfvHt3vvK6jggHtMIIB6TAMBgNVHRMBAf8E\n" + + "-----END CERTIFICATE-----") + .build(); + } + + private EnrollDomainRequest createEnrollDomainRequest() { + return EnrollDomainRequest.builder() + .domain("checkout-test-domain.com") + .build(); + } + + private GenerateSigningRequestRequest createGenerateSigningRequestRequest() { + return GenerateSigningRequestRequest.builder() + .protocolVersion(ProtocolVersions.EC_V1) + .build(); + } + + private void validateUploadCertificateResponse(final UploadCertificateResponse response) { + assertNotNull(response); + assertNotNull(response.getId()); + assertNotNull(response.getPublicKeyHash()); + assertNotNull(response.getValidFrom()); + assertNotNull(response.getValidUntil()); + assertTrue(response.getValidUntil().isAfter(response.getValidFrom()), + "Certificate valid until should be after valid from"); + } + + private void validateEmptyResponse(final EmptyResponse response) { + assertNotNull(response); + // EmptyResponse doesn't have properties to validate, but should not throw + } + + private void validateGenerateSigningRequestResponse(final GenerateSigningRequestResponse response) { + assertNotNull(response); + assertNotNull(response.getContent()); + assertTrue(response.getContent().contains("BEGIN CERTIFICATE REQUEST"), + "Response should contain certificate signing request format"); + assertTrue(response.getContent().contains("END CERTIFICATE REQUEST"), + "Response should contain certificate signing request format"); + } + +} \ No newline at end of file From 7aa474f193a396fed5c1962e0fb85021ebf5ef19 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 18 Mar 2026 16:07:43 +0100 Subject: [PATCH 17/19] dummy pull to swap networktokens folder to minor case --- .../{networkTokens => networkTokens2}/NetworkTokensClient.java | 0 .../NetworkTokensClientImpl.java | 0 .../entities/AbstractNetworkTokenSource.java | 0 .../{networkTokens => networkTokens2}/entities/CardDetails.java | 0 .../entities/CardNetworkTokenSource.java | 0 .../entities/DeletionReason.java | 0 .../entities/IdNetworkTokenSource.java | 0 .../{networkTokens => networkTokens2}/entities/InitiatedBy.java | 0 .../entities/NetworkTokenDetails.java | 0 .../entities/NetworkTokenSourceType.java | 0 .../entities/NetworkTokenState.java | 0 .../entities/NetworkTokenType.java | 0 .../entities/TransactionType.java | 0 .../requests/DeleteNetworkTokenRequest.java | 0 .../requests/ProvisionNetworkTokenRequest.java | 0 .../requests/RequestCryptogramRequest.java | 0 .../responses/CryptogramResponse.java | 0 .../responses/NetworkTokenResponse.java | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/NetworkTokensClient.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/NetworkTokensClientImpl.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/AbstractNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/CardDetails.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/CardNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/DeletionReason.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/IdNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/InitiatedBy.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/NetworkTokenDetails.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/NetworkTokenSourceType.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/NetworkTokenState.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/NetworkTokenType.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/entities/TransactionType.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/requests/DeleteNetworkTokenRequest.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/requests/ProvisionNetworkTokenRequest.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/requests/RequestCryptogramRequest.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/responses/CryptogramResponse.java (100%) rename src/main/java/com/checkout/{networkTokens => networkTokens2}/responses/NetworkTokenResponse.java (100%) diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClient.java b/src/main/java/com/checkout/networkTokens2/NetworkTokensClient.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/NetworkTokensClient.java rename to src/main/java/com/checkout/networkTokens2/NetworkTokensClient.java diff --git a/src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java b/src/main/java/com/checkout/networkTokens2/NetworkTokensClientImpl.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/NetworkTokensClientImpl.java rename to src/main/java/com/checkout/networkTokens2/NetworkTokensClientImpl.java diff --git a/src/main/java/com/checkout/networkTokens/entities/AbstractNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens2/entities/AbstractNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/AbstractNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens2/entities/AbstractNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens/entities/CardDetails.java b/src/main/java/com/checkout/networkTokens2/entities/CardDetails.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/CardDetails.java rename to src/main/java/com/checkout/networkTokens2/entities/CardDetails.java diff --git a/src/main/java/com/checkout/networkTokens/entities/CardNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens2/entities/CardNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/CardNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens2/entities/CardNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens/entities/DeletionReason.java b/src/main/java/com/checkout/networkTokens2/entities/DeletionReason.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/DeletionReason.java rename to src/main/java/com/checkout/networkTokens2/entities/DeletionReason.java diff --git a/src/main/java/com/checkout/networkTokens/entities/IdNetworkTokenSource.java b/src/main/java/com/checkout/networkTokens2/entities/IdNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/IdNetworkTokenSource.java rename to src/main/java/com/checkout/networkTokens2/entities/IdNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java b/src/main/java/com/checkout/networkTokens2/entities/InitiatedBy.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/InitiatedBy.java rename to src/main/java/com/checkout/networkTokens2/entities/InitiatedBy.java diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java b/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenDetails.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/NetworkTokenDetails.java rename to src/main/java/com/checkout/networkTokens2/entities/NetworkTokenDetails.java diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java b/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenSourceType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/NetworkTokenSourceType.java rename to src/main/java/com/checkout/networkTokens2/entities/NetworkTokenSourceType.java diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java b/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenState.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/NetworkTokenState.java rename to src/main/java/com/checkout/networkTokens2/entities/NetworkTokenState.java diff --git a/src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java b/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/NetworkTokenType.java rename to src/main/java/com/checkout/networkTokens2/entities/NetworkTokenType.java diff --git a/src/main/java/com/checkout/networkTokens/entities/TransactionType.java b/src/main/java/com/checkout/networkTokens2/entities/TransactionType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/entities/TransactionType.java rename to src/main/java/com/checkout/networkTokens2/entities/TransactionType.java diff --git a/src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens2/requests/DeleteNetworkTokenRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/requests/DeleteNetworkTokenRequest.java rename to src/main/java/com/checkout/networkTokens2/requests/DeleteNetworkTokenRequest.java diff --git a/src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java b/src/main/java/com/checkout/networkTokens2/requests/ProvisionNetworkTokenRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/requests/ProvisionNetworkTokenRequest.java rename to src/main/java/com/checkout/networkTokens2/requests/ProvisionNetworkTokenRequest.java diff --git a/src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java b/src/main/java/com/checkout/networkTokens2/requests/RequestCryptogramRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/requests/RequestCryptogramRequest.java rename to src/main/java/com/checkout/networkTokens2/requests/RequestCryptogramRequest.java diff --git a/src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java b/src/main/java/com/checkout/networkTokens2/responses/CryptogramResponse.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/responses/CryptogramResponse.java rename to src/main/java/com/checkout/networkTokens2/responses/CryptogramResponse.java diff --git a/src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java b/src/main/java/com/checkout/networkTokens2/responses/NetworkTokenResponse.java similarity index 100% rename from src/main/java/com/checkout/networkTokens/responses/NetworkTokenResponse.java rename to src/main/java/com/checkout/networkTokens2/responses/NetworkTokenResponse.java From bd494321c9d89fe8026c080ece277c46ce48bfff Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 18 Mar 2026 16:11:02 +0100 Subject: [PATCH 18/19] networktokens folder to minor case --- .../{networkTokens2 => networktokens}/NetworkTokensClient.java | 0 .../NetworkTokensClientImpl.java | 0 .../entities/AbstractNetworkTokenSource.java | 0 .../{networkTokens2 => networktokens}/entities/CardDetails.java | 0 .../entities/CardNetworkTokenSource.java | 0 .../entities/DeletionReason.java | 0 .../entities/IdNetworkTokenSource.java | 0 .../{networkTokens2 => networktokens}/entities/InitiatedBy.java | 0 .../entities/NetworkTokenDetails.java | 0 .../entities/NetworkTokenSourceType.java | 0 .../entities/NetworkTokenState.java | 0 .../entities/NetworkTokenType.java | 0 .../entities/TransactionType.java | 0 .../requests/DeleteNetworkTokenRequest.java | 0 .../requests/ProvisionNetworkTokenRequest.java | 0 .../requests/RequestCryptogramRequest.java | 0 .../responses/CryptogramResponse.java | 0 .../responses/NetworkTokenResponse.java | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/NetworkTokensClient.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/NetworkTokensClientImpl.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/AbstractNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/CardDetails.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/CardNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/DeletionReason.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/IdNetworkTokenSource.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/InitiatedBy.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/NetworkTokenDetails.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/NetworkTokenSourceType.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/NetworkTokenState.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/NetworkTokenType.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/entities/TransactionType.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/requests/DeleteNetworkTokenRequest.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/requests/ProvisionNetworkTokenRequest.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/requests/RequestCryptogramRequest.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/responses/CryptogramResponse.java (100%) rename src/main/java/com/checkout/{networkTokens2 => networktokens}/responses/NetworkTokenResponse.java (100%) diff --git a/src/main/java/com/checkout/networkTokens2/NetworkTokensClient.java b/src/main/java/com/checkout/networktokens/NetworkTokensClient.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/NetworkTokensClient.java rename to src/main/java/com/checkout/networktokens/NetworkTokensClient.java diff --git a/src/main/java/com/checkout/networkTokens2/NetworkTokensClientImpl.java b/src/main/java/com/checkout/networktokens/NetworkTokensClientImpl.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/NetworkTokensClientImpl.java rename to src/main/java/com/checkout/networktokens/NetworkTokensClientImpl.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/AbstractNetworkTokenSource.java b/src/main/java/com/checkout/networktokens/entities/AbstractNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/AbstractNetworkTokenSource.java rename to src/main/java/com/checkout/networktokens/entities/AbstractNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/CardDetails.java b/src/main/java/com/checkout/networktokens/entities/CardDetails.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/CardDetails.java rename to src/main/java/com/checkout/networktokens/entities/CardDetails.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/CardNetworkTokenSource.java b/src/main/java/com/checkout/networktokens/entities/CardNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/CardNetworkTokenSource.java rename to src/main/java/com/checkout/networktokens/entities/CardNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/DeletionReason.java b/src/main/java/com/checkout/networktokens/entities/DeletionReason.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/DeletionReason.java rename to src/main/java/com/checkout/networktokens/entities/DeletionReason.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/IdNetworkTokenSource.java b/src/main/java/com/checkout/networktokens/entities/IdNetworkTokenSource.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/IdNetworkTokenSource.java rename to src/main/java/com/checkout/networktokens/entities/IdNetworkTokenSource.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/InitiatedBy.java b/src/main/java/com/checkout/networktokens/entities/InitiatedBy.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/InitiatedBy.java rename to src/main/java/com/checkout/networktokens/entities/InitiatedBy.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenDetails.java b/src/main/java/com/checkout/networktokens/entities/NetworkTokenDetails.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/NetworkTokenDetails.java rename to src/main/java/com/checkout/networktokens/entities/NetworkTokenDetails.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenSourceType.java b/src/main/java/com/checkout/networktokens/entities/NetworkTokenSourceType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/NetworkTokenSourceType.java rename to src/main/java/com/checkout/networktokens/entities/NetworkTokenSourceType.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenState.java b/src/main/java/com/checkout/networktokens/entities/NetworkTokenState.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/NetworkTokenState.java rename to src/main/java/com/checkout/networktokens/entities/NetworkTokenState.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/NetworkTokenType.java b/src/main/java/com/checkout/networktokens/entities/NetworkTokenType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/NetworkTokenType.java rename to src/main/java/com/checkout/networktokens/entities/NetworkTokenType.java diff --git a/src/main/java/com/checkout/networkTokens2/entities/TransactionType.java b/src/main/java/com/checkout/networktokens/entities/TransactionType.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/entities/TransactionType.java rename to src/main/java/com/checkout/networktokens/entities/TransactionType.java diff --git a/src/main/java/com/checkout/networkTokens2/requests/DeleteNetworkTokenRequest.java b/src/main/java/com/checkout/networktokens/requests/DeleteNetworkTokenRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/requests/DeleteNetworkTokenRequest.java rename to src/main/java/com/checkout/networktokens/requests/DeleteNetworkTokenRequest.java diff --git a/src/main/java/com/checkout/networkTokens2/requests/ProvisionNetworkTokenRequest.java b/src/main/java/com/checkout/networktokens/requests/ProvisionNetworkTokenRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/requests/ProvisionNetworkTokenRequest.java rename to src/main/java/com/checkout/networktokens/requests/ProvisionNetworkTokenRequest.java diff --git a/src/main/java/com/checkout/networkTokens2/requests/RequestCryptogramRequest.java b/src/main/java/com/checkout/networktokens/requests/RequestCryptogramRequest.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/requests/RequestCryptogramRequest.java rename to src/main/java/com/checkout/networktokens/requests/RequestCryptogramRequest.java diff --git a/src/main/java/com/checkout/networkTokens2/responses/CryptogramResponse.java b/src/main/java/com/checkout/networktokens/responses/CryptogramResponse.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/responses/CryptogramResponse.java rename to src/main/java/com/checkout/networktokens/responses/CryptogramResponse.java diff --git a/src/main/java/com/checkout/networkTokens2/responses/NetworkTokenResponse.java b/src/main/java/com/checkout/networktokens/responses/NetworkTokenResponse.java similarity index 100% rename from src/main/java/com/checkout/networkTokens2/responses/NetworkTokenResponse.java rename to src/main/java/com/checkout/networktokens/responses/NetworkTokenResponse.java From 186bf4f8ac267c4b75a6dbc1e0d33b66695a1063 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 18 Mar 2026 16:51:07 +0100 Subject: [PATCH 19/19] PaymentMethod conflict resolution (now in common package) --- src/main/java/com/checkout/GsonSerializer.java | 1 - .../flow/requests/PaymentSessionCreateRequest.java | 6 +++--- .../flow/responses/PaymentSubmissionResponse.java | 4 ++-- .../handlepaymentsandpayouts/flow/FlowTestIT.java | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/checkout/GsonSerializer.java b/src/main/java/com/checkout/GsonSerializer.java index 6160f74c..2af579ab 100644 --- a/src/main/java/com/checkout/GsonSerializer.java +++ b/src/main/java/com/checkout/GsonSerializer.java @@ -24,7 +24,6 @@ import com.checkout.payments.sender.Sender; import com.checkout.payments.sender.SenderType; import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentSessionStatus; -import com.checkout.handlepaymentsandpayouts.flow.responses.PaymentSubmissionResponse; import com.checkout.webhooks.previous.WebhookResponse; import com.checkout.workflows.actions.WorkflowActionType; import com.checkout.workflows.conditions.WorkflowConditionType; diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/requests/PaymentSessionCreateRequest.java b/src/main/java/com/checkout/handlepaymentsandpayouts/flow/requests/PaymentSessionCreateRequest.java index be06a6f3..9020def6 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/requests/PaymentSessionCreateRequest.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/flow/requests/PaymentSessionCreateRequest.java @@ -1,7 +1,7 @@ package com.checkout.handlepaymentsandpayouts.flow.requests; +import com.checkout.common.PaymentMethodType; import com.checkout.handlepaymentsandpayouts.flow.entities.CustomerRetry; -import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentMethod; import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentMethodConfiguration; import com.google.gson.annotations.SerializedName; import lombok.AllArgsConstructor; @@ -31,12 +31,12 @@ public class PaymentSessionCreateRequest extends PaymentSessionInfo { /** * Specifies which payment method options to present to the customer. */ - private List enabledPaymentMethods; + private List enabledPaymentMethods; /** * Specifies which payment method options to not present to the customer. */ - private List disabledPaymentMethods; + private List disabledPaymentMethods; /** * Configurations for payment method-specific settings. diff --git a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/responses/PaymentSubmissionResponse.java b/src/main/java/com/checkout/handlepaymentsandpayouts/flow/responses/PaymentSubmissionResponse.java index 7313ea0f..557e18dc 100644 --- a/src/main/java/com/checkout/handlepaymentsandpayouts/flow/responses/PaymentSubmissionResponse.java +++ b/src/main/java/com/checkout/handlepaymentsandpayouts/flow/responses/PaymentSubmissionResponse.java @@ -1,7 +1,7 @@ package com.checkout.handlepaymentsandpayouts.flow.responses; import com.checkout.common.Resource; -import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentMethod; +import com.checkout.common.PaymentMethodType; import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentSessionStatus; import lombok.Data; @@ -28,5 +28,5 @@ public abstract class PaymentSubmissionResponse extends Resource { /** * The payment method name. */ - private PaymentMethod type; + private PaymentMethodType type; } \ No newline at end of file diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/FlowTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/FlowTestIT.java index 973d4134..e02ba7e0 100644 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/FlowTestIT.java +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/FlowTestIT.java @@ -6,8 +6,8 @@ import com.checkout.common.CountryCode; import com.checkout.common.Currency; import com.checkout.common.Phone; +import com.checkout.common.PaymentMethodType; import com.checkout.handlepaymentsandpayouts.flow.entities.CardConfiguration; -import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentMethod; import com.checkout.handlepaymentsandpayouts.flow.entities.PaymentMethodConfiguration; import com.checkout.handlepaymentsandpayouts.flow.requests.PaymentSessionCreateRequest; import com.checkout.handlepaymentsandpayouts.flow.requests.PaymentSessionSubmitRequest; @@ -167,7 +167,7 @@ private PaymentSessionCreateRequest createPaymentSessionCreateRequest() { .threeDS(createThreeDSRequest()) .capture(true) .locale(LocaleType.EN_GB) - .enabledPaymentMethods(Collections.singletonList(PaymentMethod.CARD)) + .enabledPaymentMethods(Collections.singletonList(PaymentMethodType.CARD)) .paymentMethodConfiguration(createPaymentMethodConfiguration()) .build(); } @@ -295,8 +295,8 @@ void paymentSessionCreateRequest_shouldHaveAllRequiredPropertiesForJsonCompatibi request.setRisk(createRiskRequest()); // Properties specific to PaymentSessionCreateRequest - request.setEnabledPaymentMethods(Collections.singletonList(PaymentMethod.CARD)); - request.setDisabledPaymentMethods(Collections.singletonList(PaymentMethod.EPS)); + request.setEnabledPaymentMethods(Collections.singletonList(PaymentMethodType.CARD)); + request.setDisabledPaymentMethods(Collections.singletonList(PaymentMethodType.EPS)); request.setPaymentMethodConfiguration(createPaymentMethodConfiguration()); request.setIpAddress("127.0.0.1");