Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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