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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,39 @@ PaymentResponse response = craftgate.payment().createPayment(request);
System.out.println(String.format("Create Payment Result: %s", response));
```

### Idempotency
Mutating operations accept an optional idempotency key. Set it on the request object and the client sends it as the `x-idempotency-key` header, so a request can be safely retried (e.g. after a timeout) without the operation being performed twice — the server returns the result of the first request when it sees a repeated key.

Every request extends `BaseRequest`, so the key is available on any request via the builder:

```java
CreatePaymentRequest request = CreatePaymentRequest.builder()
.price(BigDecimal.valueOf(100))
.paidPrice(BigDecimal.valueOf(100))
.currency(Currency.TRY)
.paymentGroup(PaymentGroup.LISTING_OR_SUBSCRIPTION)
.idempotencyKey(UUID.randomUUID().toString())
// ... other fields
.build();

PaymentResponse response = craftgate.payment().createPayment(request);
```

Operations whose parameters live in the URL path (e.g. deletes) take a request object as well, so they can carry an idempotency key too:

```java
craftgate.payment().expireCheckoutPayment(ExpireCheckoutPaymentRequest.builder()
.token("456d1297-908e-4bd6-a13b-4be31a6e47d5")
.idempotencyKey(UUID.randomUUID().toString())
.build());
```

> Use a fresh key per distinct operation, and reuse the same key when retrying that operation.

> The API honours the key on `POST`, `PATCH` and `DELETE` only. It is ignored on `PUT` endpoints, so retrying one of those is not de-duplicated.

The key is sent as a header only — it is `transient`, so it never reaches the request body, the query string, or the request signature.

### Contributions
For all contributions to this client please see the contribution guide [here](CONTRIBUTING.md). By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).

Expand Down
35 changes: 26 additions & 9 deletions src/main/java/io/craftgate/adapter/BaseAdapter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.craftgate.adapter;

import io.craftgate.request.common.BaseRequest;
import io.craftgate.request.common.HashGenerator;
import io.craftgate.request.common.HeaderOptions;
import io.craftgate.request.common.RequestOptions;

import java.util.HashMap;
Expand All @@ -18,37 +20,52 @@ public abstract class BaseAdapter {
private static final String CLIENT_VERSION_HEADER_NAME = "x-client-version";
private static final String SIGNATURE_HEADER_NAME = "x-signature";
private static final String LANGUAGE_HEADER_NAME = "lang";
private static final String IDEMPOTENCY_KEY_HEADER_NAME = "x-idempotency-key";

protected final RequestOptions requestOptions;

protected BaseAdapter(RequestOptions requestOptions) {
this.requestOptions = requestOptions;
}

protected Map<String, String> createHeaders(Object request, String path, RequestOptions requestOptions) {
return createHttpHeaders(request, path, requestOptions);
protected Map<String, String> createHeaders(BaseRequest request, String path, RequestOptions requestOptions) {
return createHttpHeaders(request, path, requestOptions, request.toHeaderOptions());
}

protected Map<String, String> createHeaders(String path, RequestOptions requestOptions) {
return createHttpHeaders(null, path, requestOptions);
return createHttpHeaders(null, path, requestOptions, null);
}

private static Map<String, String> createHttpHeaders(Object request, String path, RequestOptions options) {
protected Map<String, String> createHeadersWithoutBody(String path, RequestOptions requestOptions, HeaderOptions headerOptions) {
return createHttpHeaders(null, path, requestOptions, headerOptions);
}

private static Map<String, String> createHttpHeaders(BaseRequest request, String path, RequestOptions requestOptions, HeaderOptions headerOptions) {
Map<String, String> headers = new HashMap<>();

String randomString = UUID.randomUUID().toString();
headers.put(API_KEY_HEADER_NAME, options.getApiKey());
headers.put(API_KEY_HEADER_NAME, requestOptions.getApiKey());
headers.put(RANDOM_HEADER_NAME, randomString);
headers.put(AUTH_VERSION_HEADER_NAME, API_VERSION_HEADER_VALUE);
headers.put(CLIENT_VERSION_HEADER_NAME, CLIENT_VERSION_HEADER_VALUE + ":1.0.82");
headers.put(SIGNATURE_HEADER_NAME, prepareAuthorizationString(request, path, randomString, options));
if (Objects.nonNull(options.getLanguage())) {
headers.put(LANGUAGE_HEADER_NAME, options.getLanguage());
headers.put(SIGNATURE_HEADER_NAME, prepareAuthorizationString(request, path, randomString, requestOptions));
if (Objects.nonNull(requestOptions.getLanguage())) {
headers.put(LANGUAGE_HEADER_NAME, requestOptions.getLanguage());
}
applyRequestScopedHeaders(headers, headerOptions);
return headers;
}

private static String prepareAuthorizationString(Object request, String path, String randomString, RequestOptions options) {
private static void applyRequestScopedHeaders(Map<String, String> headers, HeaderOptions headerOptions) {
if (Objects.isNull(headerOptions)) {
return;
}
if (Objects.nonNull(headerOptions.getIdempotencyKey())) {
headers.put(IDEMPOTENCY_KEY_HEADER_NAME, headerOptions.getIdempotencyKey());
}
}

private static String prepareAuthorizationString(BaseRequest request, String path, String randomString, RequestOptions options) {
return HashGenerator.generateHash(options.getBaseUrl(), options.getApiKey(), options.getSecretKey(), randomString, request, path);
}
}
24 changes: 12 additions & 12 deletions src/main/java/io/craftgate/adapter/FraudAdapter.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.craftgate.adapter;

import io.craftgate.model.FraudCheckStatus;
import io.craftgate.model.FraudValueType;
import io.craftgate.net.HttpClient;
import io.craftgate.request.*;
Expand All @@ -23,11 +22,11 @@ public FraudCheckListResponse searchFraudChecks(SearchFraudChecksRequest searchF
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), FraudCheckListResponse.class);
}

public void updateFraudCheckStatus(Long id, FraudCheckStatus fraudCheckStatus) {
String path = "/fraud/v1/fraud-checks/" + id + "/check-status";
UpdateFraudCheckRequest updateFraudCheckRequest = UpdateFraudCheckRequest.builder().checkStatus(fraudCheckStatus).build();
HttpClient.put(requestOptions.getBaseUrl() + path, createHeaders(updateFraudCheckRequest, path, requestOptions),
updateFraudCheckRequest, Void.class);
public void updateFraudCheckStatus(UpdateFraudCheckStatusRequest updateFraudCheckStatusRequest) {
String path = "/fraud/v1/fraud-checks/" + updateFraudCheckStatusRequest.getId() + "/check-status";
HttpClient.put(requestOptions.getBaseUrl() + path,
createHeaders(updateFraudCheckStatusRequest, path, requestOptions),
updateFraudCheckStatusRequest, Void.class);
}

public FraudAllValueListsResponse retrieveAllValueLists() {
Expand All @@ -45,10 +44,10 @@ public void createValueList(String listName, FraudValueType type) {
addValueToValueList(createRequest);
}

public void deleteValueList(String listName) {
String path = "/fraud/v1/value-lists/" + listName;
public void deleteValueList(DeleteValueListRequest deleteValueListRequest) {
String path = "/fraud/v1/value-lists/" + deleteValueListRequest.getListName();

HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, deleteValueListRequest.toHeaderOptions()));
}

public void addValueToValueList(FraudValueListRequest fraudValueListRequest) {
Expand All @@ -63,9 +62,10 @@ public void addCardFingerprint(AddCardFingerprintFraudValueListRequest request,
request, Void.class);
}

public void removeValueFromValueList(String listName, String valueId) {
String path = "/fraud/v1/value-lists/" + listName + "/values/" + valueId;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void removeValueFromValueList(RemoveValueFromValueListRequest removeValueFromValueListRequest) {
String path = "/fraud/v1/value-lists/" + removeValueFromValueListRequest.getListName()
+ "/values/" + removeValueFromValueListRequest.getValueId();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, removeValueFromValueListRequest.toHeaderOptions()));
}

public FraudRuleListResponse searchRules(SearchFraudRuleRequest searchFraudRuleRequest) {
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/io/craftgate/adapter/MerchantAdapter.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.craftgate.adapter;

import io.craftgate.model.PosStatus;
import io.craftgate.net.HttpClient;
import io.craftgate.request.CreateMerchantPosRequest;
import io.craftgate.request.DeleteMerchantPosRequest;
import io.craftgate.request.SearchMerchantPosRequest;
import io.craftgate.request.UpdateMerchantPosCommissionsRequest;
import io.craftgate.request.UpdateMerchantPosRequest;
import io.craftgate.request.UpdateMerchantPosStatusRequest;
import io.craftgate.request.common.RequestOptions;
import io.craftgate.request.common.RequestQueryParamsBuilder;
import io.craftgate.response.MerchantPosCommissionListResponse;
Expand All @@ -30,9 +31,10 @@ public MerchantPosResponse updateMerchantPos(Long merchantPosId, UpdateMerchantP
updateMerchantPosRequest, MerchantPosResponse.class);
}

public void updateMerchantPosStatus(Long merchantPosId, PosStatus posStatus) {
String path = "/merchant/v1/merchant-poses/" + merchantPosId + "/status/" + posStatus.name();
HttpClient.put(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), Void.class);
public void updateMerchantPosStatus(UpdateMerchantPosStatusRequest updateMerchantPosStatusRequest) {
String path = "/merchant/v1/merchant-poses/" + updateMerchantPosStatusRequest.getMerchantPosId()
+ "/status/" + updateMerchantPosStatusRequest.getPosStatus().name();
HttpClient.put(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, updateMerchantPosStatusRequest.toHeaderOptions()), Void.class);
}

public MerchantPosListResponse searchMerchantPos(SearchMerchantPosRequest searchMerchantPosRequest) {
Expand All @@ -46,9 +48,9 @@ public MerchantPosResponse retrieve(Long merchantPosId) {
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), MerchantPosResponse.class);
}

public void deleteMerchantPos(Long merchantPosId) {
String path = "/merchant/v1/merchant-poses/" + merchantPosId;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void deleteMerchantPos(DeleteMerchantPosRequest deleteMerchantPosRequest) {
String path = "/merchant/v1/merchant-poses/" + deleteMerchantPosRequest.getMerchantPosId();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, deleteMerchantPosRequest.toHeaderOptions()));
}

public MerchantPosCommissionListResponse retrieveMerchantPosCommissions(Long merchantPosId) {
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/io/craftgate/adapter/PayByLinkAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.craftgate.net.HttpClient;
import io.craftgate.request.CreateProductRequest;
import io.craftgate.request.DeleteProductRequest;
import io.craftgate.request.SearchProductsRequest;
import io.craftgate.request.UpdateProductRequest;
import io.craftgate.request.common.RequestOptions;
Expand Down Expand Up @@ -32,9 +33,9 @@ public ProductResponse retrieveProduct(Long id) {
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), ProductResponse.class);
}

public void deleteProduct(Long id) {
String path = "/craftlink/v1/products/" + id;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void deleteProduct(DeleteProductRequest deleteProductRequest) {
String path = "/craftlink/v1/products/" + deleteProductRequest.getId();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, deleteProductRequest.toHeaderOptions()));
}

public ProductListResponse searchProducts(SearchProductsRequest searchProductsRequest) {
Expand Down
18 changes: 9 additions & 9 deletions src/main/java/io/craftgate/adapter/PaymentAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ public PaymentResponse retrieveCheckoutPayment(String token) {
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), PaymentResponse.class);
}

public void expireCheckoutPayment(String token) {
String path = "/payment/v1/checkout-payments/" + token;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void expireCheckoutPayment(ExpireCheckoutPaymentRequest expireCheckoutPaymentRequest) {
String path = "/payment/v1/checkout-payments/" + expireCheckoutPaymentRequest.getToken();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, expireCheckoutPaymentRequest.toHeaderOptions()));
}

public DepositPaymentResponse createDepositPayment(CreateDepositPaymentRequest createDepositPaymentRequest) {
Expand Down Expand Up @@ -255,14 +255,14 @@ public InitBnplPaymentResponse initBnplPayment(InitBnplPaymentRequest initBnplPa
initBnplPaymentRequest, InitBnplPaymentResponse.class);
}

public PaymentResponse approveBnplPayment(Long paymentId) {
String path = "/payment/v1/bnpl-payments/" + paymentId + "/approve";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), PaymentResponse.class);
public PaymentResponse approveBnplPayment(ApproveBnplPaymentRequest approveBnplPaymentRequest) {
String path = "/payment/v1/bnpl-payments/" + approveBnplPaymentRequest.getPaymentId() + "/approve";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, approveBnplPaymentRequest.toHeaderOptions()), PaymentResponse.class);
}

public BnplPaymentVerifyResponse verifyBnplPayment(Long paymentId) {
String path = "/payment/v1/bnpl-payments/" + paymentId + "/verify";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), BnplPaymentVerifyResponse.class);
public BnplPaymentVerifyResponse verifyBnplPayment(VerifyBnplPaymentRequest verifyBnplPaymentRequest) {
String path = "/payment/v1/bnpl-payments/" + verifyBnplPaymentRequest.getPaymentId() + "/verify";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, verifyBnplPaymentRequest.toHeaderOptions()), BnplPaymentVerifyResponse.class);
}

public BnplLimitInquiryResponse bnplLimitInquiryInit(BnplLimitInquiryRequest bnplLimitInquiryRequest) {
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/io/craftgate/adapter/PaymentTokenAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.craftgate.net.HttpClient;
import io.craftgate.request.CreatePaymentTokenRequest;
import io.craftgate.request.DeletePaymentTokenRequest;
import io.craftgate.request.common.RequestOptions;
import io.craftgate.response.PaymentTokenResponse;

Expand All @@ -19,8 +20,8 @@ public PaymentTokenResponse createPaymentToken(CreatePaymentTokenRequest createP
PaymentTokenResponse.class);
}

public void deletePaymentToken(String token) {
String path = "/payment/v1/payment-tokens/" + token;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void deletePaymentToken(DeletePaymentTokenRequest deletePaymentTokenRequest) {
String path = "/payment/v1/payment-tokens/" + deletePaymentTokenRequest.getToken();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, deletePaymentTokenRequest.toHeaderOptions()));
}
}
7 changes: 4 additions & 3 deletions src/main/java/io/craftgate/adapter/SettlementAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io.craftgate.net.HttpClient;
import io.craftgate.request.CreateInstantWalletSettlementRequest;
import io.craftgate.request.CreatePayoutAccountRequest;
import io.craftgate.request.DeletePayoutAccountRequest;
import io.craftgate.request.SearchPayoutAccountRequest;
import io.craftgate.request.UpdatePayoutAccountRequest;
import io.craftgate.request.common.RequestOptions;
Expand Down Expand Up @@ -33,9 +34,9 @@ public void updatePayoutAccount(Long id, UpdatePayoutAccountRequest request) {
request, Void.class);
}

public void deletePayoutAccount(Long id) {
String path = "/settlement/v1/payout-accounts/" + id;
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions));
public void deletePayoutAccount(DeletePayoutAccountRequest deletePayoutAccountRequest) {
String path = "/settlement/v1/payout-accounts/" + deletePayoutAccountRequest.getId();
HttpClient.delete(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, deletePayoutAccountRequest.toHeaderOptions()));
}

public PayoutAccountListResponse searchPayoutAccount(SearchPayoutAccountRequest request) {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/craftgate/adapter/WalletAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ public WithdrawResponse createWithdraw(CreateWithdrawRequest request) {
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeaders(request, path, requestOptions), request, WithdrawResponse.class);
}

public WithdrawResponse cancelWithdraw(Long withdrawId) {
String path = "/wallet/v1/withdraws/" + withdrawId + "/cancel";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), WithdrawResponse.class);
public WithdrawResponse cancelWithdraw(CancelWithdrawRequest cancelWithdrawRequest) {
String path = "/wallet/v1/withdraws/" + cancelWithdrawRequest.getWithdrawId() + "/cancel";
return HttpClient.post(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(path, requestOptions, cancelWithdrawRequest.toHeaderOptions()), WithdrawResponse.class);
}

public WithdrawResponse retrieveWithdraw(Long withdrawId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
import io.craftgate.model.FraudOperation;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import lombok.EqualsAndHashCode;
import io.craftgate.request.common.BaseRequest;

@Builder
@SuperBuilder
@Data
public class AddCardFingerprintFraudValueListRequest {
@EqualsAndHashCode(callSuper = false)
public class AddCardFingerprintFraudValueListRequest extends BaseRequest {

String label;
Integer durationInSeconds;
@Builder.Default
FraudOperation operation = FraudOperation.PAYMENT;
String operationId;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package io.craftgate.request;

import lombok.Builder;
import lombok.Data;
import lombok.experimental.SuperBuilder;
import lombok.EqualsAndHashCode;
import io.craftgate.request.common.BaseRequest;

@Data
@Builder
public class ApplePayMerchantSessionCreateRequest {
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public class ApplePayMerchantSessionCreateRequest extends BaseRequest {

private String merchantIdentifier;
private String displayName;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/craftgate/request/ApproveBnplPaymentRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.craftgate.request;

import io.craftgate.request.common.BaseRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;

@Data
@EqualsAndHashCode(callSuper = false)
@SuperBuilder
public class ApproveBnplPaymentRequest extends BaseRequest {

private Long paymentId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import lombok.Data;

import java.util.Set;
import lombok.experimental.SuperBuilder;
import lombok.EqualsAndHashCode;
import io.craftgate.request.common.BaseRequest;

@Data
@Builder
public class ApprovePaymentTransactionsRequest {
@SuperBuilder
@EqualsAndHashCode(callSuper = false)
public class ApprovePaymentTransactionsRequest extends BaseRequest {

private Set<Long> paymentTransactionIds;

Expand Down
Loading
Loading