diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 7678ee1f..c4c18c4f 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -140,12 +140,13 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.applePayClient = new ApplePayClientImpl(this.apiClient, configuration); this.googlePayClient = new GooglePayClientImpl(this.apiClient, configuration); this.complianceClient = new ComplianceClientImpl(this.apiClient, configuration); - this.forwardClient = new ForwardClientImpl(this.apiClient, configuration); - this.faceAuthenticationClient = new FaceAuthenticationClientImpl(this.apiClient, configuration); - this.applicantClient = new ApplicantClientImpl(this.apiClient, configuration); - this.identityVerificationClient = new IdentityVerificationClientImpl(this.apiClient, configuration); - this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(this.apiClient, configuration); - this.amlScreeningClient = new AmlScreeningClientImpl(this.apiClient, configuration); + this.forwardClient = new ForwardClientImpl(getForwardClient(configuration), configuration); + final ApiClient identityApiClient = getIdentityClient(configuration); + this.faceAuthenticationClient = new FaceAuthenticationClientImpl(identityApiClient, configuration); + this.applicantClient = new ApplicantClientImpl(identityApiClient, configuration); + this.identityVerificationClient = new IdentityVerificationClientImpl(identityApiClient, configuration); + this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(identityApiClient, configuration); + this.amlScreeningClient = new AmlScreeningClientImpl(identityApiClient, configuration); this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); this.agenticCommerceClient = new AgenticCommerceClientImpl(this.apiClient, configuration); @@ -303,6 +304,14 @@ private ApiClient getBalancesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new BalancesApiUriStrategy(configuration)); } + private ApiClient getForwardClient(final CheckoutConfiguration configuration) { + return new ApiClientImpl(configuration, new ForwardApiUriStrategy(configuration)); + } + + private ApiClient getIdentityClient(final CheckoutConfiguration configuration) { + return new ApiClientImpl(configuration, new IdentityApiUriStrategy(configuration)); + } + private static class FilesApiUriStrategy implements UriStrategy { private final CheckoutConfiguration configuration; @@ -348,4 +357,34 @@ public URI getUri() { } + private static class ForwardApiUriStrategy implements UriStrategy { + + private final CheckoutConfiguration configuration; + + private ForwardApiUriStrategy(final CheckoutConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public URI getUri() { + return configuration.getEnvironment().getForwardApi(); + } + + } + + private static class IdentityApiUriStrategy implements UriStrategy { + + private final CheckoutConfiguration configuration; + + private IdentityApiUriStrategy(final CheckoutConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public URI getUri() { + return configuration.getEnvironment().getIdentityApi(); + } + + } + } diff --git a/src/main/java/com/checkout/CustomEnvironment.java b/src/main/java/com/checkout/CustomEnvironment.java index 856f0a9d..1a0766af 100644 --- a/src/main/java/com/checkout/CustomEnvironment.java +++ b/src/main/java/com/checkout/CustomEnvironment.java @@ -17,6 +17,10 @@ public final class CustomEnvironment implements IEnvironment { private URI balancesApi; + private URI forwardApi; + + private URI identityApi; + private URI oAuthAuthorizationApi; private boolean sandbox; diff --git a/src/main/java/com/checkout/Environment.java b/src/main/java/com/checkout/Environment.java index 48dd74a1..41ac60e4 100644 --- a/src/main/java/com/checkout/Environment.java +++ b/src/main/java/com/checkout/Environment.java @@ -13,12 +13,16 @@ public enum Environment implements IEnvironment { create("https://files.sandbox.checkout.com/"), create("https://transfers.sandbox.checkout.com/"), create("https://balances.sandbox.checkout.com/"), + create("https://forward.sandbox.checkout.com/"), + create("https://identity-verification.sandbox.checkout.com/"), create("https://access.sandbox.checkout.com/connect/token"), true), PRODUCTION(create("https://api.checkout.com/"), create("https://files.checkout.com/"), create("https://transfers.checkout.com/"), create("https://balances.checkout.com/"), + create("https://forward.checkout.com/"), + create("https://identity-verification.checkout.com/"), create("https://access.checkout.com/connect/token"), false); @@ -26,6 +30,8 @@ public enum Environment implements IEnvironment { private final URI filesApi; private final URI transfersApi; private final URI balancesApi; + private final URI forwardApi; + private final URI identityApi; private final URI oAuthAuthorizationApi; private final boolean sandbox; @@ -33,12 +39,16 @@ public enum Environment implements IEnvironment { final URI filesApi, final URI transfersApi, final URI balancesApi, + final URI forwardApi, + final URI identityApi, final URI oAuthAuthorizationApi, final boolean sandbox) { this.checkoutApi = checkoutApi; this.filesApi = filesApi; this.transfersApi = transfersApi; this.balancesApi = balancesApi; + this.forwardApi = forwardApi; + this.identityApi = identityApi; this.oAuthAuthorizationApi = oAuthAuthorizationApi; this.sandbox = sandbox; } diff --git a/src/main/java/com/checkout/EnvironmentSubdomain.java b/src/main/java/com/checkout/EnvironmentSubdomain.java index 91884735..54841a6e 100644 --- a/src/main/java/com/checkout/EnvironmentSubdomain.java +++ b/src/main/java/com/checkout/EnvironmentSubdomain.java @@ -40,7 +40,7 @@ private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) { throw new CheckoutException(e); } - Pattern pattern = Pattern.compile("^[0-9a-z]+$"); + Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$"); Matcher matcher = pattern.matcher(subdomain); if (matcher.matches()) { String host = originalUrl.getHost(); diff --git a/src/main/java/com/checkout/IEnvironment.java b/src/main/java/com/checkout/IEnvironment.java index 3241bfe5..7a6b5a22 100644 --- a/src/main/java/com/checkout/IEnvironment.java +++ b/src/main/java/com/checkout/IEnvironment.java @@ -12,6 +12,10 @@ public interface IEnvironment { URI getBalancesApi(); + URI getForwardApi(); + + URI getIdentityApi(); + URI getOAuthAuthorizationApi(); boolean isSandbox(); diff --git a/src/main/java/com/checkout/OAuthScope.java b/src/main/java/com/checkout/OAuthScope.java index 3c53ca69..25966881 100644 --- a/src/main/java/com/checkout/OAuthScope.java +++ b/src/main/java/com/checkout/OAuthScope.java @@ -58,7 +58,8 @@ public enum OAuthScope { VAULT_TOKENIZATION("vault:tokenization"), VAULT_NETWORK_TOKENS("vault:network-tokens"), FORWARD("forward"), - FORWARD_SECRETS("forward:secrets"); + FORWARD_SECRETS("forward:secrets"), + IDENTITY_VERIFICATION("identity-verification"); private final String scope; diff --git a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java index e66e170a..e8f9da6e 100644 --- a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java +++ b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java @@ -53,7 +53,7 @@ void shouldCreateConfiguration() { } @ParameterizedTest - @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma"}) + @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma", "pl-abc123", "pl-loquesea", "vkuhvk4v", "pl-vkuhvk4v"}) void shouldCreateConfigurationWithSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -65,7 +65,7 @@ void shouldCreateConfigurationWithSubdomain(String subdomain) { } @ParameterizedTest - @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1"}) + @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1", "foo-", "-foo", "ABC123", "FOO", "test-123", "foo-bar", "pl-"}) void shouldCreateConfigurationWithBadSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -88,6 +88,28 @@ void shouldCreateConfigurationWithSubdomainForProduction() { assertEquals("https://" + subdomain + ".access.checkout.com/connect/token", configuration.getEnvironmentSubdomain().getOAuthAuthorizationApi().toString()); } + @Test + void shouldHaveCorrectSandboxUrls() { + + assertEquals(create("https://api.sandbox.checkout.com/"), Environment.SANDBOX.getCheckoutApi()); + assertEquals(create("https://files.sandbox.checkout.com/"), Environment.SANDBOX.getFilesApi()); + assertEquals(create("https://transfers.sandbox.checkout.com/"), Environment.SANDBOX.getTransfersApi()); + assertEquals(create("https://balances.sandbox.checkout.com/"), Environment.SANDBOX.getBalancesApi()); + assertEquals(create("https://forward.sandbox.checkout.com/"), Environment.SANDBOX.getForwardApi()); + assertEquals(create("https://identity-verification.sandbox.checkout.com/"), Environment.SANDBOX.getIdentityApi()); + } + + @Test + void shouldHaveCorrectProductionUrls() { + + assertEquals(create("https://api.checkout.com/"), Environment.PRODUCTION.getCheckoutApi()); + assertEquals(create("https://files.checkout.com/"), Environment.PRODUCTION.getFilesApi()); + assertEquals(create("https://transfers.checkout.com/"), Environment.PRODUCTION.getTransfersApi()); + assertEquals(create("https://balances.checkout.com/"), Environment.PRODUCTION.getBalancesApi()); + assertEquals(create("https://forward.checkout.com/"), Environment.PRODUCTION.getForwardApi()); + assertEquals(create("https://identity-verification.checkout.com/"), Environment.PRODUCTION.getIdentityApi()); + } + @Test void shouldCreateConfiguration_defaultHttpClientBuilderAndExecutor() { @@ -136,6 +158,8 @@ void shouldCreateConfigurationWithCustomEnvironment() { .filesApi(create("https://the.files.uri/")) .transfersApi(create("https://the.transfers.uri/")) .balancesApi(create("https://the.balances.uri/")) + .forwardApi(create("https://the.forward.uri/")) + .identityApi(create("https://the.identity.uri/")) .build(); final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -147,6 +171,8 @@ void shouldCreateConfigurationWithCustomEnvironment() { assertEquals(environment.getFilesApi(), configuration.getEnvironment().getFilesApi()); assertEquals(environment.getTransfersApi(), configuration.getEnvironment().getTransfersApi()); assertEquals(environment.getBalancesApi(), configuration.getEnvironment().getBalancesApi()); + assertEquals(environment.getForwardApi(), configuration.getEnvironment().getForwardApi()); + assertEquals(environment.getIdentityApi(), configuration.getEnvironment().getIdentityApi()); } /** diff --git a/src/test/java/com/checkout/payments/contexts/PaymentContextsTestIT.java b/src/test/java/com/checkout/payments/contexts/PaymentContextsTestIT.java index 37e81fd1..77e2db29 100644 --- a/src/test/java/com/checkout/payments/contexts/PaymentContextsTestIT.java +++ b/src/test/java/com/checkout/payments/contexts/PaymentContextsTestIT.java @@ -1,5 +1,6 @@ package com.checkout.payments.contexts; +import com.checkout.CheckoutApiException; import com.checkout.PlatformType; import com.checkout.SandboxTestFixture; import com.checkout.TestHelper; @@ -7,10 +8,17 @@ import com.checkout.payments.PaymentType; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; import static com.checkout.payments.AbstractPaymentsTestIT.APM_SERVICE_UNAVAILABLE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.abort; class PaymentContextsTestIT extends SandboxTestFixture { @@ -21,8 +29,9 @@ class PaymentContextsTestIT extends SandboxTestFixture { @Test void shouldMakeAPaymentContextPayPalRequest() { final PaymentContextsRequest request = TestHelper.createPaymentContextsPayPalRequest(); - final PaymentContextsRequestResponse response = blocking(() -> checkoutApi.paymentContextsClient().requestPaymentContexts(request, null)); - + final PaymentContextsRequestResponse response = tolerateApmUnavailable( + () -> checkoutApi.paymentContextsClient().requestPaymentContexts(request, null)); + validatePayPalResponse(response); } @@ -52,9 +61,10 @@ void shouldMakeAPaymentContextTabbyRequest() { @Test void shouldGetAPaymentContext() { final PaymentContextsRequest request = TestHelper.createPaymentContextsPayPalRequest(); - final PaymentContextsRequestResponse paymentContextsResponse = blocking(() -> checkoutApi.paymentContextsClient().requestPaymentContexts(request, null)); + final PaymentContextsRequestResponse paymentContextsResponse = tolerateApmUnavailable( + () -> checkoutApi.paymentContextsClient().requestPaymentContexts(request, null)); final PaymentContextDetailsResponse response = blocking(() -> checkoutApi.paymentContextsClient().getPaymentContextDetails(paymentContextsResponse.getId())); - + validatePaymentContextDetails(response); } @@ -62,8 +72,9 @@ void shouldGetAPaymentContext() { @Test void shouldMakeAPaymentContextPayPalRequestSync() { final PaymentContextsRequest request = TestHelper.createPaymentContextsPayPalRequest(); - final PaymentContextsRequestResponse response = checkoutApi.paymentContextsClient().requestPaymentContextsSync(request, null); - + final PaymentContextsRequestResponse response = tolerateApmUnavailableSync( + () -> checkoutApi.paymentContextsClient().requestPaymentContextsSync(request, null)); + validatePayPalResponse(response); } @@ -93,9 +104,10 @@ void shouldMakeAPaymentContextTabbyRequestSync() { @Test void shouldGetAPaymentContextSync() { final PaymentContextsRequest request = TestHelper.createPaymentContextsPayPalRequest(); - final PaymentContextsRequestResponse paymentContextsResponse = checkoutApi.paymentContextsClient().requestPaymentContextsSync(request, null); + final PaymentContextsRequestResponse paymentContextsResponse = tolerateApmUnavailableSync( + () -> checkoutApi.paymentContextsClient().requestPaymentContextsSync(request, null)); final PaymentContextDetailsResponse response = checkoutApi.paymentContextsClient().getPaymentContextDetailsSync(paymentContextsResponse.getId()); - + validatePaymentContextDetails(response); } @@ -134,4 +146,42 @@ private void validatePaymentContextDetails(PaymentContextDetailsResponse respons assertNotNull(response.getPartnerMetadata()); assertNotNull(response.getPartnerMetadata().getOrderId()); } + + // PayPal is an APM that can be temporarily unavailable in sandbox, returning + // a 422 'apm_service_unavailable'. When that happens we skip the test instead of + // failing, since it reflects an environment outage rather than an SDK defect. + private T tolerateApmUnavailable(final Supplier> supplier) { + try { + return supplier.get().get(); + } catch (final InterruptedException | ExecutionException exception) { + if (isApmServiceUnavailable(exception.getCause())) { + abort("PayPal APM unavailable in sandbox (apm_service_unavailable) - skipping"); + } + throw new AssertionFailedError("Unexpected error", exception); + } + } + + private T tolerateApmUnavailableSync(final Supplier supplier) { + try { + return supplier.get(); + } catch (final CheckoutApiException exception) { + if (isApmServiceUnavailable(exception)) { + abort("PayPal APM unavailable in sandbox (apm_service_unavailable) - skipping"); + } + throw exception; + } + } + + @SuppressWarnings("unchecked") + private static boolean isApmServiceUnavailable(final Throwable throwable) { + if (!(throwable instanceof CheckoutApiException)) { + return false; + } + final CheckoutApiException apiException = (CheckoutApiException) throwable; + if (apiException.getErrorDetails() == null) { + return false; + } + final Object errorCodes = apiException.getErrorDetails().get("error_codes"); + return errorCodes instanceof List && ((List) errorCodes).contains(APM_SERVICE_UNAVAILABLE); + } } \ No newline at end of file