From 16d5ae6d9f62ac34acd0f051c1a9d74ad4910c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:04:17 +0200 Subject: [PATCH 1/3] WIP --- .../checkout/ApacheHttpClientTransport.java | 49 ++++++++++++--- src/main/java/com/checkout/ApiClient.java | 4 ++ src/main/java/com/checkout/ApiClientImpl.java | 35 +++++++++++ src/main/java/com/checkout/Transport.java | 24 +++++++- .../requests/RequestAnAccessTokenRequest.java | 59 +++++++++++++++++++ .../RequestAnAccessTokenResponse.java | 28 +++++++++ .../com/checkout/issuing/IssuingClient.java | 4 ++ .../checkout/issuing/IssuingClientImpl.java | 23 ++++++++ .../flow/PaymentSessionsClientImplTest.java | 0 .../flow/PaymentSessionsTestIT.java | 5 ++ 10 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java create mode 100644 src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java create mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java create mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java diff --git a/src/main/java/com/checkout/ApacheHttpClientTransport.java b/src/main/java/com/checkout/ApacheHttpClientTransport.java index a6dd328e..b4dfd9dd 100644 --- a/src/main/java/com/checkout/ApacheHttpClientTransport.java +++ b/src/main/java/com/checkout/ApacheHttpClientTransport.java @@ -83,12 +83,14 @@ class ApacheHttpClientTransport implements Transport { } @Override - public CompletableFuture invoke(final ClientOperation clientOperation, - final String path, - final SdkAuthorization authorization, - final String requestBody, - final String idempotencyKey, - final Map queryParams) { + public CompletableFuture invoke( + final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final String requestBody, + final String idempotencyKey, + final Map queryParams + ) { return CompletableFuture.supplyAsync(() -> { final HttpUriRequest request; switch (clientOperation) { @@ -129,7 +131,40 @@ public CompletableFuture invoke(final ClientOperation clientOperation, } @Override - public CompletableFuture submitFile(final String path, final SdkAuthorization authorization, final AbstractFileRequest fileRequest) { + public CompletableFuture invoke( + final ClientOperation clientOperation, + final String path, + final SdkAuthorization authorization, + final String requestBody, + final String idempotencyKey, + final Map queryParams, + final String contentType + ) { + return CompletableFuture.supplyAsync(() -> { + final HttpPost request = new HttpPost(getRequestUrl(path)); + + if (idempotencyKey != null) { + request.setHeader("Cko-Idempotency-Key", idempotencyKey); + } + + request.setHeader("User-Agent", PROJECT_NAME + "/" + getVersionFromManifest()); + request.setHeader("Accept", ACCEPT_JSON); + request.setHeader("Authorization", authorization.getAuthorizationHeader()); + + if (requestBody != null) { + request.setEntity(new StringEntity(requestBody, ContentType.parse(contentType))); + } + + return performCall(authorization, null, request, clientOperation); + }, executor); + } + + @Override + public CompletableFuture submitFile( + final String path, + final SdkAuthorization authorization, + final AbstractFileRequest fileRequest + ) { return CompletableFuture.supplyAsync(() -> { final HttpPost request = new HttpPost(getRequestUrl(path)); request.setEntity(getMultipartFileEntity(fileRequest)); diff --git a/src/main/java/com/checkout/ApiClient.java b/src/main/java/com/checkout/ApiClient.java index 85e4c082..1e7c1fb4 100644 --- a/src/main/java/com/checkout/ApiClient.java +++ b/src/main/java/com/checkout/ApiClient.java @@ -1,8 +1,10 @@ package com.checkout; import com.checkout.common.AbstractFileRequest; +import org.apache.http.NameValuePair; import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -24,6 +26,8 @@ public interface ApiClient { CompletableFuture postAsync(String path, SdkAuthorization authorization, Map> resultTypeMappings, Object request, String idempotencyKey); + CompletableFuture postFormUrlEncodedAsync(String path, SdkAuthorization authorization, List formParams, Class responseType); + CompletableFuture deleteAsync(String path, SdkAuthorization authorization); CompletableFuture deleteAsync(String path, SdkAuthorization authorization, Class responseType); diff --git a/src/main/java/com/checkout/ApiClientImpl.java b/src/main/java/com/checkout/ApiClientImpl.java index 36fb15ed..927a0ff8 100644 --- a/src/main/java/com/checkout/ApiClientImpl.java +++ b/src/main/java/com/checkout/ApiClientImpl.java @@ -16,14 +16,17 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import com.checkout.common.AbstractFileRequest; import com.checkout.common.CheckoutUtils; import com.google.gson.reflect.TypeToken; +import org.apache.http.NameValuePair; public class ApiClientImpl implements ApiClient { @@ -176,6 +179,38 @@ private CompletableFuture sendRequestAsync(final Cli .thenApply(response -> deserialize(response, responseType)); } + public CompletableFuture postFormUrlEncodedAsync( + final String path, + final SdkAuthorization authorization, + final List formParams, + final Class responseType + ) { + validateParams(PATH, path, AUTHORIZATION, authorization, "formParams", formParams); + + final String body = formParams.stream() + .map(p -> p.getName() + "=" + encode(p.getValue())) + .collect(Collectors.joining("&")); + + return transport.invoke( + ClientOperation.POST, + path, + authorization, + body, + null, + null, + "application/x-www-form-urlencoded" + ).thenApply(this::errorCheck) + .thenApply(response -> deserialize(response, responseType)); + } + + private String encode(final String value) { + try { + return java.net.URLEncoder.encode(value, "UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + throw new CheckoutException("Failed to encode form param", e); + } + } + private Response errorCheck(final Response response) { if (!CheckoutUtils.isSuccessHttpStatusCode(response.getStatusCode())) { final Map errorDetails = serializer.fromJson(response.getBody()); diff --git a/src/main/java/com/checkout/Transport.java b/src/main/java/com/checkout/Transport.java index e2ad26e8..39f76fe3 100644 --- a/src/main/java/com/checkout/Transport.java +++ b/src/main/java/com/checkout/Transport.java @@ -7,8 +7,28 @@ public interface Transport { - CompletableFuture invoke(ClientOperation clientOperation, String path, SdkAuthorization authorization, String jsonRequest, String idempotencyKey, Map queryParams); + CompletableFuture invoke( + ClientOperation clientOperation, + String path, + SdkAuthorization authorization, + String jsonRequest, + String idempotencyKey, + Map queryParams + ); - CompletableFuture submitFile(String path, SdkAuthorization authorization, AbstractFileRequest fileRequest); + CompletableFuture invoke( + ClientOperation clientOperation, + String path, + SdkAuthorization authorization, + String requestBody, + String idempotencyKey, + Map queryParams, + String contentType + ); + CompletableFuture submitFile( + String path, + SdkAuthorization authorization, + AbstractFileRequest fileRequest + ); } diff --git a/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java new file mode 100644 index 00000000..8a773bd3 --- /dev/null +++ b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/requests/RequestAnAccessTokenRequest.java @@ -0,0 +1,59 @@ +package com.checkout.cardissuing.cardholderaccesstokens.requests; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +import java.util.ArrayList; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public final class RequestAnAccessTokenRequest { + + @SerializedName("grant_type") + private String grantType; + + /** + * Access key ID + */ + @SerializedName("client_id") + private String clientId; + + /** + * Access key secret + */ + @SerializedName("client_secret") + private String clientSecret; + + /** + * The cardholder's unique identifier + */ + @SerializedName("cardholder_id") + private String cardholderId; + + /** + * Specifies if the request is for a single-use token. Single-use tokens are required for sensitive endpoints + */ + @SerializedName("single_use") + private Boolean singleUse; + + public List toFormParams() { + final List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", grantType)); + params.add(new BasicNameValuePair("client_id", clientId)); + params.add(new BasicNameValuePair("client_secret", clientSecret)); + params.add(new BasicNameValuePair("cardholder_id", cardholderId)); + if (singleUse != null) { + params.add(new BasicNameValuePair("single_use", singleUse.toString())); + } + return params; + } + +} diff --git a/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java new file mode 100644 index 00000000..0a327825 --- /dev/null +++ b/src/main/java/com/checkout/cardissuing/cardholderaccesstokens/responses/RequestAnAccessTokenResponse.java @@ -0,0 +1,28 @@ +package com.checkout.cardissuing.cardholderaccesstokens.responses; + +import com.checkout.HttpMetadata; +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public final class RequestAnAccessTokenResponse extends HttpMetadata { + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("token_type") + private String tokenType; + + /** + * The remaining time the access token is valid for, in seconds + */ + @SerializedName("expires_in") + private Double expiresIn; + + private String scope; + +} \ No newline at end of file diff --git a/src/main/java/com/checkout/issuing/IssuingClient.java b/src/main/java/com/checkout/issuing/IssuingClient.java index d590a986..cf476d0e 100644 --- a/src/main/java/com/checkout/issuing/IssuingClient.java +++ b/src/main/java/com/checkout/issuing/IssuingClient.java @@ -1,6 +1,8 @@ package com.checkout.issuing; import com.checkout.EmptyResponse; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; import com.checkout.common.IdResponse; import com.checkout.issuing.cardholders.CardholderCardsResponse; import com.checkout.issuing.cardholders.CardholderDetailsResponse; @@ -70,6 +72,8 @@ public interface IssuingClient { CompletableFuture removeCardControl(final String controlId); + CompletableFuture RequestAnAccessToken(final RequestAnAccessTokenRequest requestAnAccessTokenRequest); + CompletableFuture simulateAuthorization(final CardAuthorizationRequest cardAuthorizationRequest); CompletableFuture simulateIncrementingAuthorization( diff --git a/src/main/java/com/checkout/issuing/IssuingClientImpl.java b/src/main/java/com/checkout/issuing/IssuingClientImpl.java index e27b303d..f26c3474 100644 --- a/src/main/java/com/checkout/issuing/IssuingClientImpl.java +++ b/src/main/java/com/checkout/issuing/IssuingClientImpl.java @@ -4,7 +4,11 @@ import com.checkout.ApiClient; import com.checkout.CheckoutConfiguration; import com.checkout.EmptyResponse; +import com.checkout.PlatformType; +import com.checkout.SdkAuthorization; import com.checkout.SdkAuthorizationType; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; import com.checkout.common.IdResponse; import com.checkout.issuing.cardholders.CardholderCardsResponse; import com.checkout.issuing.cardholders.CardholderDetailsResponse; @@ -60,6 +64,12 @@ public class IssuingClientImpl extends AbstractClient implements IssuingClient { private static final String CONTROLS_PATH = "controls"; + private static final String ACCESS_PATH = "access"; + + private static final String CONNECT_PATH = "connect"; + + private static final String TOKEN_PATH = "token"; + private static final String SIMULATE_PATH = "simulate"; private static final String AUTHORIZATIONS_PATH = "authorizations"; @@ -278,6 +288,19 @@ public CompletableFuture removeCardControl(final String controlId) { ); } + @Override + public CompletableFuture RequestAnAccessToken ( + final RequestAnAccessTokenRequest requestAnAccessTokenRequest + ) { + validateParams("requestAnAccessTokenRequest", requestAnAccessTokenRequest); + return apiClient.postFormUrlEncodedAsync( + buildPath(ISSUING_PATH, ACCESS_PATH, CONNECT_PATH, TOKEN_PATH), + new SdkAuthorization(PlatformType.DEFAULT, ""), + requestAnAccessTokenRequest.toFormParams(), + RequestAnAccessTokenResponse.class + ); + } + @Override public CompletableFuture simulateAuthorization(final CardAuthorizationRequest cardAuthorizationRequest) { validateParams("cardAuthorizationRequest", cardAuthorizationRequest); diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java new file mode 100644 index 00000000..e69de29b diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java new file mode 100644 index 00000000..fe5f27e0 --- /dev/null +++ b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java @@ -0,0 +1,5 @@ +package com.checkout.handlepaymentsandpayouts.flow; + +public class PaymentSessionsTestIT { + +} From 2b021f4f06de0fb8d968400b8eb757e5f6e24694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:31:35 +0200 Subject: [PATCH 2/3] WIP 2 --- src/main/java/com/checkout/ApiClientImpl.java | 1 + ...uinClientImplRequestAnAccessTokenTest.java | 64 +++++++++++++++++++ .../flow/PaymentSessionsClientImplTest.java | 0 .../flow/PaymentSessionsTestIT.java | 5 -- 4 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java delete mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java delete mode 100644 src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java diff --git a/src/main/java/com/checkout/ApiClientImpl.java b/src/main/java/com/checkout/ApiClientImpl.java index 927a0ff8..fcfac747 100644 --- a/src/main/java/com/checkout/ApiClientImpl.java +++ b/src/main/java/com/checkout/ApiClientImpl.java @@ -179,6 +179,7 @@ private CompletableFuture sendRequestAsync(final Cli .thenApply(response -> deserialize(response, responseType)); } + @Override public CompletableFuture postFormUrlEncodedAsync( final String path, final SdkAuthorization authorization, diff --git a/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java new file mode 100644 index 00000000..a9cac353 --- /dev/null +++ b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java @@ -0,0 +1,64 @@ +package com.checkout.cardissuing.cardholderaccesstokens; + +import com.checkout.ApiClient; +import com.checkout.CheckoutConfiguration; +import com.checkout.SdkAuthorization; +import com.checkout.SdkAuthorizationType; +import com.checkout.SdkCredentials; +import com.checkout.cardissuing.cardholderaccesstokens.requests.RequestAnAccessTokenRequest; +import com.checkout.cardissuing.cardholderaccesstokens.responses.RequestAnAccessTokenResponse; +import com.checkout.issuing.IssuingClient; +import com.checkout.issuing.IssuingClientImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class IssuinClientImplRequestAnAccessTokenTest { + + @Mock + private ApiClient apiClient; + + @Mock + private CheckoutConfiguration configuration; + + @Mock + private SdkCredentials sdkCredentials; + + @Mock + private SdkAuthorization authorization; + + private IssuingClient client; + + @BeforeEach + void setUp() { + when(sdkCredentials.getAuthorization(SdkAuthorizationType.)).thenReturn(authorization); + when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); + client = new IssuingClientImpl(apiClient, configuration); + } + + @Test + void shouldRequestAccessToken() throws ExecutionException, InterruptedException { + RequestAnAccessTokenRequest request = mock(RequestAnAccessTokenRequest.class); + RequestAnAccessTokenResponse response = mock(RequestAnAccessTokenResponse.class); + + when(apiClient.postAsync( + "issuing/cardholder-access/token", + authorization, + RequestAnAccessTokenResponse.class, + request, + null + )).thenReturn(CompletableFuture.completedFuture(response)); + + CompletableFuture future = client.RequestAnAccessToken(request); + + assertNotNull(future.get()); + assertEquals(response, future.get()); + } +} diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsClientImplTest.java deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java b/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java deleted file mode 100644 index fe5f27e0..00000000 --- a/src/test/java/com/checkout/handlepaymentsandpayouts/flow/PaymentSessionsTestIT.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.checkout.handlepaymentsandpayouts.flow; - -public class PaymentSessionsTestIT { - -} From 8f95abef8880fa01a2aef9acf8a56d2a7bc34d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Thu, 28 Aug 2025 16:33:08 +0200 Subject: [PATCH 3/3] WIP 3 --- .../IssuinClientImplRequestAnAccessTokenTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java index a9cac353..310604b1 100644 --- a/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java +++ b/src/test/java/com/checkout/cardissuing/cardholderaccesstokens/IssuinClientImplRequestAnAccessTokenTest.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -38,7 +37,7 @@ public class IssuinClientImplRequestAnAccessTokenTest { @BeforeEach void setUp() { - when(sdkCredentials.getAuthorization(SdkAuthorizationType.)).thenReturn(authorization); + when(sdkCredentials.getAuthorization(SdkAuthorizationType.SECRET_KEY_OR_OAUTH)).thenReturn(authorization); when(configuration.getSdkCredentials()).thenReturn(sdkCredentials); client = new IssuingClientImpl(apiClient, configuration); }