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

### Idempotency
Mutating operations (`POST`/`PUT`/`DELETE`) 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.

### 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
29 changes: 25 additions & 4 deletions src/main/java/io/craftgate/adapter/BaseAdapter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.craftgate.adapter;

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

Expand All @@ -18,22 +19,39 @@ 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);
/**
* Headers for a request that carries a body. The body is hashed for the signature and its
* idempotency key, when present, is sent as a header.
*/
protected Map<String, String> createHeaders(BaseRequest request, String path, RequestOptions requestOptions) {
return createHttpHeaders(request, path, requestOptions, request.getIdempotencyKey());
}

/**
* Headers for a body-less request (e.g. GET). No body is hashed and no idempotency key is sent.
*/
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) {
/**
* Headers for a body-less mutating request (e.g. DELETE, or a POST/PUT whose parameters live in
* the path). The {@code request} wrapper is <b>not</b> hashed or sent as a body — only its
* idempotency key is used, so the signature stays identical to the body-less call the server expects.
*/
protected Map<String, String> createHeaders(String path, RequestOptions requestOptions, BaseRequest request) {
return createHttpHeaders(null, path, requestOptions, request.getIdempotencyKey());
}

private static Map<String, String> createHttpHeaders(Object request, String path, RequestOptions options, String idempotencyKey) {
Map<String, String> headers = new HashMap<>();

String randomString = UUID.randomUUID().toString();
Expand All @@ -45,6 +63,9 @@ private static Map<String, String> createHttpHeaders(Object request, String path
if (Objects.nonNull(options.getLanguage())) {
headers.put(LANGUAGE_HEADER_NAME, options.getLanguage());
}
if (Objects.nonNull(idempotencyKey)) {
headers.put(IDEMPOTENCY_KEY_HEADER_NAME, idempotencyKey);
}
return headers;
}

Expand Down
13 changes: 7 additions & 6 deletions src/main/java/io/craftgate/adapter/FraudAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,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, createHeaders(path, requestOptions, deleteValueListRequest));
}

public void addValueToValueList(FraudValueListRequest fraudValueListRequest) {
Expand All @@ -63,9 +63,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, createHeaders(path, requestOptions, removeValueFromValueListRequest));
}

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, createHeaders(path, requestOptions, updateMerchantPosStatusRequest), 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, createHeaders(path, requestOptions, deleteMerchantPosRequest));
}

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, createHeaders(path, requestOptions, deleteProductRequest));
}

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, createHeaders(path, requestOptions, expireCheckoutPaymentRequest));
}

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, createHeaders(path, requestOptions, approveBnplPaymentRequest), 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, createHeaders(path, requestOptions, verifyBnplPaymentRequest), 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, createHeaders(path, requestOptions, deletePaymentTokenRequest));
}
}
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, createHeaders(path, requestOptions, deletePayoutAccountRequest));
}

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, createHeaders(path, requestOptions, cancelWithdrawRequest), WithdrawResponse.class);
}

public WithdrawResponse retrieveWithdraw(Long withdrawId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@


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;
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@

import io.craftgate.model.ApmType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

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

@Data
@Builder
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class BnplLimitInquiryRequest {
@EqualsAndHashCode(callSuper = false)
public class BnplLimitInquiryRequest extends BaseRequest {

private ApmType apmType;
private Long merchantApmId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import io.craftgate.model.ApmType;
import io.craftgate.model.Currency;
import io.craftgate.request.dto.BnplPaymentCartItem;
import lombok.Builder;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import lombok.experimental.SuperBuilder;
import lombok.EqualsAndHashCode;
import io.craftgate.request.common.BaseRequest;

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

private ApmType apmType;
private Long merchantApmId;
Expand Down
Loading
Loading