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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ resp = payment.create_payment(req)
print(f"Create Payment Result: {resp}")
~~~

## 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:

~~~python
import uuid

req = CreatePaymentRequest()
req.price = Decimal("100")
req.paid_price = Decimal("100")
req.currency = Currency.TRY
req.payment_group = PaymentGroup.LISTING_OR_SUBSCRIPTION
req.idempotency_key = str(uuid.uuid4())
# ... other fields

resp = payment.create_payment(req)
~~~

`with_idempotency_key()` sets it inline and returns the request, which is handy for operations whose parameters live in
the URL path:

~~~python
payment.expire_checkout_payment(
ExpireCheckoutPaymentRequest(token="456d1297-908e-4bd6-a13b-4be31a6e47d5")
.with_idempotency_key(str(uuid.uuid4())))
~~~

> 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.

## Examples

A variety of end-to-end samples (3DS, Checkout, APM, refunds, stored cards, marketplace, pre/post-auth) live under the
Expand Down
38 changes: 19 additions & 19 deletions craftgate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,59 +23,59 @@ def __init__(self, options):

self.options = options

def payment(self):
def payment(self) -> PaymentAdapter:
return PaymentAdapter(self.options)

def bank_account_tracking(self):
def bank_account_tracking(self) -> BankAccountTrackingAdapter:
return BankAccountTrackingAdapter(self.options)

def bkm_express_payment(self):
def bkm_express_payment(self) -> BkmExpressPaymentAdapter:
return BkmExpressPaymentAdapter(self.options)

def file_reporting(self):
def file_reporting(self) -> FileReportingAdapter:
return FileReportingAdapter(self.options)

def fraud(self):
def fraud(self) -> FraudAdapter:
return FraudAdapter(self.options)

def hook(self):
def hook(self) -> HookAdapter:
return HookAdapter(self.options)

def installment(self):
def installment(self) -> InstallmentAdapter:
return InstallmentAdapter(self.options)

def juzdan_payment(self):
def juzdan_payment(self) -> JuzdanPaymentAdapter:
return JuzdanPaymentAdapter(self.options)

def masterpass_payment(self):
def masterpass_payment(self) -> MasterpassPaymentAdapter:
return MasterpassPaymentAdapter(self.options)

def merchant(self):
def merchant(self) -> MerchantAdapter:
return MerchantAdapter(self.options)

def merchant_apm(self):
def merchant_apm(self) -> MerchantApmAdapter:
return MerchantApmAdapter(self.options)

def onboarding(self):
def onboarding(self) -> OnboardingAdapter:
return OnboardingAdapter(self.options)

def pay_by_link(self):
def pay_by_link(self) -> PayByLinkAdapter:
return PayByLinkAdapter(self.options)

def payment_reporting(self):
def payment_reporting(self) -> PaymentReportingAdapter:
return PaymentReportingAdapter(self.options)

def payment_token(self):
def payment_token(self) -> PaymentTokenAdapter:
return PaymentTokenAdapter(self.options)

def settlement(self):
def settlement(self) -> SettlementAdapter:
return SettlementAdapter(self.options)

def settlement_reporting(self):
def settlement_reporting(self) -> SettlementReportingAdapter:
return SettlementReportingAdapter(self.options)

def wallet(self):
def wallet(self) -> WalletAdapter:
return WalletAdapter(self.options)

def meal_voucher_card_tokenization(self):
def meal_voucher_card_tokenization(self) -> MealVoucherCardTokenizationAdapter:
return MealVoucherCardTokenizationAdapter(self.options)
20 changes: 18 additions & 2 deletions craftgate/adapter/base_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
from typing import Any, Dict, Optional

from _version import VERSION
from craftgate.request.common.base_request import BaseRequest
from craftgate.request.common.header_options import HeaderOptions
from craftgate.request_options import RequestOptions
from craftgate.utils.hash_generator import HashGenerator


class BaseAdapter:
API_VERSION_HEADER_VALUE = "v1"
CLIENT_NAME = "craftgate-python-client"
Expand All @@ -16,6 +17,7 @@ class BaseAdapter:
CLIENT_VERSION_HEADER_NAME = "x-client-version"
SIGNATURE_HEADER_NAME = "x-signature"
LANGUAGE_HEADER_NAME = "lang"
IDEMPOTENCY_KEY_HEADER_NAME = "x-idempotency-key"

def __init__(self, request_options: RequestOptions) -> None:
self.request_options = request_options
Expand All @@ -24,7 +26,8 @@ def _create_headers(
self,
request_body: Optional[Any],
path: str,
custom_options: Optional[RequestOptions] = None
custom_options: Optional[RequestOptions] = None,
header_options: Optional[HeaderOptions] = None
) -> Dict[str, str]:
options = custom_options or self.request_options
random_key = self._generate_random_string()
Expand All @@ -49,7 +52,20 @@ def _create_headers(
if options.language:
headers[self.LANGUAGE_HEADER_NAME] = options.language

if header_options is None and isinstance(request_body, BaseRequest):
header_options = request_body.to_header_options()
self._apply_request_scoped_headers(headers, header_options)

return headers

def _apply_request_scoped_headers(
self, headers: Dict[str, str], header_options: Optional[HeaderOptions]
) -> None:
if header_options is None:
return

if header_options.idempotency_key is not None:
headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = header_options.idempotency_key

def _generate_random_string(self) -> str:
return str(uuid.uuid4())
25 changes: 13 additions & 12 deletions craftgate/adapter/fraud_adapter.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
from craftgate.adapter.base_adapter import BaseAdapter
from craftgate.model.fraud_check_status import FraudCheckStatus
from craftgate.model.fraud_value_type import FraudValueType
from craftgate.net.base_http_client import BaseHttpClient
from craftgate.request.delete_value_list_request import DeleteValueListRequest
from craftgate.request.fraud_add_card_fingerprint_to_list_request import FraudAddCardFingerprintToListRequest
from craftgate.request.fraud_value_list_request import FraudValueListRequest
from craftgate.request.remove_value_from_value_list_request import RemoveValueFromValueListRequest
from craftgate.request.search_fraud_checks_request import SearchFraudChecksRequest
from craftgate.request.search_fraud_rule_request import SearchFraudRuleRequest
from craftgate.request.update_fraud_check_request import UpdateFraudCheckRequest
from craftgate.request.update_fraud_check_status_request import UpdateFraudCheckStatusRequest
from craftgate.request_options import RequestOptions
from craftgate.response.fraud_all_value_lists_response import FraudAllValueListsResponse
from craftgate.response.fraud_check_list_response import FraudCheckListResponse
from craftgate.response.fraud_rule_response import FraudRuleResponse
from craftgate.response.fraud_value_list_response import FraudValueListResponse
from craftgate.utils.request_query_params_builder import RequestQueryParamsBuilder


class FraudAdapter(BaseAdapter):
def __init__(self, request_options: RequestOptions) -> None:
super(FraudAdapter, self).__init__(request_options)
Expand Down Expand Up @@ -44,10 +45,10 @@ def search_fraud_rules(self, request: SearchFraudRuleRequest) -> FraudRuleRespon
response_type=FraudCheckListResponse
)

def update_fraud_check_status(self, id: int, fraud_check_status: FraudCheckStatus) -> None:
path = "/fraud/v1/fraud-checks/{}/check-status".format(id)
body = UpdateFraudCheckRequest(check_status=fraud_check_status)
headers = self._create_headers(body, path)
def update_fraud_check_status(self, request: UpdateFraudCheckStatusRequest) -> None:
path = "/fraud/v1/fraud-checks/{}/check-status".format(request.id)
body = UpdateFraudCheckRequest(check_status=request.check_status)
headers = self._create_headers(body, path, header_options=request.to_header_options())
self._http_client.request(
method="PUT",
url=self.request_options.base_url + path,
Expand Down Expand Up @@ -87,9 +88,9 @@ def create_value_list(self, list_name: str, value_type: FraudValueType) -> None:
)
self.add_value_to_value_list(body)

def delete_value_list(self, list_name: str) -> None:
path = "/fraud/v1/value-lists/{}".format(list_name)
headers = self._create_headers(None, path)
def delete_value_list(self, request: DeleteValueListRequest) -> None:
path = "/fraud/v1/value-lists/{}".format(request.list_name)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down Expand Up @@ -128,9 +129,9 @@ def add_card_fingerprint_to_value_list(
"""
self.add_card_fingerprint(request=request, list_name=list_name)

def remove_value_from_value_list(self, list_name: str, value_id: str) -> None:
path = "/fraud/v1/value-lists/{}/values/{}".format(list_name, value_id)
headers = self._create_headers(None, path)
def remove_value_from_value_list(self, request: RemoveValueFromValueListRequest) -> None:
path = "/fraud/v1/value-lists/{}/values/{}".format(request.list_name, request.value_id)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down
15 changes: 8 additions & 7 deletions craftgate/adapter/merchant_adapter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from craftgate.adapter.base_adapter import BaseAdapter
from craftgate.model.pos_status import PosStatus
from craftgate.net.base_http_client import BaseHttpClient
from craftgate.request.create_merchant_pos_request import CreateMerchantPosRequest
from craftgate.request.delete_merchant_pos_request import DeleteMerchantPosRequest
from craftgate.request.search_merchant_pos_request import SearchMerchantPosRequest
from craftgate.request.update_merchant_pos_commissions_request import UpdateMerchantPosCommissionsRequest
from craftgate.request.update_merchant_pos_request import UpdateMerchantPosRequest
from craftgate.request.update_merchant_pos_status_request import UpdateMerchantPosStatusRequest
from craftgate.request_options import RequestOptions
from craftgate.response.merchant_pos_commission_list_response import MerchantPosCommissionListResponse
from craftgate.response.merchant_pos_list_response import MerchantPosListResponse
Expand Down Expand Up @@ -39,9 +40,9 @@ def update_merchant_pos(self, merchant_pos_id: int, request: UpdateMerchantPosRe
response_type=MerchantPosResponse
)

def update_merchant_pos_status(self, merchant_pos_id: int, pos_status: PosStatus) -> None:
path = "/merchant/v1/merchant-poses/{}/status/{}".format(merchant_pos_id, pos_status.name)
headers = self._create_headers(None, path)
def update_merchant_pos_status(self, request: UpdateMerchantPosStatusRequest) -> None:
path = "/merchant/v1/merchant-poses/{}/status/{}".format(request.merchant_pos_id, request.pos_status.name)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="PUT",
url=self.request_options.base_url + path,
Expand Down Expand Up @@ -73,9 +74,9 @@ def retrieve(self, merchant_pos_id: int) -> MerchantPosResponse:
response_type=MerchantPosResponse
)

def delete_merchant_pos(self, merchant_pos_id: int) -> None:
path = "/merchant/v1/merchant-poses/{}".format(merchant_pos_id)
headers = self._create_headers(None, path)
def delete_merchant_pos(self, request: DeleteMerchantPosRequest) -> None:
path = "/merchant/v1/merchant-poses/{}".format(request.merchant_pos_id)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down
7 changes: 4 additions & 3 deletions craftgate/adapter/pay_by_link_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from craftgate.adapter.base_adapter import BaseAdapter
from craftgate.net.base_http_client import BaseHttpClient
from craftgate.request.create_product_request import CreateProductRequest
from craftgate.request.delete_product_request import DeleteProductRequest
from craftgate.request.search_products_request import SearchProductsRequest
from craftgate.request.update_product_request import UpdateProductRequest
from craftgate.request_options import RequestOptions
Expand Down Expand Up @@ -47,9 +48,9 @@ def retrieve_product(self, product_id: int) -> ProductResponse:
response_type=ProductResponse
)

def delete_product(self, product_id: int) -> None:
path = "/craftlink/v1/products/{}".format(product_id)
headers = self._create_headers(None, path)
def delete_product(self, request: DeleteProductRequest) -> None:
path = "/craftlink/v1/products/{}".format(request.id)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down
21 changes: 12 additions & 9 deletions craftgate/adapter/payment_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from craftgate.adapter.base_adapter import BaseAdapter
from craftgate.net.base_http_client import BaseHttpClient
from craftgate.request.apple_pay_merchant_session_create_request import ApplePayMerchantSessionCreateRequest
from craftgate.request.approve_bnpl_payment_request import ApproveBnplPaymentRequest
from craftgate.request.approve_payment_transactions_request import ApprovePaymentTransactionsRequest
from craftgate.request.bnpl_payment_offer_request import BnplPaymentOfferRequest
from craftgate.request.clone_card_request import CloneCardRequest
Expand All @@ -13,6 +14,7 @@
from craftgate.request.create_payment_request import CreatePaymentRequest
from craftgate.request.delete_stored_card_request import DeleteStoredCardRequest
from craftgate.request.disapprove_payment_transactions_request import DisapprovePaymentTransactionsRequest
from craftgate.request.expire_checkout_payment_request import ExpireCheckoutPaymentRequest
from craftgate.request.init_apm_deposit_payment_request import InitApmDepositPaymentRequest
from craftgate.request.init_apm_payment_request import InitApmPaymentRequest
from craftgate.request.bnpl_limit_inquiry_request import BnplLimitInquiryRequest
Expand All @@ -35,6 +37,7 @@
from craftgate.request.store_card_request import StoreCardRequest
from craftgate.request.update_card_request import UpdateCardRequest
from craftgate.request.update_payment_transaction_request import UpdatePaymentTransactionRequest
from craftgate.request.verify_bnpl_payment_request import VerifyBnplPaymentRequest
from craftgate.request.verify_card_request import VerifyCardRequest
from craftgate.request_options import RequestOptions
from craftgate.response.apm_deposit_payment_response import ApmDepositPaymentResponse
Expand Down Expand Up @@ -174,9 +177,9 @@ def retrieve_checkout_payment(self, token: str) -> PaymentResponse:
response_type=PaymentResponse
)

def expire_checkout_payment(self, token: str) -> None:
path = "/payment/v1/checkout-payments/{}".format(token)
headers = self._create_headers(None, path)
def expire_checkout_payment(self, request: ExpireCheckoutPaymentRequest) -> None:
path = "/payment/v1/checkout-payments/{}".format(request.token)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down Expand Up @@ -525,9 +528,9 @@ def init_bnpl_payment(self, request: InitBnplPaymentRequest) -> InitBnplPaymentR
response_type=InitBnplPaymentResponse
)

def approve_bnpl_payment(self, payment_id: int) -> PaymentResponse:
path = "/payment/v1/bnpl-payments/{}/approve".format(payment_id)
headers = self._create_headers(None, path)
def approve_bnpl_payment(self, request: ApproveBnplPaymentRequest) -> PaymentResponse:
path = "/payment/v1/bnpl-payments/{}/approve".format(request.payment_id)
headers = self._create_headers(None, path, header_options=request.to_header_options())
return self._http_client.request(
method="POST",
url=self.request_options.base_url + path,
Expand All @@ -536,9 +539,9 @@ def approve_bnpl_payment(self, payment_id: int) -> PaymentResponse:
response_type=PaymentResponse
)

def verify_bnpl_payment(self, payment_id: int) -> BnplPaymentVerifyResponse:
path = "/payment/v1/bnpl-payments/{}/verify".format(payment_id)
headers = self._create_headers(None, path)
def verify_bnpl_payment(self, request: VerifyBnplPaymentRequest) -> BnplPaymentVerifyResponse:
path = "/payment/v1/bnpl-payments/{}/verify".format(request.payment_id)
headers = self._create_headers(None, path, header_options=request.to_header_options())
return self._http_client.request(
method="POST",
url=self.request_options.base_url + path,
Expand Down
7 changes: 4 additions & 3 deletions craftgate/adapter/payment_token_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from craftgate.adapter.base_adapter import BaseAdapter
from craftgate.net.base_http_client import BaseHttpClient
from craftgate.request.create_payment_token_request import CreatePaymentTokenRequest
from craftgate.request.delete_payment_token_request import DeletePaymentTokenRequest
from craftgate.request_options import RequestOptions
from craftgate.response.payment_token_response import PaymentTokenResponse

Expand All @@ -21,9 +22,9 @@ def create_payment_token(self, request: CreatePaymentTokenRequest) -> PaymentTok
response_type=PaymentTokenResponse
)

def delete_payment_token(self, token: str) -> None:
path = "/payment/v1/payment-tokens/{}".format(token)
headers = self._create_headers(None, path)
def delete_payment_token(self, request: DeletePaymentTokenRequest) -> None:
path = "/payment/v1/payment-tokens/{}".format(request.token)
headers = self._create_headers(None, path, header_options=request.to_header_options())
self._http_client.request(
method="DELETE",
url=self.request_options.base_url + path,
Expand Down
Loading
Loading