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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,40 @@ craftgate.payment().createPayment(request)
});
```

## 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 type includes `BaseRequest`, so the key is available on any request:

```javascript
const { randomUUID } = require('crypto');

craftgate.payment().createPayment({
price: 100.0,
paidPrice: 100.0,
currency: Craftgate.Model.Currency.Turkish_Lira,
paymentGroup: Craftgate.Model.PaymentGroup.ListingOrSubscription,
idempotencyKey: randomUUID(),
// ... other fields
});
```

Operations whose parameters live in the URL path take a request object as well, so they can carry a key too:

```javascript
craftgate.payment().expireCheckoutPayment({
token: '456d1297-908e-4bd6-a13b-4be31a6e47d5',
idempotencyKey: randomUUID()
});
```

> 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 never appears in the request body, the query string, or the request signature. Your request object is not modified, so you can pass the very same object again to retry.

## Development
To contribute to the project, please see our guidelines at [CONTRIBUTING](./CONTRIBUTING.md). By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).

Expand Down
2 changes: 1 addition & 1 deletion samples/fraud/DeleteFraudValueList.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.fraud().deleteValueList("ipList")
craftgate.fraud().deleteValueList({listName: "ipList"})
.then(() => console.info('Fraud value list deleted'))
.catch(err => console.error('Failed to delete fraud value list', err));
5 changes: 4 additions & 1 deletion samples/fraud/RemoveValueFromFraudValueList.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.fraud().removeValueFromValueList("ipList", "5758a029-4a82-4e47-a0cf-36d34052b8e5")
craftgate.fraud().removeValueFromValueList({
listName: "ipList",
valueId: "5758a029-4a82-4e47-a0cf-36d34052b8e5"
})
.then(() => console.info('Value removed from fraud value list'))
.catch(err => console.error('Failed to remove value from fraud value list', err));
5 changes: 4 additions & 1 deletion samples/fraud/UpdateFraudCheckStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.fraud().updateFraudCheckStatus(257, Craftgate.Model.FraudCheckStatus.Fraud)
craftgate.fraud().updateFraudCheckStatus({
id: 257,
checkStatus: Craftgate.Model.FraudCheckStatus.Fraud
})
.then(() => console.info('Fraud check status updated'))
.catch(err => console.error('Failed to update fraud check status', err));
2 changes: 1 addition & 1 deletion samples/merchant/DeleteMerchantPos.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.merchant().deleteMerchantPos(10)
craftgate.merchant().deleteMerchantPos({merchantPosId: 10})
.then(result => console.info("Merchant pos deleted"))
.catch(err => console.error("Failed to delete merchant pos", err));
5 changes: 4 additions & 1 deletion samples/merchant/UpdateMerchantPosStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.merchant().updateMerchantPosStatus(11, Craftgate.Model.PosStatus.Passive)
craftgate.merchant().updateMerchantPosStatus({
merchantPosId: 11,
posStatus: Craftgate.Model.PosStatus.Passive
})
.then(result => console.info("Merchant pos status updated"))
.catch(err => console.error("Failed to update merchant pos status", err));
2 changes: 1 addition & 1 deletion samples/payByLink/DeleteProduct.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ const craftgate = new Craftgate.Client({
baseUrl: "https://sandbox-api.craftgate.io"
});

craftgate.payByLink().deleteProduct(1)
craftgate.payByLink().deleteProduct({id: 1})
.then(result => console.info("Product successfully deleted"))
.catch(err => console.error("Delete product failed", err));
2 changes: 1 addition & 1 deletion samples/payment/ApproveBnplPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.payment().approveBnplPayment(1)
craftgate.payment().approveBnplPayment({paymentId: 1})
.then(results => console.info('Approve bnpl payment response ', results))
.catch(err => console.error('Failed to approve bnpl payment', err));
2 changes: 1 addition & 1 deletion samples/payment/ExpireCheckoutPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const craftgate = new Craftgate.Client({
});

// change checkout payment token below with a valid one
craftgate.payment().expireCheckoutPayment('456d1297-908e-4bd6-a13b-4be31a6e47d5')
craftgate.payment().expireCheckoutPayment({token: '456d1297-908e-4bd6-a13b-4be31a6e47d5'})
.then(() => console.info('Checkout payment token expired'))
.catch(err => console.error('Failed to retrieve checkout payment', err));
2 changes: 1 addition & 1 deletion samples/payment/VerifyBnplPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.payment().verifyBnplPayment(1)
craftgate.payment().verifyBnplPayment({paymentId: 1})
.then(results => console.info('Verify bnpl payment response ', results))
.catch(err => console.error('Failed to verify bnpl payment', err));
2 changes: 1 addition & 1 deletion samples/settlement/DeletePayoutAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ const craftgate = new Craftgate.Client({
baseUrl: 'https://sandbox-api.craftgate.io'
});

craftgate.settlement().deletePayoutAccount(11)
craftgate.settlement().deletePayoutAccount({id: 11})
.catch(err => console.error('Failed to delete payout account', err));
2 changes: 1 addition & 1 deletion samples/wallet/CancelWithdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ const craftgate = new Craftgate.Client({
});

// change withdraw id below with valid withdraw id
craftgate.wallet().cancelWithdraw(1)
craftgate.wallet().cancelWithdraw({withdrawId: 1})
.then(result => console.info("Withdraw cancelled", result))
.catch(err => console.error("Failed to cancel withdraw", err));
18 changes: 10 additions & 8 deletions src/adapter/FraudAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import FraudCheckStatus from '../model/FraudCheckStatus';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';
import FraudValueType from '../model/FraudValueType';

import AddCardFingerprintFraudValueListRequest from '../request/AddFraudValueListRequest';
import DeleteValueListRequest from '../request/DeleteValueListRequest';
import FraudValueListRequest from '../request/FraudValueListRequest';
import RemoveValueFromValueListRequest from '../request/RemoveValueFromValueListRequest';
import SearchFraudChecksRequest from '../request/SearchFraudChecksRequest';
import SearchFraudRuleRequest from '../request/SearchFraudRuleRequest';
import UpdateFraudCheckStatusRequest from '../request/UpdateFraudCheckStatusRequest';

import FraudAllValueListsResponse from '../response/FraudAllValueListsResponse';
import FraudCheckListResponse from '../response/FraudCheckListResponse';
Expand All @@ -27,8 +29,8 @@ export default class FraudAdapter extends BaseAdapter {
return this._client.get('/fraud/v1/rules', request);
}

async updateFraudCheckStatus(id: number, fraudCheckStatus: FraudCheckStatus): Promise<void> {
await this._client.put(`/fraud/v1/fraud-checks/${id}/check-status`, {checkStatus: fraudCheckStatus});
async updateFraudCheckStatus(request: UpdateFraudCheckStatusRequest): Promise<void> {
await this._client.put(`/fraud/v1/fraud-checks/${request.id}/check-status`, {checkStatus: request.checkStatus}, requestScopedConfig(request));
}

async retrieveAllValueLists(): Promise<FraudAllValueListsResponse> {
Expand All @@ -46,8 +48,8 @@ export default class FraudAdapter extends BaseAdapter {
});
}

async deleteValueList(listName: string): Promise<void> {
await this._client.delete(`/fraud/v1/value-lists/${listName}`);
async deleteValueList(request: DeleteValueListRequest): Promise<void> {
await this._client.delete(`/fraud/v1/value-lists/${request.listName}`, undefined, requestScopedConfig(request));
}

async addValueToValueList(request: FraudValueListRequest): Promise<void> {
Expand All @@ -58,7 +60,7 @@ export default class FraudAdapter extends BaseAdapter {
await this._client.post(`/fraud/v1/value-lists/${listName}/card-fingerprints`, request);
}

async removeValueFromValueList(listName: string, valueId: string): Promise<void> {
await this._client.delete(`/fraud/v1/value-lists/${listName}/values/${valueId}`);
async removeValueFromValueList(request: RemoveValueFromValueListRequest): Promise<void> {
await this._client.delete(`/fraud/v1/value-lists/${request.listName}/values/${request.valueId}`, undefined, requestScopedConfig(request));
}
}
13 changes: 7 additions & 6 deletions src/adapter/MerchantAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import PosStatus from '../model/PosStatus';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';

import CreateMerchantPosRequest from '../request/CreateMerchantPosRequest';
import DeleteMerchantPosRequest from '../request/DeleteMerchantPosRequest';
import SearchMerchantPosRequest from '../request/SearchMerchantPosRequest';
import UpdateMerchantPosCommissionRequest from '../request/UpdateMerchantPosCommissionRequest';
import UpdateMerchantPosRequest from '../request/UpdateMerchantPosRequest';
import UpdateMerchantPosStatusRequest from '../request/UpdateMerchantPosStatusRequest';

import DataResponse from '../response/DataResponse';
import MerchantPosCommissionResponse from '../response/MerchantPosCommissionResponse';
Expand All @@ -25,12 +26,12 @@ export default class MerchantAdapter extends BaseAdapter {
return this._client.get(`/merchant/v1/merchant-poses/${id}`);
}

async deleteMerchantPos(id: number): Promise<void> {
return this._client.delete(`/merchant/v1/merchant-poses/${id}`);
async deleteMerchantPos(request: DeleteMerchantPosRequest): Promise<void> {
return this._client.delete(`/merchant/v1/merchant-poses/${request.merchantPosId}`, undefined, requestScopedConfig(request));
}

async updateMerchantPosStatus(id: number, status: PosStatus): Promise<void> {
return this._client.put(`/merchant/v1/merchant-poses/${id}/status/${status}`);
async updateMerchantPosStatus(request: UpdateMerchantPosStatusRequest): Promise<void> {
return this._client.put(`/merchant/v1/merchant-poses/${request.merchantPosId}/status/${request.posStatus}`, undefined, requestScopedConfig(request));
}

async updateMerchantPos(id: number, request: UpdateMerchantPosRequest): Promise<MerchantPosResponse> {
Expand Down
7 changes: 4 additions & 3 deletions src/adapter/PayByLinkApiAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';

import CreateProductRequest from '../request/CreateProductRequest';
import DeleteProductRequest from '../request/DeleteProductRequest';
import SearchProductsRequest from '../request/SearchProductsRequest';
import UpdateProductRequest from '../request/UpdateProductRequest';

Expand All @@ -26,8 +27,8 @@ export default class PayByLinkApiAdapter extends BaseAdapter {
return this._client.get(`/craftlink/v1/products/${productId}`);
}

async deleteProduct(productId: number): Promise<void> {
await this._client.delete(`/craftlink/v1/products/${productId}`);
async deleteProduct(request: DeleteProductRequest): Promise<void> {
await this._client.delete(`/craftlink/v1/products/${request.id}`, undefined, requestScopedConfig(request));
}

async searchProducts(request: SearchProductsRequest): Promise<DataResponse<ProductResponse>> {
Expand Down
17 changes: 10 additions & 7 deletions src/adapter/PaymentAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';
import {calculateHash} from '../lib/utils';

import ApplePayMerchantSessionCreateRequest from '../request/ApplePayMerchantSessionCreateRequest';
import ApproveBnplPaymentRequest from '../request/ApproveBnplPaymentRequest';
import ApprovePaymentTransactionsRequest from '../request/ApprovePaymentTransactionsRequest';
import BnplPaymentOfferRequest from '../request/BnplPaymentOfferRequest';
import CloneCardRequest from '../request/CloneCardRequest';
Expand All @@ -15,6 +16,7 @@ import CreateFundTransferDepositPaymentRequest from '../request/CreateFundTransf
import CreatePaymentRequest from '../request/CreatePaymentRequest';
import DeleteStoredCardRequest from '../request/DeleteStoredCardRequest';
import DisapprovePaymentTransactionsRequest from '../request/DisapprovePaymentTransactionsRequest';
import ExpireCheckoutPaymentRequest from '../request/ExpireCheckoutPaymentRequest';
import InitApmDepositPaymentRequest from '../request/InitApmDepositPaymentRequest';
import InitApmPaymentRequest from '../request/InitApmPaymentRequest';
import InitBnplLimitInquiryRequest from '../request/InitBnplLimitInquiryRequest';
Expand All @@ -37,6 +39,7 @@ import SearchStoredCardsRequest from '../request/SearchStoredCardsRequest';
import StoreCardRequest from '../request/StoreCardRequest';
import UpdateCardRequest from '../request/UpdateCardRequest';
import UpdatePaymentTransactionRequest from '../request/UpdatePaymentTransactionRequest';
import VerifyBnplPaymentRequest from '../request/VerifyBnplPaymentRequest';
import VerifyCardRequest from '../request/VerifyCardRequest';

import ApmDepositPaymentResponse from '../response/ApmDepositPaymentResponse';
Expand Down Expand Up @@ -113,8 +116,8 @@ export default class PaymentAdapter extends BaseAdapter {
return this._client.get(`/payment/v1/checkout-payments/${token}`);
}

async expireCheckoutPayment(token: string): Promise<void> {
await this._client.delete(`/payment/v1/checkout-payments/${token}`);
async expireCheckoutPayment(request: ExpireCheckoutPaymentRequest): Promise<void> {
await this._client.delete(`/payment/v1/checkout-payments/${request.token}`, undefined, requestScopedConfig(request));
}

async createDepositPayment(request: CreateDepositPaymentRequest): Promise<DepositPaymentResponse> {
Expand Down Expand Up @@ -241,12 +244,12 @@ export default class PaymentAdapter extends BaseAdapter {
return this._client.post(`/payment/v1/bnpl-payments/init`, request);
}

async approveBnplPayment(paymentId: number): Promise<PaymentResponse> {
return this._client.post(`/payment/v1/bnpl-payments/${paymentId}/approve`);
async approveBnplPayment(request: ApproveBnplPaymentRequest): Promise<PaymentResponse> {
return this._client.post(`/payment/v1/bnpl-payments/${request.paymentId}/approve`, undefined, requestScopedConfig(request));
}

async verifyBnplPayment(paymentId: number): Promise<BnplPaymentVerifyResponse> {
return this._client.post(`/payment/v1/bnpl-payments/${paymentId}/verify`);
async verifyBnplPayment(request: VerifyBnplPaymentRequest): Promise<BnplPaymentVerifyResponse> {
return this._client.post(`/payment/v1/bnpl-payments/${request.paymentId}/verify`, undefined, requestScopedConfig(request));
}

async bnplLimitInquiryInit(request: InitBnplLimitInquiryRequest): Promise<BnplLimitInquiryResponse> {
Expand Down
7 changes: 4 additions & 3 deletions src/adapter/SettlementAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';

import CreateInstantWalletSettlementRequest from '../request/CreateInstantWalletSettlementRequest';
import CreatePayoutAccountRequest from '../request/CreatePayoutAccountRequest';
import DeletePayoutAccountRequest from '../request/DeletePayoutAccountRequest';
import SearchPayoutAccountRequest from '../request/SearchPayoutAccountRequest';
import UpdatePayoutAccountRequest from '../request/UpdatePayoutAccountRequest';

Expand All @@ -28,8 +29,8 @@ export default class SettlementAdapter extends BaseAdapter {
await this._client.put(`/settlement/v1/payout-accounts/${id}`, request);
}

async deletePayoutAccount(id: number): Promise<void> {
await this._client.delete(`/settlement/v1/payout-accounts/${id}`);
async deletePayoutAccount(request: DeletePayoutAccountRequest): Promise<void> {
await this._client.delete(`/settlement/v1/payout-accounts/${request.id}`, undefined, requestScopedConfig(request));
}

async searchPayoutAccount(request: SearchPayoutAccountRequest): Promise<DataResponse<PayoutAccountResponse>> {
Expand Down
7 changes: 4 additions & 3 deletions src/adapter/WalletAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {ClientCreationOptions} from '../lib/HttpClient';
import {ClientCreationOptions, requestScopedConfig} from '../lib/HttpClient';

import CancelWithdrawRequest from '../request/CancelWithdrawRequest';
import CreateMemberWalletRequest from '../request/CreateMemberWalletRequest';
import CreateRemittanceRequest from '../request/CreateRemittanceRequest';
import CreateWithdrawRequest from '../request/CreateWithdrawRequest';
Expand Down Expand Up @@ -77,8 +78,8 @@ export default class WalletAdapter extends BaseAdapter {
return this._client.post('/wallet/v1/withdraws', request);
}

async cancelWithdraw(withdrawId: number): Promise<WithdrawResponse> {
return this._client.post(`/wallet/v1/withdraws/${withdrawId}/cancel`);
async cancelWithdraw(request: CancelWithdrawRequest): Promise<WithdrawResponse> {
return this._client.post(`/wallet/v1/withdraws/${request.withdrawId}/cancel`, undefined, requestScopedConfig(request));
}

async retrieveWithdraw(withdrawId: number): Promise<WithdrawResponse> {
Expand Down
31 changes: 29 additions & 2 deletions src/lib/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios, {AxiosInstance, AxiosRequestConfig, AxiosResponse} from 'axios';

import CraftgateError from '../CraftgateError';

import {calculateSignature, generateRandomString, serializeParams} from './utils';
import {calculateSignature, generateRandomString, omitRequestScopedOptions, REQUEST_SCOPED_HEADERS, serializeParams} from './utils';

export type ClientOptions = {
apiKey: string;
Expand Down Expand Up @@ -30,6 +30,25 @@ const CLIENT_VERSION_HEADER_NAME = 'x-client-version';
const SIGNATURE_HEADER_NAME = 'x-signature';
const LANGUAGE_HEADER_NAME = 'lang';

function requestScopedHeadersOf(source: any): Record<string, string> {
const headers: Record<string, string> = {};
if (!source || typeof source !== 'object') {
return headers;
}

Object.keys(REQUEST_SCOPED_HEADERS).forEach((key: string) => {
if (source[key]) {
headers[REQUEST_SCOPED_HEADERS[key]] = source[key];
}
});
return headers;
}

export function requestScopedConfig(request?: any): AxiosRequestConfig {
const headers = requestScopedHeadersOf(request);
return Object.keys(headers).length > 0 ? {headers} : {};
}

export class HttpClient {
private readonly _client: AxiosInstance;
private readonly _options: ClientOptions;
Expand Down Expand Up @@ -82,6 +101,14 @@ export class HttpClient {
private _injectHeaders(config: AxiosRequestConfig): AxiosRequestConfig {
const randomStr: string = generateRandomString();

const scopedHeaders: Record<string, string> = {
...requestScopedHeadersOf(config.params),
...requestScopedHeadersOf(config.data)
};
Object.keys(scopedHeaders).forEach((name: string) => {
config.headers[name] = scopedHeaders[name];
});

config.headers[API_KEY_HEADER_NAME] = this._options.apiKey;
config.headers[RANDOM_HEADER_NAME] = randomStr;
config.headers[AUTH_VERSION_HEADER_NAME] = '1';
Expand All @@ -91,7 +118,7 @@ export class HttpClient {
}
config.maxRedirects = 0;

const requestBody: string | null = config.data ? JSON.stringify(config.data, null, 0) : null;
const requestBody: string | null = config.data ? JSON.stringify(omitRequestScopedOptions(config.data), null, 0) : null;

if (!config.paramsSerializer) {
config.paramsSerializer = {
Expand Down
Loading
Loading