Skip to content
Merged
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,43 @@ 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`, which carries a `HeaderOptions` object, 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)
.headerOptions(HeaderOptions.builder()
.idempotencyKey(UUID.randomUUID().toString())
.build())
// ... 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")
.headerOptions(HeaderOptions.builder()
.idempotencyKey(UUID.randomUUID().toString())
.build())
.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.

`HeaderOptions` is sent as headers only — it is excluded from serialization, 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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public BankAccountTrackingAdapter(RequestOptions requestOptions) {
public BankAccountTrackingRecordListResponse searchRecords(SearchBankAccountTrackingRecordsRequest request) {
String query = RequestQueryParamsBuilder.buildQueryParam(request);
String path = "/bank-account-tracking/v1/merchant-bank-account-trackings/records" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), BankAccountTrackingRecordListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(request, path, requestOptions), BankAccountTrackingRecordListResponse.class);
}

public BankAccountTrackingRecordResponse retrieveRecord(Long id) {
Expand Down
36 changes: 25 additions & 11 deletions src/main/java/io/craftgate/adapter/BaseAdapter.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.craftgate.adapter;

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

import java.util.HashMap;
import java.util.Map;
Expand All @@ -18,37 +17,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.getHeaderOptions());
}

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(BaseRequest request, String path, RequestOptions requestOptions) {
return createHttpHeaders(null, path, requestOptions, request.getHeaderOptions());
}

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()) && !headerOptions.getIdempotencyKey().isEmpty()) {
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);
}
}
6 changes: 3 additions & 3 deletions src/main/java/io/craftgate/adapter/FileReportingAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public FileReportingAdapter(RequestOptions requestOptions) {
public byte[] retrieveDailyTransactionReport(RetrieveDailyTransactionReportRequest retrieveDailyTransactionReportRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(retrieveDailyTransactionReportRequest);
String path = "/file-reporting/v1/transaction-reports" + query;
Map<String, String> headers = createHeaders(path, requestOptions);
Map<String, String> headers = createHeadersWithoutBody(retrieveDailyTransactionReportRequest, path, requestOptions);
headers.put(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
return HttpClient.get(requestOptions.getBaseUrl() + path, headers, byte[].class);
}

public byte[] retrieveDailyPaymentReport(RetrieveDailyPaymentReportRequest retrieveDailyPaymentReportRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(retrieveDailyPaymentReportRequest);
String path = "/file-reporting/v1/payment-reports" + query;
Map<String, String> headers = createHeaders(path, requestOptions);
Map<String, String> headers = createHeadersWithoutBody(retrieveDailyPaymentReportRequest, path, requestOptions);
headers.put(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
return HttpClient.get(requestOptions.getBaseUrl() + path, headers, byte[].class);
}
Expand All @@ -48,7 +48,7 @@ public ReportDemandResponse createReport(CreateReportRequest request) {
public byte[] retrieveReport(RetrieveReportRequest retrieveReportRequest, Long reportId) {
String query = RequestQueryParamsBuilder.buildQueryParam(retrieveReportRequest);
String path = "/file-reporting/v1/reports/" + reportId + query;
Map<String, String> headers = createHeaders(path, requestOptions);
Map<String, String> headers = createHeadersWithoutBody(retrieveReportRequest, path, requestOptions);
headers.put(CONTENT_TYPE, APPLICATION_OCTET_STREAM);
return HttpClient.get(requestOptions.getBaseUrl() + path, headers, byte[].class);
}
Expand Down
28 changes: 14 additions & 14 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 @@ -20,14 +19,14 @@ public FraudAdapter(RequestOptions requestOptions) {
public FraudCheckListResponse searchFraudChecks(SearchFraudChecksRequest searchFraudChecksRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchFraudChecksRequest);
String path = "/fraud/v1/fraud-checks" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), FraudCheckListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchFraudChecksRequest, 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(deleteValueListRequest, path, requestOptions));
}

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

public FraudRuleListResponse searchRules(SearchFraudRuleRequest searchFraudRuleRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchFraudRuleRequest);
String path = "/fraud/v1/rules" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), FraudRuleListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchFraudRuleRequest, path, requestOptions), FraudRuleListResponse.class);
}
}
2 changes: 1 addition & 1 deletion src/main/java/io/craftgate/adapter/InstallmentAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public InstallmentAdapter(RequestOptions requestOptions) {
public InstallmentListResponse searchInstallments(SearchInstallmentsRequest searchInstallmentsRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchInstallmentsRequest);
String path = "/installment/v1/installments" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), InstallmentListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchInstallmentsRequest, path, requestOptions), InstallmentListResponse.class);
}

public BinNumberResponse retrieveBinNumber(String binNumber) {
Expand Down
18 changes: 10 additions & 8 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,25 +31,26 @@ 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(updateMerchantPosStatusRequest, path, requestOptions), Void.class);
}

public MerchantPosListResponse searchMerchantPos(SearchMerchantPosRequest searchMerchantPosRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchMerchantPosRequest);
String path = "/merchant/v1/merchant-poses" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), MerchantPosListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchMerchantPosRequest, path, requestOptions), MerchantPosListResponse.class);
}

public MerchantPosResponse retrieve(Long merchantPosId) {
String path = "/merchant/v1/merchant-poses/" + 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(deleteMerchantPosRequest, path, requestOptions));
}

public MerchantPosCommissionListResponse retrieveMerchantPosCommissions(Long merchantPosId) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/craftgate/adapter/OnboardingAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public MemberResponse retrieveMember(Long id) {
public MemberListResponse searchMembers(SearchMembersRequest searchMembersRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchMembersRequest);
String path = "/onboarding/v1/members" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), MemberListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchMembersRequest, path, requestOptions), MemberListResponse.class);
}

public CreateMerchantResponse createMerchant(CreateMerchantRequest createMerchantRequest) {
Expand Down
9 changes: 5 additions & 4 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,14 +33,14 @@ 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(deleteProductRequest, path, requestOptions));
}

public ProductListResponse searchProducts(SearchProductsRequest searchProductsRequest) {
String query = RequestQueryParamsBuilder.buildQueryParam(searchProductsRequest);
String path = "/craftlink/v1/products" + query;
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeaders(path, requestOptions), ProductListResponse.class);
return HttpClient.get(requestOptions.getBaseUrl() + path, createHeadersWithoutBody(searchProductsRequest, path, requestOptions), ProductListResponse.class);
}
}
Loading
Loading