Skip to content
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ If you don't have your own API keys, you can sign up for a test account [here](h

**PLEASE NEVER SHARE OR PUBLISH YOUR CHECKOUT CREDENTIALS.**

### Subdomain value

Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`. When `environmentSubdomain` is set the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID.

### Default

Default keys client instantiation can be done as follows:
Expand All @@ -99,7 +103,7 @@ public static void main(String[] args) {
.publicKey("public_key") // optional, only required for operations related with tokens
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.executor() // optional for a custom Executor Service
.build();

Expand All @@ -125,7 +129,7 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
//.clientCredentials(new URI("https://access.sandbox.checkout.com/connect/token"), "client_id", "client_secret")
.scopes(OAuthScope.GATEWAY, OAuthScope.VAULT, OAuthScope.FX)
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.executor() // optional for a custom Executor Service
.build();

Expand All @@ -149,7 +153,7 @@ public static void main(String[] args) {
.publicKey("public_key") // optional, only required for operations related with tokens
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // optional for the Previous platform, Merchant-specific DNS name
.executor() // optional for a custom Executor Service
.build();

Expand Down Expand Up @@ -399,7 +403,7 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
.staticKeys()
.secretKey("secret_key")
.environment(Environment.PRODUCTION) // required
.environmentSubdomain("subdomain") // optional, Merchant-specific DNS name
.environmentSubdomain("subdomain") // required, Merchant-specific DNS name, the first 8 characters of your client ID
.httpClientBuilder(customHttpClient) // optional for a custom HttpClient
.build();
```
Expand Down Expand Up @@ -656,6 +660,23 @@ final CheckoutApi checkoutApi = CheckoutSdk.builder()
- All resilience patterns are optional - configure only what you need
- Rate limiter helps respect API rate limits and prevent overwhelming the service

## Legacy domain (emergency use only)

> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated.

If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out by calling `useLegacyDomain()` instead of `environmentSubdomain(...)`:

```java
final CheckoutApi checkoutApi = CheckoutSdk.builder()
.staticKeys()
.secretKey("secret_key")
.environment(Environment.SANDBOX)
.useLegacyDomain() // deprecated, emergency fallback only
.build();
```

This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The method is annotated `@Deprecated` and produces a compile-time warning. Exactly one of `environmentSubdomain(...)` or `useLegacyDomain()` must be set: the SDK throws a `CheckoutArgumentException` if both, or neither, are set. The Previous (ABC) platform predates merchant-specific subdomains and is exempt from this requirement.

## Code of Conduct

Please refer to [Code of Conduct](CODE_OF_CONDUCT.md)
Expand Down
42 changes: 39 additions & 3 deletions src/main/java/com/checkout/AbstractCheckoutSdkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public abstract class AbstractCheckoutSdkBuilder<T extends CheckoutApiClient> {

protected HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
private IEnvironment environment;
private EnvironmentSubdomain environmentSubdomain;
private String subdomain;
private boolean useLegacyDomain;
private Executor executor = ForkJoinPool.commonPool();
private TransportConfiguration transportConfiguration;
private Boolean recordTelemetry = true;
Expand All @@ -25,7 +26,23 @@ public AbstractCheckoutSdkBuilder<T> environmentSubdomain(final String subdomain
if (subdomain == null) {
throw new CheckoutArgumentException("subdomain must be specified");
}
this.environmentSubdomain = new EnvironmentSubdomain(this.environment, subdomain);
this.subdomain = subdomain;
return this;
}

/**
* Opts out of the merchant-specific subdomain, sending every request to the shared
* hosts instead ({@code api.checkout.com} and {@code access.checkout.com}, or their
* sandbox equivalents).
*
* @deprecated this is an emergency fallback for the rare case where the
* merchant-specific subdomain cannot be used, and will be removed in a future release.
* Call {@link #environmentSubdomain(String)} instead.
* See <a href="https://api-reference.checkout.com/#section/Base-URLs">Base URLs</a>.
*/
@Deprecated
public AbstractCheckoutSdkBuilder<T> useLegacyDomain() {
this.useLegacyDomain = true;
return this;
}

Expand All @@ -49,7 +66,16 @@ protected IEnvironment getEnvironment() {
}

protected EnvironmentSubdomain getEnvironmentSubdomain() {
return environmentSubdomain;
return subdomain != null ? new EnvironmentSubdomain(environment, subdomain) : null;
}

/**
* Whether this builder requires the merchant-specific subdomain to be configured.
* The Previous (ABC) platform predates merchant-specific subdomains, so it overrides
* this to {@code false}.
*/
protected boolean requiresEnvironmentSubdomain() {
return true;
}

public AbstractCheckoutSdkBuilder<T> recordTelemetry(final Boolean recordTelemetry) {
Expand All @@ -73,13 +99,23 @@ protected CheckoutConfiguration getCheckoutConfiguration() {
if (environment == null) {
throw new CheckoutArgumentException("environment must be specified");
}
validateEnvironmentSettings();
final SdkCredentials sdkCredentials = getSdkCredentials();
if (transportConfiguration == null) {
transportConfiguration = new DefaultTransportConfiguration();
}
return buildCheckoutConfiguration(sdkCredentials);
}

private void validateEnvironmentSettings() {
if (subdomain != null && useLegacyDomain) {
throw new CheckoutArgumentException("environmentSubdomain and useLegacyDomain cannot both be set - provide only your merchant-specific subdomain");
}
if (subdomain == null && !useLegacyDomain && requiresEnvironmentSubdomain()) {
throw new CheckoutArgumentException("environmentSubdomain is required - provide your merchant-specific subdomain (the first 8 characters of your client ID, see https://api-reference.checkout.com/#section/Base-URLs), or call useLegacyDomain() to opt out only if merchant specific sub domains are causing issues");
}
}

private CheckoutConfiguration buildCheckoutConfiguration(final SdkCredentials sdkCredentials) {
return new DefaultCheckoutConfiguration(sdkCredentials, getEnvironment(), getEnvironmentSubdomain(), httpClientBuilder, executor, transportConfiguration, recordTelemetry, synchronous, resilience4jConfiguration);
}
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/checkout/CheckoutPreviousSdkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ public static class CheckoutStaticKeysSdkBuilder extends AbstractCheckoutSdkBuil
private String publicKey;
private String secretKey;

// The Previous (ABC) platform predates merchant-specific subdomains, so it is exempt
// from the mandatory environmentSubdomain/useLegacyDomain configuration.
@Override
protected boolean requiresEnvironmentSubdomain() {
return false;
}

public CheckoutStaticKeysSdkBuilder publicKey(final String publicKey) {
this.publicKey = publicKey;
return this;
Expand Down
35 changes: 14 additions & 21 deletions src/main/java/com/checkout/EnvironmentSubdomain.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,29 @@ public URI getOAuthAuthorizationApi() {
}

/**
* Applies subdomain transformation to any given URI.
* If the subdomain is valid (alphanumeric pattern), prepends it to the host.
* Otherwise, returns the original URI unchanged.
* Applies subdomain transformation to any given URI, prepending the subdomain to the host.
*
* @param originalUrl the original URI to transform
* @param subdomain the subdomain to prepend
* @return the transformed URI with subdomain, or original URI if subdomain is invalid
* @return the transformed URI with subdomain
* @throws CheckoutArgumentException if the subdomain is not a valid merchant-specific subdomain
*/
private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) {
URI newEnvironment = null;
Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$");
Matcher matcher = subdomain == null ? null : pattern.matcher(subdomain);
if (matcher == null || !matcher.matches()) {
throw new CheckoutArgumentException("invalid environment subdomain - provide your merchant-specific subdomain, the first 8 characters of your client ID (see https://api-reference.checkout.com/#section/Base-URLs)");
}

String host = originalUrl.getHost();
String scheme = originalUrl.getScheme();
int port = originalUrl.getPort();
String newHost = subdomain + "." + host;
try {
newEnvironment = new URI(originalUrl.toString());
return new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}

Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$");
Matcher matcher = pattern.matcher(subdomain);
if (matcher.matches()) {
String host = originalUrl.getHost();
String scheme = originalUrl.getScheme();
int port = originalUrl.getPort();
String newHost = subdomain + "." + host;
try {
newEnvironment = new URI(scheme, null, newHost, port, originalUrl.getPath(), originalUrl.getQuery(), originalUrl.getFragment());
} catch (final URISyntaxException e) {
throw new CheckoutException(e);
}
}
return newEnvironment;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void shouldCreateCheckoutApiWithSynchronousMode() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.build();

Expand All @@ -35,6 +36,7 @@ void shouldCreateCheckoutApiWithResilience4jConfiguration() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.resilience4jConfiguration(resilience4jConfig)
.build();

Expand All @@ -49,6 +51,7 @@ void shouldCreateCheckoutApiWithSynchronousAndResilience4j() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.resilience4jConfiguration(resilience4jConfig)
.build();
Expand All @@ -63,6 +66,7 @@ void shouldCreateCheckoutApiWithoutNewParameters() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();

assertNotNull(checkoutApi);
Expand Down Expand Up @@ -90,6 +94,7 @@ void shouldCreateCheckoutApiWithCustomResilience4jConfiguration() {
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.synchronous(true)
.resilience4jConfiguration(resilience4jConfig)
.build();
Expand Down
79 changes: 79 additions & 0 deletions src/test/java/com/checkout/CheckoutSdkBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static com.checkout.TestHelper.VALID_DEFAULT_SK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

Expand All @@ -23,13 +24,15 @@
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();

assertNotNull(checkoutApi1);

final CheckoutApi checkoutApi2 = new CheckoutSdkBuilder().staticKeys()
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();

assertNotNull(checkoutApi2);
Expand Down Expand Up @@ -66,6 +69,7 @@
.clientCredentials(new URI("test"), "client_id", "client_secret")
.scopes(OAuthScope.GATEWAY)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();
fail();
} catch (final CheckoutException e) {
Expand Down Expand Up @@ -94,6 +98,78 @@

}

@SuppressWarnings("deprecation")
@Test
void shouldCreateStaticKeysCheckoutSdkWithLegacyDomain() {

final CheckoutApi checkoutApi = new CheckoutSdkBuilder().staticKeys()
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.useLegacyDomain()
Comment on lines +105 to +109
.build();

assertNotNull(checkoutApi);

}

@Test
void shouldFailToCreateCheckoutSdkWithoutSubdomainOrLegacyDomain() {

final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class,
() -> new CheckoutSdkBuilder().staticKeys()
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.build());

assertTrue(exception.getMessage().contains("environmentSubdomain is required"));

}

@SuppressWarnings("deprecation")
@Test
void shouldFailToCreateCheckoutSdkWithBothSubdomainAndLegacyDomain() {

final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class,
() -> new CheckoutSdkBuilder().staticKeys()
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.useLegacyDomain()
Comment on lines +135 to +140
.build());

assertTrue(exception.getMessage().contains("cannot both be set"));

}

@Test
void shouldFailToCreateCheckoutSdkWithInvalidSubdomain() {

final CheckoutArgumentException exception = assertThrows(CheckoutArgumentException.class,
() -> new CheckoutSdkBuilder().staticKeys()
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("not a subdomain")
.build());

assertTrue(exception.getMessage().contains("invalid environment subdomain"));

}

@Test
void shouldCreatePreviousSdkWithoutSubdomain() {

assertNotNull(new CheckoutSdkBuilder().previous().staticKeys()
.publicKey(TestHelper.VALID_PREVIOUS_PK)
.secretKey(TestHelper.VALID_PREVIOUS_SK)
.environment(Environment.SANDBOX)
.build());

}

@Test
void shouldFailToCreateCheckoutSdks() {

Expand All @@ -102,6 +178,7 @@
.publicKey(INVALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();
} catch (final Exception e) {
assertTrue(e instanceof CheckoutArgumentException);
Expand All @@ -113,6 +190,7 @@
.publicKey(VALID_DEFAULT_PK)
.secretKey(INVALID_DEFAULT_SK)
.environment(Environment.SANDBOX)
.environmentSubdomain("1234doma")
.build();
} catch (final Exception e) {
assertTrue(e instanceof CheckoutArgumentException);
Expand All @@ -123,6 +201,7 @@
new CheckoutSdkBuilder().staticKeys()
.publicKey(VALID_DEFAULT_PK)
.secretKey(VALID_DEFAULT_SK)
.environmentSubdomain("1234doma")
.build();
} catch (final Exception e) {
assertTrue(e instanceof CheckoutArgumentException);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ private CheckoutApi buildCheckoutApi(CloseableHttpClient httpClientMock, boolean
.secretKey(requireNonNull(System.getenv("CHECKOUT_DEFAULT_SECRET_KEY")))
.recordTelemetry(telemetryEnabled)
.environment(SANDBOX)
.environmentSubdomain("1234doma")
.httpClientBuilder(httpClientBuilderMock)
.build();
}
Expand Down
Loading
Loading