From fbedef73551bf6d0d54f32c58496cb26c3710088 Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Thu, 30 Jul 2026 11:33:06 +0300 Subject: [PATCH 1/9] Idempotency support --- README.md | 35 ++++ craftgate/adapter/base_adapter.py | 15 +- craftgate/adapter/fraud_adapter.py | 25 +-- craftgate/adapter/merchant_adapter.py | 15 +- craftgate/adapter/pay_by_link_adapter.py | 7 +- craftgate/adapter/payment_adapter.py | 21 ++- craftgate/adapter/payment_token_adapter.py | 7 +- craftgate/adapter/settlement_adapter.py | 7 +- craftgate/adapter/wallet_adapter.py | 7 +- craftgate/request/__init__.py | 13 ++ ...ple_pay_merchant_session_create_request.py | 5 +- .../request/approve_bnpl_payment_request.py | 11 ++ .../approve_payment_transactions_request.py | 4 +- .../request/bnpl_limit_inquiry_request.py | 3 +- .../request/bnpl_payment_offer_request.py | 3 +- craftgate/request/cancel_withdraw_request.py | 11 ++ .../request/check_masterpass_user_request.py | 4 +- craftgate/request/clone_card_request.py | 4 +- craftgate/request/common/__init__.py | 1 + craftgate/request/common/base_request.py | 31 ++++ .../request/complete_apm_payment_request.py | 4 +- .../request/complete_bkm_express_request.py | 4 +- .../complete_pos_apm_payment_request.py | 4 +- .../complete_three_ds_payment_request.py | 4 +- .../request/create_apm_payment_request.py | 3 +- .../request/create_deposit_payment_request.py | 3 +- ...e_fund_transfer_deposit_payment_request.py | 4 +- ...reate_instant_wallet_settlement_request.py | 4 +- craftgate/request/create_member_request.py | 3 +- .../request/create_merchant_pos_request.py | 3 +- craftgate/request/create_merchant_request.py | 4 +- craftgate/request/create_payment_request.py | 3 +- .../request/create_payment_token_request.py | 4 +- .../request/create_payout_account_request.py | 3 +- craftgate/request/create_product_request.py | 3 +- .../request/create_remittance_request.py | 3 +- craftgate/request/create_report_request.py | 3 +- craftgate/request/create_wallet_request.py | 3 +- craftgate/request/create_withdraw_request.py | 3 +- .../request/delete_merchant_pos_request.py | 11 ++ .../request/delete_payment_token_request.py | 11 ++ .../request/delete_payout_account_request.py | 11 ++ craftgate/request/delete_product_request.py | 11 ++ .../request/delete_stored_card_request.py | 4 +- .../request/delete_value_list_request.py | 11 ++ ...disapprove_payment_transactions_request.py | 4 +- .../expire_checkout_payment_request.py | 11 ++ ...ud_add_card_fingerprint_to_list_request.py | 3 +- craftgate/request/fraud_value_list_request.py | 3 +- .../init_apm_deposit_payment_request.py | 3 +- craftgate/request/init_apm_payment_request.py | 3 +- craftgate/request/init_bkm_express_request.py | 3 +- .../init_checkout_card_verify_request.py | 3 +- .../request/init_checkout_payment_request.py | 3 +- .../init_garanti_pay_payment_request.py | 3 +- .../request/init_juzdan_payment_request.py | 3 +- ..._meal_voucher_card_tokenization_request.py | 3 +- .../request/init_multi_payment_request.py | 3 +- .../request/init_pos_apm_payment_request.py | 3 +- .../masterpass_payment_complete_request.py | 4 +- ...erpass_payment_threeds_complete_request.py | 4 +- ...masterpass_payment_threeds_init_request.py | 4 +- ...sterpass_payment_token_generate_request.py | 3 +- .../masterpass_retrieve_loyalties_request.py | 4 +- ...cher_card_tokenization_complete_request.py | 4 +- ...er_card_tokenization_regenerate_request.py | 3 +- .../request/post_auth_payment_request.py | 4 +- craftgate/request/refund_payment_request.py | 3 +- ...nt_transaction_mark_as_refunded_request.py | 3 +- .../refund_payment_transaction_request.py | 3 +- ...fund_wallet_transaction_to_card_request.py | 4 +- .../remove_value_from_value_list_request.py | 13 ++ ..._merchant_member_wallet_balance_request.py | 4 +- .../retrieve_daily_payment_report_request.py | 3 +- ...trieve_daily_transaction_report_request.py | 3 +- .../request/retrieve_loyalties_request.py | 3 +- .../request/retrieve_provider_card_request.py | 3 +- craftgate/request/retrieve_report_request.py | 3 +- ...h_bank_account_tracking_records_request.py | 3 +- .../request/search_fraud_checks_request.py | 3 +- .../request/search_fraud_rule_request.py | 3 +- .../request/search_installments_request.py | 3 +- craftgate/request/search_members_request.py | 3 +- .../request/search_merchant_pos_request.py | 3 +- .../request/search_payment_refunds_request.py | 3 +- ...rch_payment_transaction_refunds_request.py | 3 +- craftgate/request/search_payments_request.py | 3 +- .../request/search_payout_account_request.py | 3 +- ...rch_payout_bounced_transactions_request.py | 4 +- ...h_payout_completed_transactions_request.py | 3 +- .../request/search_payout_rows_request.py | 3 +- craftgate/request/search_products_request.py | 3 +- .../request/search_stored_cards_request.py | 3 +- .../search_wallet_transactions_request.py | 3 +- craftgate/request/search_withdraws_request.py | 3 +- craftgate/request/store_card_request.py | 3 +- craftgate/request/update_card_request.py | 4 +- .../request/update_fraud_check_request.py | 3 +- .../update_fraud_check_status_request.py | 14 ++ craftgate/request/update_member_request.py | 3 +- ...update_merchant_pos_commissions_request.py | 3 +- .../request/update_merchant_pos_request.py | 3 +- .../update_merchant_pos_status_request.py | 14 ++ .../update_payment_transaction_request.py | 4 +- .../request/update_payout_account_request.py | 3 +- craftgate/request/update_product_request.py | 3 +- craftgate/request/update_wallet_request.py | 4 +- .../request/verify_bnpl_payment_request.py | 11 ++ craftgate/request/verify_card_request.py | 3 +- tests/test_bnpl_payment_sample.py | 7 +- tests/test_fraud_adapter.py | 11 +- tests/test_idempotency.py | 163 ++++++++++++++++++ tests/test_merchant_sample.py | 9 +- tests/test_pay_by_link_sample.py | 4 +- tests/test_payment_sample.py | 6 +- tests/test_payment_token_sample.py | 4 +- tests/test_settlement_sample.py | 4 +- tests/test_wallet_sample.py | 8 +- 118 files changed, 673 insertions(+), 149 deletions(-) create mode 100644 craftgate/request/approve_bnpl_payment_request.py create mode 100644 craftgate/request/cancel_withdraw_request.py create mode 100644 craftgate/request/common/__init__.py create mode 100644 craftgate/request/common/base_request.py create mode 100644 craftgate/request/delete_merchant_pos_request.py create mode 100644 craftgate/request/delete_payment_token_request.py create mode 100644 craftgate/request/delete_payout_account_request.py create mode 100644 craftgate/request/delete_product_request.py create mode 100644 craftgate/request/delete_value_list_request.py create mode 100644 craftgate/request/expire_checkout_payment_request.py create mode 100644 craftgate/request/remove_value_from_value_list_request.py create mode 100644 craftgate/request/update_fraud_check_status_request.py create mode 100644 craftgate/request/update_merchant_pos_status_request.py create mode 100644 craftgate/request/verify_bnpl_payment_request.py create mode 100644 tests/test_idempotency.py diff --git a/README.md b/README.md index 50af1d5..b23b24a 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,41 @@ resp = payment.create_payment(req) print(f"Create Payment Result: {resp}") ~~~ +## 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: + +~~~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 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 diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index 2fd481a..8502af8 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -16,6 +16,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 @@ -24,11 +25,20 @@ def _create_headers( self, request_body: Optional[Any], path: str, - custom_options: Optional[RequestOptions] = None + custom_options: Optional[RequestOptions] = None, + idempotency_key: Optional[str] = None ) -> Dict[str, str]: + """Builds the request headers. + + ``request_body`` is hashed into the signature, so body-less calls must pass ``None`` + and supply the wrapper's key via ``idempotency_key`` instead. + """ options = custom_options or self.request_options random_key = self._generate_random_string() + if idempotency_key is None: + idempotency_key = getattr(request_body, "_idempotency_key", None) + signature = HashGenerator.generate_hash( base_url=options.base_url, api_key=options.api_key, @@ -49,6 +59,9 @@ def _create_headers( if options.language: headers[self.LANGUAGE_HEADER_NAME] = options.language + if idempotency_key is not None: + headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = idempotency_key + return headers def _generate_random_string(self) -> str: diff --git a/craftgate/adapter/fraud_adapter.py b/craftgate/adapter/fraud_adapter.py index e14465a..92b6d4d 100644 --- a/craftgate/adapter/fraud_adapter.py +++ b/craftgate/adapter/fraud_adapter.py @@ -1,12 +1,14 @@ 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 @@ -44,10 +46,11 @@ 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) + # The id belongs in the path, so only the status is sent as the body. + body = UpdateFraudCheckRequest(check_status=request.check_status) + headers = self._create_headers(body, path, idempotency_key=request.idempotency_key) self._http_client.request( method="PUT", url=self.request_options.base_url + path, @@ -87,9 +90,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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, @@ -128,9 +131,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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/merchant_adapter.py b/craftgate/adapter/merchant_adapter.py index abc62b5..af6ac11 100644 --- a/craftgate/adapter/merchant_adapter.py +++ b/craftgate/adapter/merchant_adapter.py @@ -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 @@ -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, idempotency_key=request.idempotency_key) self._http_client.request( method="PUT", url=self.request_options.base_url + path, @@ -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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/pay_by_link_adapter.py b/craftgate/adapter/pay_by_link_adapter.py index ddadfdf..c9d8b88 100644 --- a/craftgate/adapter/pay_by_link_adapter.py +++ b/craftgate/adapter/pay_by_link_adapter.py @@ -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 @@ -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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/payment_adapter.py b/craftgate/adapter/payment_adapter.py index 4a478e6..84ef091 100644 --- a/craftgate/adapter/payment_adapter.py +++ b/craftgate/adapter/payment_adapter.py @@ -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 @@ -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 @@ -34,6 +36,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 @@ -172,9 +175,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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, @@ -523,9 +526,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, idempotency_key=request.idempotency_key) return self._http_client.request( method="POST", url=self.request_options.base_url + path, @@ -534,9 +537,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, idempotency_key=request.idempotency_key) return self._http_client.request( method="POST", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/payment_token_adapter.py b/craftgate/adapter/payment_token_adapter.py index 99ee791..598b639 100644 --- a/craftgate/adapter/payment_token_adapter.py +++ b/craftgate/adapter/payment_token_adapter.py @@ -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 @@ -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, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/settlement_adapter.py b/craftgate/adapter/settlement_adapter.py index c766636..78a2cf0 100644 --- a/craftgate/adapter/settlement_adapter.py +++ b/craftgate/adapter/settlement_adapter.py @@ -2,6 +2,7 @@ from craftgate.net.base_http_client import BaseHttpClient from craftgate.request.create_instant_wallet_settlement_request import CreateInstantWalletSettlementRequest from craftgate.request.create_payout_account_request import CreatePayoutAccountRequest +from craftgate.request.delete_payout_account_request import DeletePayoutAccountRequest from craftgate.request.search_payout_account_request import SearchPayoutAccountRequest from craftgate.request.update_payout_account_request import UpdatePayoutAccountRequest from craftgate.request_options import RequestOptions @@ -49,9 +50,9 @@ def update_payout_account(self, id: int, request: UpdatePayoutAccountRequest) -> response_type=None ) - def delete_payout_account(self, id: int) -> None: - path = "/settlement/v1/payout-accounts/{}".format(id) - headers = self._create_headers(None, path) + def delete_payout_account(self, request: DeletePayoutAccountRequest) -> None: + path = "/settlement/v1/payout-accounts/{}".format(request.id) + headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/wallet_adapter.py b/craftgate/adapter/wallet_adapter.py index 94e7392..6245a4d 100644 --- a/craftgate/adapter/wallet_adapter.py +++ b/craftgate/adapter/wallet_adapter.py @@ -1,5 +1,6 @@ from craftgate.adapter.base_adapter import BaseAdapter from craftgate.net.base_http_client import BaseHttpClient +from craftgate.request.cancel_withdraw_request import CancelWithdrawRequest from craftgate.request.create_remittance_request import CreateRemittanceRequest from craftgate.request.create_wallet_request import CreateWalletRequest from craftgate.request.create_withdraw_request import CreateWithdrawRequest @@ -179,9 +180,9 @@ def create_withdraw(self, request: CreateWithdrawRequest) -> WithdrawResponse: response_type=WithdrawResponse ) - def cancel_withdraw(self, withdraw_id: int) -> WithdrawResponse: - path = "/wallet/v1/withdraws/{}/cancel".format(withdraw_id) - headers = self._create_headers(None, path) + def cancel_withdraw(self, request: CancelWithdrawRequest) -> WithdrawResponse: + path = "/wallet/v1/withdraws/{}/cancel".format(request.withdraw_id) + headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) return self._http_client.request( method="POST", url=self.request_options.base_url + path, diff --git a/craftgate/request/__init__.py b/craftgate/request/__init__.py index b3ab8b3..8db42e9 100644 --- a/craftgate/request/__init__.py +++ b/craftgate/request/__init__.py @@ -1,9 +1,12 @@ from .apple_pay_merchant_session_create_request import ApplePayMerchantSessionCreateRequest +from .approve_bnpl_payment_request import ApproveBnplPaymentRequest from .approve_payment_transactions_request import ApprovePaymentTransactionsRequest from .bnpl_payment_offer_request import BnplPaymentOfferRequest from .bnpl_limit_inquiry_request import BnplLimitInquiryRequest +from .cancel_withdraw_request import CancelWithdrawRequest from .check_masterpass_user_request import CheckMasterpassUserRequest from .clone_card_request import CloneCardRequest +from .common import BaseRequest from .complete_apm_payment_request import CompleteApmPaymentRequest from .complete_bkm_express_request import CompleteBkmExpressRequest from .complete_pos_apm_payment_request import CompletePosApmPaymentRequest @@ -23,9 +26,15 @@ from .create_report_request import CreateReportRequest from .create_wallet_request import CreateWalletRequest from .create_withdraw_request import CreateWithdrawRequest +from .delete_merchant_pos_request import DeleteMerchantPosRequest +from .delete_payment_token_request import DeletePaymentTokenRequest +from .delete_payout_account_request import DeletePayoutAccountRequest +from .delete_product_request import DeleteProductRequest from .delete_stored_card_request import DeleteStoredCardRequest +from .delete_value_list_request import DeleteValueListRequest from .disapprove_payment_transactions_request import DisapprovePaymentTransactionsRequest from .dto import * +from .expire_checkout_payment_request import ExpireCheckoutPaymentRequest from .fraud_value_list_request import FraudValueListRequest from .init_apm_deposit_payment_request import InitApmDepositPaymentRequest from .init_apm_payment_request import InitApmPaymentRequest @@ -50,6 +59,7 @@ from .refund_payment_transaction_mark_as_refunded_request import RefundPaymentTransactionMarkAsRefundedRequest from .refund_payment_transaction_request import RefundPaymentTransactionRequest from .refund_wallet_transaction_to_card_request import RefundWalletTransactionToCardRequest +from .remove_value_from_value_list_request import RemoveValueFromValueListRequest from .reset_merchant_member_wallet_balance_request import ResetMerchantMemberWalletBalanceRequest from .retrieve_daily_payment_report_request import RetrieveDailyPaymentReportRequest from .retrieve_daily_transaction_report_request import RetrieveDailyTransactionReportRequest @@ -75,11 +85,14 @@ from .store_card_request import StoreCardRequest from .update_card_request import UpdateCardRequest from .update_fraud_check_request import UpdateFraudCheckRequest +from .update_fraud_check_status_request import UpdateFraudCheckStatusRequest from .update_member_request import UpdateMemberRequest from .update_merchant_pos_commissions_request import UpdateMerchantPosCommissionsRequest from .update_merchant_pos_request import UpdateMerchantPosRequest +from .update_merchant_pos_status_request import UpdateMerchantPosStatusRequest from .update_payment_transaction_request import UpdatePaymentTransactionRequest from .update_payout_account_request import UpdatePayoutAccountRequest from .update_product_request import UpdateProductRequest from .update_wallet_request import UpdateWalletRequest +from .verify_bnpl_payment_request import VerifyBnplPaymentRequest from .verify_card_request import VerifyCardRequest diff --git a/craftgate/request/apple_pay_merchant_session_create_request.py b/craftgate/request/apple_pay_merchant_session_create_request.py index 4e7bb4f..abbacdc 100644 --- a/craftgate/request/apple_pay_merchant_session_create_request.py +++ b/craftgate/request/apple_pay_merchant_session_create_request.py @@ -1,4 +1,7 @@ -class ApplePayMerchantSessionCreateRequest(object): +from craftgate.request.common.base_request import BaseRequest + + +class ApplePayMerchantSessionCreateRequest(BaseRequest): def __init__( self, merchant_identifier: str, diff --git a/craftgate/request/approve_bnpl_payment_request.py b/craftgate/request/approve_bnpl_payment_request.py new file mode 100644 index 0000000..206116b --- /dev/null +++ b/craftgate/request/approve_bnpl_payment_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class ApproveBnplPaymentRequest(BaseRequest): + def __init__( + self, + payment_id: Optional[int] = None + ) -> None: + self.payment_id = payment_id diff --git a/craftgate/request/approve_payment_transactions_request.py b/craftgate/request/approve_payment_transactions_request.py index e9d6a4d..9e65e97 100644 --- a/craftgate/request/approve_payment_transactions_request.py +++ b/craftgate/request/approve_payment_transactions_request.py @@ -1,7 +1,9 @@ from typing import List +from craftgate.request.common.base_request import BaseRequest -class ApprovePaymentTransactionsRequest(object): + +class ApprovePaymentTransactionsRequest(BaseRequest): def __init__( self, payment_transaction_ids: List[int], diff --git a/craftgate/request/bnpl_limit_inquiry_request.py b/craftgate/request/bnpl_limit_inquiry_request.py index f2c5513..96fa493 100644 --- a/craftgate/request/bnpl_limit_inquiry_request.py +++ b/craftgate/request/bnpl_limit_inquiry_request.py @@ -4,10 +4,11 @@ from craftgate.model.apm_type import ApmType from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class BnplLimitInquiryRequest(object): +class BnplLimitInquiryRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/bnpl_payment_offer_request.py b/craftgate/request/bnpl_payment_offer_request.py index 2e33949..77f3a98 100644 --- a/craftgate/request/bnpl_payment_offer_request.py +++ b/craftgate/request/bnpl_payment_offer_request.py @@ -3,10 +3,11 @@ from craftgate.model.apm_type import ApmType from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.bnpl_payment_cart_item import BnplPaymentCartItem -class BnplPaymentOfferRequest(object): +class BnplPaymentOfferRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/cancel_withdraw_request.py b/craftgate/request/cancel_withdraw_request.py new file mode 100644 index 0000000..f52e480 --- /dev/null +++ b/craftgate/request/cancel_withdraw_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class CancelWithdrawRequest(BaseRequest): + def __init__( + self, + withdraw_id: Optional[int] = None + ) -> None: + self.withdraw_id = withdraw_id diff --git a/craftgate/request/check_masterpass_user_request.py b/craftgate/request/check_masterpass_user_request.py index 22ff6b3..873dd87 100644 --- a/craftgate/request/check_masterpass_user_request.py +++ b/craftgate/request/check_masterpass_user_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CheckMasterpassUserRequest(object): + +class CheckMasterpassUserRequest(BaseRequest): def __init__( self, masterpass_gsm_number: Optional[str] = None diff --git a/craftgate/request/clone_card_request.py b/craftgate/request/clone_card_request.py index d9a2219..40cff56 100644 --- a/craftgate/request/clone_card_request.py +++ b/craftgate/request/clone_card_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CloneCardRequest(object): + +class CloneCardRequest(BaseRequest): def __init__( self, source_card_user_key: Optional[str] = None, diff --git a/craftgate/request/common/__init__.py b/craftgate/request/common/__init__.py new file mode 100644 index 0000000..d6fb92f --- /dev/null +++ b/craftgate/request/common/__init__.py @@ -0,0 +1 @@ +from .base_request import BaseRequest diff --git a/craftgate/request/common/base_request.py b/craftgate/request/common/base_request.py new file mode 100644 index 0000000..909d801 --- /dev/null +++ b/craftgate/request/common/base_request.py @@ -0,0 +1,31 @@ +from typing import Optional, TypeVar + +T = TypeVar("T", bound="BaseRequest") + + +class BaseRequest(object): + """Base class for request objects sent to the Craftgate API. + + The backing attribute is underscore-prefixed, which is what keeps it out of the + JSON body, the request signature and query parameters. + """ + + _idempotency_key: Optional[str] = None + + @property + def idempotency_key(self) -> Optional[str]: + """Optional key, sent as the ``x-idempotency-key`` header so a mutating call can + be safely retried.""" + return self._idempotency_key + + @idempotency_key.setter + def idempotency_key(self, value: Optional[str]) -> None: + self._idempotency_key = value + + def with_idempotency_key(self: T, value: Optional[str]) -> T: + """Sets the idempotency key and returns the request, for inline use. + + Returns the concrete type so the result stays assignable under a type checker. + """ + self._idempotency_key = value + return self diff --git a/craftgate/request/complete_apm_payment_request.py b/craftgate/request/complete_apm_payment_request.py index ad0f30d..2d311aa 100644 --- a/craftgate/request/complete_apm_payment_request.py +++ b/craftgate/request/complete_apm_payment_request.py @@ -1,7 +1,9 @@ from typing import Dict, Optional +from craftgate.request.common.base_request import BaseRequest -class CompleteApmPaymentRequest(object): + +class CompleteApmPaymentRequest(BaseRequest): def __init__( self, payment_id: Optional[int] = None, diff --git a/craftgate/request/complete_bkm_express_request.py b/craftgate/request/complete_bkm_express_request.py index 7868552..505fd56 100644 --- a/craftgate/request/complete_bkm_express_request.py +++ b/craftgate/request/complete_bkm_express_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CompleteBkmExpressRequest: + +class CompleteBkmExpressRequest(BaseRequest): def __init__( self, status: bool = False, diff --git a/craftgate/request/complete_pos_apm_payment_request.py b/craftgate/request/complete_pos_apm_payment_request.py index db5643f..241fc89 100644 --- a/craftgate/request/complete_pos_apm_payment_request.py +++ b/craftgate/request/complete_pos_apm_payment_request.py @@ -1,7 +1,9 @@ from typing import Any, Dict, Optional +from craftgate.request.common.base_request import BaseRequest -class CompletePosApmPaymentRequest(object): + +class CompletePosApmPaymentRequest(BaseRequest): def __init__( self, payment_id: Optional[int] = None, diff --git a/craftgate/request/complete_three_ds_payment_request.py b/craftgate/request/complete_three_ds_payment_request.py index 13ae1c7..ed9ed2c 100644 --- a/craftgate/request/complete_three_ds_payment_request.py +++ b/craftgate/request/complete_three_ds_payment_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CompleteThreeDSPaymentRequest(object): + +class CompleteThreeDSPaymentRequest(BaseRequest): def __init__( self, payment_id: Optional[int] = None diff --git a/craftgate/request/create_apm_payment_request.py b/craftgate/request/create_apm_payment_request.py index d6e913e..3f5a34d 100644 --- a/craftgate/request/create_apm_payment_request.py +++ b/craftgate/request/create_apm_payment_request.py @@ -4,10 +4,11 @@ from craftgate.model.apm_type import ApmType from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class CreateApmPaymentRequest(object): +class CreateApmPaymentRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/create_deposit_payment_request.py b/craftgate/request/create_deposit_payment_request.py index b3cf1e5..aa90e3e 100644 --- a/craftgate/request/create_deposit_payment_request.py +++ b/craftgate/request/create_deposit_payment_request.py @@ -2,11 +2,12 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.card import Card from craftgate.request.dto.routing_options import RoutingOptions -class CreateDepositPaymentRequest(object): +class CreateDepositPaymentRequest(BaseRequest): def __init__( self, buyer_member_id: Optional[int] = None, diff --git a/craftgate/request/create_fund_transfer_deposit_payment_request.py b/craftgate/request/create_fund_transfer_deposit_payment_request.py index bb7467f..948f6a0 100644 --- a/craftgate/request/create_fund_transfer_deposit_payment_request.py +++ b/craftgate/request/create_fund_transfer_deposit_payment_request.py @@ -1,8 +1,10 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CreateFundTransferDepositPaymentRequest(object): + +class CreateFundTransferDepositPaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/create_instant_wallet_settlement_request.py b/craftgate/request/create_instant_wallet_settlement_request.py index 6c41f44..df04a96 100644 --- a/craftgate/request/create_instant_wallet_settlement_request.py +++ b/craftgate/request/create_instant_wallet_settlement_request.py @@ -1,7 +1,9 @@ from typing import List, Optional +from craftgate.request.common.base_request import BaseRequest -class CreateInstantWalletSettlementRequest(object): + +class CreateInstantWalletSettlementRequest(BaseRequest): def __init__( self, excluded_sub_merchant_member_ids: Optional[List[int]] = None diff --git a/craftgate/request/create_member_request.py b/craftgate/request/create_member_request.py index 3dde6f9..b614918 100644 --- a/craftgate/request/create_member_request.py +++ b/craftgate/request/create_member_request.py @@ -3,9 +3,10 @@ from craftgate.model.member_type import MemberType from craftgate.model.settlement_earnings_destination import SettlementEarningsDestination +from craftgate.request.common.base_request import BaseRequest -class CreateMemberRequest(object): +class CreateMemberRequest(BaseRequest): def __init__( self, member_external_id: Optional[str] = None, diff --git a/craftgate/request/create_merchant_pos_request.py b/craftgate/request/create_merchant_pos_request.py index 1629900..d6ccf8f 100644 --- a/craftgate/request/create_merchant_pos_request.py +++ b/craftgate/request/create_merchant_pos_request.py @@ -4,10 +4,11 @@ from craftgate.model.payment_authentication_type import PaymentAuthenticationType from craftgate.model.pos_integrator import PosIntegrator from craftgate.model.pos_status import PosStatus +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.create_merchant_pos_user import CreateMerchantPosUser -class CreateMerchantPosRequest(object): +class CreateMerchantPosRequest(BaseRequest): def __init__( self, status: PosStatus = PosStatus.AUTOPILOT, diff --git a/craftgate/request/create_merchant_request.py b/craftgate/request/create_merchant_request.py index 9e06385..5c47eb5 100644 --- a/craftgate/request/create_merchant_request.py +++ b/craftgate/request/create_merchant_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CreateMerchantRequest(object): + +class CreateMerchantRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/create_payment_request.py b/craftgate/request/create_payment_request.py index d75fc38..c7505a0 100644 --- a/craftgate/request/create_payment_request.py +++ b/craftgate/request/create_payment_request.py @@ -4,13 +4,14 @@ from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_phase import PaymentPhase +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.card import Card from craftgate.request.dto.fraud_check_parameters import FraudCheckParameters from craftgate.request.dto.payment_item import PaymentItem from craftgate.request.dto.routing_options import RoutingOptions -class CreatePaymentRequest(object): +class CreatePaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/create_payment_token_request.py b/craftgate/request/create_payment_token_request.py index bc23383..d5fa659 100644 --- a/craftgate/request/create_payment_token_request.py +++ b/craftgate/request/create_payment_token_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class CreatePaymentTokenRequest(object): + +class CreatePaymentTokenRequest(BaseRequest): def __init__( self, value: Optional[str] = None, diff --git a/craftgate/request/create_payout_account_request.py b/craftgate/request/create_payout_account_request.py index 888492f..9324308 100644 --- a/craftgate/request/create_payout_account_request.py +++ b/craftgate/request/create_payout_account_request.py @@ -3,9 +3,10 @@ from craftgate.model.payout_account_type import PayoutAccountType from craftgate.model.currency import Currency from craftgate.model.account_owner import AccountOwner +from craftgate.request.common.base_request import BaseRequest -class CreatePayoutAccountRequest(object): +class CreatePayoutAccountRequest(BaseRequest): def __init__( self, type: Optional[PayoutAccountType] = None, diff --git a/craftgate/request/create_product_request.py b/craftgate/request/create_product_request.py index 8bea5b3..6b564d0 100644 --- a/craftgate/request/create_product_request.py +++ b/craftgate/request/create_product_request.py @@ -3,9 +3,10 @@ from typing import Optional, Set from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class CreateProductRequest(object): +class CreateProductRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/create_remittance_request.py b/craftgate/request/create_remittance_request.py index 74ee348..28860a7 100644 --- a/craftgate/request/create_remittance_request.py +++ b/craftgate/request/create_remittance_request.py @@ -3,9 +3,10 @@ from craftgate.model.currency import Currency from craftgate.model.remittance_reason_type import RemittanceReasonType +from craftgate.request.common.base_request import BaseRequest -class CreateRemittanceRequest(object): +class CreateRemittanceRequest(BaseRequest): def __init__( self, member_id: Optional[int] = None, diff --git a/craftgate/request/create_report_request.py b/craftgate/request/create_report_request.py index 8a1bede..2de3002 100644 --- a/craftgate/request/create_report_request.py +++ b/craftgate/request/create_report_request.py @@ -3,9 +3,10 @@ from craftgate.model.report_period import ReportPeriod from craftgate.model.report_type import ReportType +from craftgate.request.common.base_request import BaseRequest -class CreateReportRequest: +class CreateReportRequest(BaseRequest): def __init__( self, start_date: Optional[datetime] = None, diff --git a/craftgate/request/create_wallet_request.py b/craftgate/request/create_wallet_request.py index 630d1ae..6a5e339 100644 --- a/craftgate/request/create_wallet_request.py +++ b/craftgate/request/create_wallet_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class CreateWalletRequest(object): +class CreateWalletRequest(BaseRequest): def __init__( self, negative_amount_limit: Optional[Decimal] = None, diff --git a/craftgate/request/create_withdraw_request.py b/craftgate/request/create_withdraw_request.py index 5d2ca9b..641e3bf 100644 --- a/craftgate/request/create_withdraw_request.py +++ b/craftgate/request/create_withdraw_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class CreateWithdrawRequest(object): +class CreateWithdrawRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/delete_merchant_pos_request.py b/craftgate/request/delete_merchant_pos_request.py new file mode 100644 index 0000000..2824d9c --- /dev/null +++ b/craftgate/request/delete_merchant_pos_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class DeleteMerchantPosRequest(BaseRequest): + def __init__( + self, + merchant_pos_id: Optional[int] = None + ) -> None: + self.merchant_pos_id = merchant_pos_id diff --git a/craftgate/request/delete_payment_token_request.py b/craftgate/request/delete_payment_token_request.py new file mode 100644 index 0000000..35f559f --- /dev/null +++ b/craftgate/request/delete_payment_token_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class DeletePaymentTokenRequest(BaseRequest): + def __init__( + self, + token: Optional[str] = None + ) -> None: + self.token = token diff --git a/craftgate/request/delete_payout_account_request.py b/craftgate/request/delete_payout_account_request.py new file mode 100644 index 0000000..883b4ff --- /dev/null +++ b/craftgate/request/delete_payout_account_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class DeletePayoutAccountRequest(BaseRequest): + def __init__( + self, + id: Optional[int] = None + ) -> None: + self.id = id diff --git a/craftgate/request/delete_product_request.py b/craftgate/request/delete_product_request.py new file mode 100644 index 0000000..c12e786 --- /dev/null +++ b/craftgate/request/delete_product_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class DeleteProductRequest(BaseRequest): + def __init__( + self, + id: Optional[int] = None + ) -> None: + self.id = id diff --git a/craftgate/request/delete_stored_card_request.py b/craftgate/request/delete_stored_card_request.py index 9f1cbd0..b17ff23 100644 --- a/craftgate/request/delete_stored_card_request.py +++ b/craftgate/request/delete_stored_card_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class DeleteStoredCardRequest(object): + +class DeleteStoredCardRequest(BaseRequest): def __init__( self, card_user_key: Optional[str] = None, diff --git a/craftgate/request/delete_value_list_request.py b/craftgate/request/delete_value_list_request.py new file mode 100644 index 0000000..bd6b417 --- /dev/null +++ b/craftgate/request/delete_value_list_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class DeleteValueListRequest(BaseRequest): + def __init__( + self, + list_name: Optional[str] = None + ) -> None: + self.list_name = list_name diff --git a/craftgate/request/disapprove_payment_transactions_request.py b/craftgate/request/disapprove_payment_transactions_request.py index 835f1e4..6429c8d 100644 --- a/craftgate/request/disapprove_payment_transactions_request.py +++ b/craftgate/request/disapprove_payment_transactions_request.py @@ -1,7 +1,9 @@ from typing import List +from craftgate.request.common.base_request import BaseRequest -class DisapprovePaymentTransactionsRequest(object): + +class DisapprovePaymentTransactionsRequest(BaseRequest): def __init__( self, payment_transaction_ids: List[int], diff --git a/craftgate/request/expire_checkout_payment_request.py b/craftgate/request/expire_checkout_payment_request.py new file mode 100644 index 0000000..74c0754 --- /dev/null +++ b/craftgate/request/expire_checkout_payment_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class ExpireCheckoutPaymentRequest(BaseRequest): + def __init__( + self, + token: Optional[str] = None + ) -> None: + self.token = token diff --git a/craftgate/request/fraud_add_card_fingerprint_to_list_request.py b/craftgate/request/fraud_add_card_fingerprint_to_list_request.py index 3658ec1..87c56f0 100644 --- a/craftgate/request/fraud_add_card_fingerprint_to_list_request.py +++ b/craftgate/request/fraud_add_card_fingerprint_to_list_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.fraud_operation import FraudOperation +from craftgate.request.common.base_request import BaseRequest -class FraudAddCardFingerprintToListRequest(object): +class FraudAddCardFingerprintToListRequest(BaseRequest): def __init__( self, label: Optional[str] = None, diff --git a/craftgate/request/fraud_value_list_request.py b/craftgate/request/fraud_value_list_request.py index 453902e..008463b 100644 --- a/craftgate/request/fraud_value_list_request.py +++ b/craftgate/request/fraud_value_list_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.fraud_value_type import FraudValueType +from craftgate.request.common.base_request import BaseRequest -class FraudValueListRequest(object): +class FraudValueListRequest(BaseRequest): def __init__( self, list_name: Optional[str] = None, diff --git a/craftgate/request/init_apm_deposit_payment_request.py b/craftgate/request/init_apm_deposit_payment_request.py index aaf9d0f..8617a11 100644 --- a/craftgate/request/init_apm_deposit_payment_request.py +++ b/craftgate/request/init_apm_deposit_payment_request.py @@ -3,9 +3,10 @@ from craftgate.model.apm_type import ApmType from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class InitApmDepositPaymentRequest(object): +class InitApmDepositPaymentRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/init_apm_payment_request.py b/craftgate/request/init_apm_payment_request.py index 4987e46..9620130 100644 --- a/craftgate/request/init_apm_payment_request.py +++ b/craftgate/request/init_apm_payment_request.py @@ -4,10 +4,11 @@ from craftgate.model.apm_type import ApmType from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class InitApmPaymentRequest(object): +class InitApmPaymentRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/init_bkm_express_request.py b/craftgate/request/init_bkm_express_request.py index c12d6d7..de64c8c 100644 --- a/craftgate/request/init_bkm_express_request.py +++ b/craftgate/request/init_bkm_express_request.py @@ -4,10 +4,11 @@ from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_phase import PaymentPhase +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class InitBkmExpressRequest: +class InitBkmExpressRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/init_checkout_card_verify_request.py b/craftgate/request/init_checkout_card_verify_request.py index 42085d9..ad3f1cf 100644 --- a/craftgate/request/init_checkout_card_verify_request.py +++ b/craftgate/request/init_checkout_card_verify_request.py @@ -3,9 +3,10 @@ from craftgate.model.card_verification_auth_type import CardVerificationAuthType from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class InitCheckoutCardVerifyRequest(object): +class InitCheckoutCardVerifyRequest(BaseRequest): def __init__( self, verification_price: Optional[Decimal] = None, diff --git a/craftgate/request/init_checkout_payment_request.py b/craftgate/request/init_checkout_payment_request.py index 6fd7c0a..b127334 100644 --- a/craftgate/request/init_checkout_payment_request.py +++ b/craftgate/request/init_checkout_payment_request.py @@ -5,13 +5,14 @@ from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_method import PaymentMethod from craftgate.model.payment_phase import PaymentPhase +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.custom_installment import CustomInstallment from craftgate.request.dto.fraud_check_parameters import FraudCheckParameters from craftgate.request.dto.payment_item import PaymentItem from craftgate.request.dto.routing_options import RoutingOptions -class InitCheckoutPaymentRequest(object): +class InitCheckoutPaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/init_garanti_pay_payment_request.py b/craftgate/request/init_garanti_pay_payment_request.py index 078537a..8631944 100644 --- a/craftgate/request/init_garanti_pay_payment_request.py +++ b/craftgate/request/init_garanti_pay_payment_request.py @@ -3,11 +3,12 @@ from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.garanti_pay_installment import GarantiPayInstallment from craftgate.request.dto.payment_item import PaymentItem -class InitGarantiPayPaymentRequest(object): +class InitGarantiPayPaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/init_juzdan_payment_request.py b/craftgate/request/init_juzdan_payment_request.py index 6bc96f1..7befa8a 100644 --- a/craftgate/request/init_juzdan_payment_request.py +++ b/craftgate/request/init_juzdan_payment_request.py @@ -4,10 +4,11 @@ from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_phase import PaymentPhase +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class InitJuzdanPaymentRequest(object): +class InitJuzdanPaymentRequest(BaseRequest): class ClientType: M = "M" W = "W" diff --git a/craftgate/request/init_meal_voucher_card_tokenization_request.py b/craftgate/request/init_meal_voucher_card_tokenization_request.py index 63adc63..00b09b1 100644 --- a/craftgate/request/init_meal_voucher_card_tokenization_request.py +++ b/craftgate/request/init_meal_voucher_card_tokenization_request.py @@ -1,10 +1,11 @@ from typing import Optional from craftgate.model.apm_type import ApmType +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.meal_voucher_card_tokenization_data import MealVoucherCardTokenizationData -class InitMealVoucherCardTokenizationRequest: +class InitMealVoucherCardTokenizationRequest(BaseRequest): def __init__( self, apm_type: Optional[ApmType] = None, diff --git a/craftgate/request/init_multi_payment_request.py b/craftgate/request/init_multi_payment_request.py index 18bff49..6856e9b 100644 --- a/craftgate/request/init_multi_payment_request.py +++ b/craftgate/request/init_multi_payment_request.py @@ -5,10 +5,11 @@ from craftgate.model.currency import Currency from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_method import PaymentMethod +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.payment_item import PaymentItem -class InitMultiPaymentRequest(object): +class InitMultiPaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/init_pos_apm_payment_request.py b/craftgate/request/init_pos_apm_payment_request.py index e3f791c..0b8a577 100644 --- a/craftgate/request/init_pos_apm_payment_request.py +++ b/craftgate/request/init_pos_apm_payment_request.py @@ -4,12 +4,13 @@ from craftgate.model.payment_group import PaymentGroup from craftgate.model.payment_phase import PaymentPhase from craftgate.model.pos_apm_payment_provider import PosApmPaymentProvider +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.fraud_check_parameters import FraudCheckParameters from craftgate.request.dto.payment_item import PaymentItem from craftgate.request.dto.pos_apm_installment import PosApmInstallment -class InitPosApmPaymentRequest(object): +class InitPosApmPaymentRequest(BaseRequest): def __init__( self, price: Optional[Decimal] = None, diff --git a/craftgate/request/masterpass_payment_complete_request.py b/craftgate/request/masterpass_payment_complete_request.py index 495cbd8..a53632e 100644 --- a/craftgate/request/masterpass_payment_complete_request.py +++ b/craftgate/request/masterpass_payment_complete_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class MasterpassPaymentCompleteRequest(object): + +class MasterpassPaymentCompleteRequest(BaseRequest): def __init__( self, reference_id: Optional[str] = None, diff --git a/craftgate/request/masterpass_payment_threeds_complete_request.py b/craftgate/request/masterpass_payment_threeds_complete_request.py index 54fd34f..4d2b313 100644 --- a/craftgate/request/masterpass_payment_threeds_complete_request.py +++ b/craftgate/request/masterpass_payment_threeds_complete_request.py @@ -1,6 +1,8 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class MasterpassPaymentThreeDSCompleteRequest(object): + +class MasterpassPaymentThreeDSCompleteRequest(BaseRequest): def __init__(self, payment_id: Optional[int] = None) -> None: self.payment_id = payment_id diff --git a/craftgate/request/masterpass_payment_threeds_init_request.py b/craftgate/request/masterpass_payment_threeds_init_request.py index 8fe9673..d77e0b7 100644 --- a/craftgate/request/masterpass_payment_threeds_init_request.py +++ b/craftgate/request/masterpass_payment_threeds_init_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class MasterpassPaymentThreeDSInitRequest(object): + +class MasterpassPaymentThreeDSInitRequest(BaseRequest): def __init__( self, reference_id: Optional[str] = None, diff --git a/craftgate/request/masterpass_payment_token_generate_request.py b/craftgate/request/masterpass_payment_token_generate_request.py index 1a4635f..3bc75b5 100644 --- a/craftgate/request/masterpass_payment_token_generate_request.py +++ b/craftgate/request/masterpass_payment_token_generate_request.py @@ -2,10 +2,11 @@ from craftgate.model.loyalty import Loyalty from craftgate.model.masterpass_validation_type import MasterpassValidationType +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.masterpass_create_payment import MasterpassCreatePayment -class MasterpassPaymentTokenGenerateRequest(object): +class MasterpassPaymentTokenGenerateRequest(BaseRequest): def __init__( self, msisdn: Optional[str] = None, diff --git a/craftgate/request/masterpass_retrieve_loyalties_request.py b/craftgate/request/masterpass_retrieve_loyalties_request.py index 786be2b..4397b82 100644 --- a/craftgate/request/masterpass_retrieve_loyalties_request.py +++ b/craftgate/request/masterpass_retrieve_loyalties_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class MasterpassRetrieveLoyaltiesRequest(object): + +class MasterpassRetrieveLoyaltiesRequest(BaseRequest): def __init__( self, msisdn: Optional[str] = None, diff --git a/craftgate/request/meal_voucher_card_tokenization_complete_request.py b/craftgate/request/meal_voucher_card_tokenization_complete_request.py index ffab182..ec79bc3 100644 --- a/craftgate/request/meal_voucher_card_tokenization_complete_request.py +++ b/craftgate/request/meal_voucher_card_tokenization_complete_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class MealVoucherCardTokenizationCompleteRequest: + +class MealVoucherCardTokenizationCompleteRequest(BaseRequest): def __init__( self, validation_code: Optional[str] = None diff --git a/craftgate/request/meal_voucher_card_tokenization_regenerate_request.py b/craftgate/request/meal_voucher_card_tokenization_regenerate_request.py index db72884..11c247a 100644 --- a/craftgate/request/meal_voucher_card_tokenization_regenerate_request.py +++ b/craftgate/request/meal_voucher_card_tokenization_regenerate_request.py @@ -1,8 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.meal_voucher_card_tokenization_data import MealVoucherCardTokenizationData -class MealVoucherCardTokenizationRegenerateRequest: +class MealVoucherCardTokenizationRegenerateRequest(BaseRequest): def __init__( self, meal_voucher_card_tokenization_data: Optional[MealVoucherCardTokenizationData] = None diff --git a/craftgate/request/post_auth_payment_request.py b/craftgate/request/post_auth_payment_request.py index 50044a0..fc4e344 100644 --- a/craftgate/request/post_auth_payment_request.py +++ b/craftgate/request/post_auth_payment_request.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class PostAuthPaymentRequest(object): + +class PostAuthPaymentRequest(BaseRequest): def __init__(self, paid_price: Optional[Decimal] = None) -> None: self.paid_price = paid_price diff --git a/craftgate/request/refund_payment_request.py b/craftgate/request/refund_payment_request.py index f4dfb63..7eec243 100644 --- a/craftgate/request/refund_payment_request.py +++ b/craftgate/request/refund_payment_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.refund_destination_type import RefundDestinationType +from craftgate.request.common.base_request import BaseRequest -class RefundPaymentRequest(object): +class RefundPaymentRequest(BaseRequest): def __init__( self, payment_id: Optional[int] = None, diff --git a/craftgate/request/refund_payment_transaction_mark_as_refunded_request.py b/craftgate/request/refund_payment_transaction_mark_as_refunded_request.py index b9dc8c7..19c4da4 100644 --- a/craftgate/request/refund_payment_transaction_mark_as_refunded_request.py +++ b/craftgate/request/refund_payment_transaction_mark_as_refunded_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.refund_destination_type import RefundDestinationType +from craftgate.request.common.base_request import BaseRequest -class RefundPaymentTransactionMarkAsRefundedRequest(object): +class RefundPaymentTransactionMarkAsRefundedRequest(BaseRequest): def __init__( self, payment_transaction_id: Optional[int] = None, diff --git a/craftgate/request/refund_payment_transaction_request.py b/craftgate/request/refund_payment_transaction_request.py index eb3f43e..bada8d4 100644 --- a/craftgate/request/refund_payment_transaction_request.py +++ b/craftgate/request/refund_payment_transaction_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.refund_destination_type import RefundDestinationType +from craftgate.request.common.base_request import BaseRequest -class RefundPaymentTransactionRequest(object): +class RefundPaymentTransactionRequest(BaseRequest): def __init__( self, payment_transaction_id: Optional[int] = None, diff --git a/craftgate/request/refund_wallet_transaction_to_card_request.py b/craftgate/request/refund_wallet_transaction_to_card_request.py index 56a72ba..74e6f92 100644 --- a/craftgate/request/refund_wallet_transaction_to_card_request.py +++ b/craftgate/request/refund_wallet_transaction_to_card_request.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class RefundWalletTransactionToCardRequest(object): + +class RefundWalletTransactionToCardRequest(BaseRequest): def __init__(self, refund_price: Optional[Decimal] = None) -> None: self.refund_price = refund_price diff --git a/craftgate/request/remove_value_from_value_list_request.py b/craftgate/request/remove_value_from_value_list_request.py new file mode 100644 index 0000000..fa178a7 --- /dev/null +++ b/craftgate/request/remove_value_from_value_list_request.py @@ -0,0 +1,13 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class RemoveValueFromValueListRequest(BaseRequest): + def __init__( + self, + list_name: Optional[str] = None, + value_id: Optional[str] = None + ) -> None: + self.list_name = list_name + self.value_id = value_id diff --git a/craftgate/request/reset_merchant_member_wallet_balance_request.py b/craftgate/request/reset_merchant_member_wallet_balance_request.py index 504cc3e..77115be 100644 --- a/craftgate/request/reset_merchant_member_wallet_balance_request.py +++ b/craftgate/request/reset_merchant_member_wallet_balance_request.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class ResetMerchantMemberWalletBalanceRequest(object): + +class ResetMerchantMemberWalletBalanceRequest(BaseRequest): def __init__(self, wallet_amount: Optional[Decimal] = None) -> None: self.wallet_amount = wallet_amount diff --git a/craftgate/request/retrieve_daily_payment_report_request.py b/craftgate/request/retrieve_daily_payment_report_request.py index 46ee20c..fa204d9 100644 --- a/craftgate/request/retrieve_daily_payment_report_request.py +++ b/craftgate/request/retrieve_daily_payment_report_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.report_file_type import ReportFileType +from craftgate.request.common.base_request import BaseRequest -class RetrieveDailyPaymentReportRequest(object): +class RetrieveDailyPaymentReportRequest(BaseRequest): def __init__( self, report_date: Optional[date] = None, diff --git a/craftgate/request/retrieve_daily_transaction_report_request.py b/craftgate/request/retrieve_daily_transaction_report_request.py index fc9d4d8..e05b8e0 100644 --- a/craftgate/request/retrieve_daily_transaction_report_request.py +++ b/craftgate/request/retrieve_daily_transaction_report_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.report_file_type import ReportFileType +from craftgate.request.common.base_request import BaseRequest -class RetrieveDailyTransactionReportRequest(object): +class RetrieveDailyTransactionReportRequest(BaseRequest): def __init__( self, report_date: Optional[date] = None, diff --git a/craftgate/request/retrieve_loyalties_request.py b/craftgate/request/retrieve_loyalties_request.py index 9266432..8bffaa7 100644 --- a/craftgate/request/retrieve_loyalties_request.py +++ b/craftgate/request/retrieve_loyalties_request.py @@ -1,9 +1,10 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.fraud_check_parameters import FraudCheckParameters -class RetrieveLoyaltiesRequest(object): +class RetrieveLoyaltiesRequest(BaseRequest): def __init__( self, card_number: Optional[str] = None, diff --git a/craftgate/request/retrieve_provider_card_request.py b/craftgate/request/retrieve_provider_card_request.py index 102fb42..dcbfa6b 100644 --- a/craftgate/request/retrieve_provider_card_request.py +++ b/craftgate/request/retrieve_provider_card_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.card_provider import CardProvider +from craftgate.request.common.base_request import BaseRequest -class RetrieveProviderCardRequest(object): +class RetrieveProviderCardRequest(BaseRequest): def __init__( self, provider_card_token: Optional[str] = None, diff --git a/craftgate/request/retrieve_report_request.py b/craftgate/request/retrieve_report_request.py index cd5a6cf..a86ffe3 100644 --- a/craftgate/request/retrieve_report_request.py +++ b/craftgate/request/retrieve_report_request.py @@ -1,8 +1,9 @@ from typing import Optional from craftgate.model.report_file_type import ReportFileType +from craftgate.request.common.base_request import BaseRequest -class RetrieveReportRequest: +class RetrieveReportRequest(BaseRequest): def __init__(self, file_type: Optional[ReportFileType] = None) -> None: self.file_type = file_type diff --git a/craftgate/request/search_bank_account_tracking_records_request.py b/craftgate/request/search_bank_account_tracking_records_request.py index 0fd2690..bf567c0 100644 --- a/craftgate/request/search_bank_account_tracking_records_request.py +++ b/craftgate/request/search_bank_account_tracking_records_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class SearchBankAccountTrackingRecordsRequest(object): +class SearchBankAccountTrackingRecordsRequest(BaseRequest): def __init__( self, currency: Optional[Currency] = None, diff --git a/craftgate/request/search_fraud_checks_request.py b/craftgate/request/search_fraud_checks_request.py index 476fa86..1a83ed4 100644 --- a/craftgate/request/search_fraud_checks_request.py +++ b/craftgate/request/search_fraud_checks_request.py @@ -4,9 +4,10 @@ from craftgate.model.fraud_action import FraudAction from craftgate.model.fraud_check_status import FraudCheckStatus from craftgate.model.payment_status import PaymentStatus +from craftgate.request.common.base_request import BaseRequest -class SearchFraudChecksRequest(object): +class SearchFraudChecksRequest(BaseRequest): def __init__( self, page: Optional[int] = None, diff --git a/craftgate/request/search_fraud_rule_request.py b/craftgate/request/search_fraud_rule_request.py index 4fc03ae..2e6a1c3 100644 --- a/craftgate/request/search_fraud_rule_request.py +++ b/craftgate/request/search_fraud_rule_request.py @@ -3,8 +3,9 @@ from craftgate.model.fraud_action import FraudAction from craftgate.model.fraud_operation import FraudOperation +from craftgate.request.common.base_request import BaseRequest -class SearchFraudRuleRequest(object): +class SearchFraudRuleRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/search_installments_request.py b/craftgate/request/search_installments_request.py index 14aa2db..c6a3f69 100644 --- a/craftgate/request/search_installments_request.py +++ b/craftgate/request/search_installments_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class SearchInstallmentsRequest(object): +class SearchInstallmentsRequest(BaseRequest): def __init__( self, bin_number: Optional[str] = None, diff --git a/craftgate/request/search_members_request.py b/craftgate/request/search_members_request.py index 58c5bcb..babaa7e 100644 --- a/craftgate/request/search_members_request.py +++ b/craftgate/request/search_members_request.py @@ -1,9 +1,10 @@ from typing import Optional, Set from craftgate.model.member_type import MemberType +from craftgate.request.common.base_request import BaseRequest -class SearchMembersRequest(object): +class SearchMembersRequest(BaseRequest): def __init__( self, page: int = 0, diff --git a/craftgate/request/search_merchant_pos_request.py b/craftgate/request/search_merchant_pos_request.py index 2b6b3c0..d79c2b4 100644 --- a/craftgate/request/search_merchant_pos_request.py +++ b/craftgate/request/search_merchant_pos_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class SearchMerchantPosRequest(object): +class SearchMerchantPosRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/search_payment_refunds_request.py b/craftgate/request/search_payment_refunds_request.py index 4c54330..608c9e7 100644 --- a/craftgate/request/search_payment_refunds_request.py +++ b/craftgate/request/search_payment_refunds_request.py @@ -4,9 +4,10 @@ from craftgate.model.currency import Currency from craftgate.model.refund_status import RefundStatus +from craftgate.request.common.base_request import BaseRequest -class SearchPaymentRefundsRequest(object): +class SearchPaymentRefundsRequest(BaseRequest): def __init__( self, page: Optional[int] = None, diff --git a/craftgate/request/search_payment_transaction_refunds_request.py b/craftgate/request/search_payment_transaction_refunds_request.py index 5a91770..d00b620 100644 --- a/craftgate/request/search_payment_transaction_refunds_request.py +++ b/craftgate/request/search_payment_transaction_refunds_request.py @@ -4,9 +4,10 @@ from craftgate.model.currency import Currency from craftgate.model.refund_status import RefundStatus +from craftgate.request.common.base_request import BaseRequest -class SearchPaymentTransactionRefundsRequest(object): +class SearchPaymentTransactionRefundsRequest(BaseRequest): def __init__( self, page: Optional[int] = None, diff --git a/craftgate/request/search_payments_request.py b/craftgate/request/search_payments_request.py index 03b749c..20ec497 100644 --- a/craftgate/request/search_payments_request.py +++ b/craftgate/request/search_payments_request.py @@ -7,9 +7,10 @@ from craftgate.model.payment_source import PaymentSource from craftgate.model.payment_status import PaymentStatus from craftgate.model.payment_type import PaymentType +from craftgate.request.common.base_request import BaseRequest -class SearchPaymentsRequest(object): +class SearchPaymentsRequest(BaseRequest): def __init__( self, page: Optional[int] = None, diff --git a/craftgate/request/search_payout_account_request.py b/craftgate/request/search_payout_account_request.py index a4ef8a7..8c44db8 100644 --- a/craftgate/request/search_payout_account_request.py +++ b/craftgate/request/search_payout_account_request.py @@ -2,9 +2,10 @@ from craftgate.model.account_owner import AccountOwner from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class SearchPayoutAccountRequest(object): +class SearchPayoutAccountRequest(BaseRequest): def __init__( self, currency: Optional[Currency] = None, diff --git a/craftgate/request/search_payout_bounced_transactions_request.py b/craftgate/request/search_payout_bounced_transactions_request.py index 33c7272..c71eead 100644 --- a/craftgate/request/search_payout_bounced_transactions_request.py +++ b/craftgate/request/search_payout_bounced_transactions_request.py @@ -1,8 +1,10 @@ from datetime import datetime from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class SearchPayoutBouncedTransactionsRequest(object): + +class SearchPayoutBouncedTransactionsRequest(BaseRequest): def __init__( self, start_date: Optional[datetime] = None, diff --git a/craftgate/request/search_payout_completed_transactions_request.py b/craftgate/request/search_payout_completed_transactions_request.py index 353c736..c1a92a3 100644 --- a/craftgate/request/search_payout_completed_transactions_request.py +++ b/craftgate/request/search_payout_completed_transactions_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.settlement_type import SettlementType +from craftgate.request.common.base_request import BaseRequest -class SearchPayoutCompletedTransactionsRequest(object): +class SearchPayoutCompletedTransactionsRequest(BaseRequest): def __init__( self, settlement_file_id: Optional[int] = None, diff --git a/craftgate/request/search_payout_rows_request.py b/craftgate/request/search_payout_rows_request.py index 6d670b0..8d37946 100644 --- a/craftgate/request/search_payout_rows_request.py +++ b/craftgate/request/search_payout_rows_request.py @@ -2,9 +2,10 @@ from typing import Optional from craftgate.model.file_status import FileStatus +from craftgate.request.common.base_request import BaseRequest -class SearchPayoutRowsRequest(object): +class SearchPayoutRowsRequest(BaseRequest): def __init__( self, page: int = 0, diff --git a/craftgate/request/search_products_request.py b/craftgate/request/search_products_request.py index a5b74fe..0805b34 100644 --- a/craftgate/request/search_products_request.py +++ b/craftgate/request/search_products_request.py @@ -3,9 +3,10 @@ from typing import Optional from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest -class SearchProductsRequest(object): +class SearchProductsRequest(BaseRequest): def __init__( self, id: Optional[int] = None, diff --git a/craftgate/request/search_stored_cards_request.py b/craftgate/request/search_stored_cards_request.py index 6483ac8..8db2e42 100644 --- a/craftgate/request/search_stored_cards_request.py +++ b/craftgate/request/search_stored_cards_request.py @@ -4,9 +4,10 @@ from craftgate.model.card_association import CardAssociation from craftgate.model.card_expiry_status import CardExpiryStatus from craftgate.model.card_type import CardType +from craftgate.request.common.base_request import BaseRequest -class SearchStoredCardsRequest(object): +class SearchStoredCardsRequest(BaseRequest): def __init__( self, card_alias: Optional[str] = None, diff --git a/craftgate/request/search_wallet_transactions_request.py b/craftgate/request/search_wallet_transactions_request.py index e0fef8a..37db0e0 100644 --- a/craftgate/request/search_wallet_transactions_request.py +++ b/craftgate/request/search_wallet_transactions_request.py @@ -3,9 +3,10 @@ from typing import Optional, Set from craftgate.model.wallet_transaction_type import WalletTransactionType +from craftgate.request.common.base_request import BaseRequest -class SearchWalletTransactionsRequest(object): +class SearchWalletTransactionsRequest(BaseRequest): def __init__( self, page: int = 0, diff --git a/craftgate/request/search_withdraws_request.py b/craftgate/request/search_withdraws_request.py index 841148d..aa717f9 100644 --- a/craftgate/request/search_withdraws_request.py +++ b/craftgate/request/search_withdraws_request.py @@ -4,9 +4,10 @@ from craftgate.model.currency import Currency from craftgate.model.transaction_payout_status import TransactionPayoutStatus +from craftgate.request.common.base_request import BaseRequest -class SearchWithdrawsRequest(object): +class SearchWithdrawsRequest(BaseRequest): def __init__( self, member_id: Optional[int] = None, diff --git a/craftgate/request/store_card_request.py b/craftgate/request/store_card_request.py index 22c9d2e..e0ff208 100644 --- a/craftgate/request/store_card_request.py +++ b/craftgate/request/store_card_request.py @@ -1,9 +1,10 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.encrypted_card import EncryptedCard -class StoreCardRequest(object): +class StoreCardRequest(BaseRequest): def __init__( self, card_holder_name: Optional[str] = None, diff --git a/craftgate/request/update_card_request.py b/craftgate/request/update_card_request.py index a7b61de..315962d 100644 --- a/craftgate/request/update_card_request.py +++ b/craftgate/request/update_card_request.py @@ -1,7 +1,9 @@ from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class UpdateCardRequest(object): + +class UpdateCardRequest(BaseRequest): def __init__( self, card_user_key: Optional[str] = None, diff --git a/craftgate/request/update_fraud_check_request.py b/craftgate/request/update_fraud_check_request.py index 8571a9e..6ce7be9 100644 --- a/craftgate/request/update_fraud_check_request.py +++ b/craftgate/request/update_fraud_check_request.py @@ -1,8 +1,9 @@ from typing import Optional from craftgate.model.fraud_check_status import FraudCheckStatus +from craftgate.request.common.base_request import BaseRequest -class UpdateFraudCheckRequest(object): +class UpdateFraudCheckRequest(BaseRequest): def __init__(self, check_status: Optional[FraudCheckStatus] = None) -> None: self.check_status = check_status diff --git a/craftgate/request/update_fraud_check_status_request.py b/craftgate/request/update_fraud_check_status_request.py new file mode 100644 index 0000000..9d8c9cd --- /dev/null +++ b/craftgate/request/update_fraud_check_status_request.py @@ -0,0 +1,14 @@ +from typing import Optional + +from craftgate.model.fraud_check_status import FraudCheckStatus +from craftgate.request.common.base_request import BaseRequest + + +class UpdateFraudCheckStatusRequest(BaseRequest): + def __init__( + self, + id: Optional[int] = None, + check_status: Optional[FraudCheckStatus] = None + ) -> None: + self.id = id + self.check_status = check_status diff --git a/craftgate/request/update_member_request.py b/craftgate/request/update_member_request.py index 58a558c..ab9212d 100644 --- a/craftgate/request/update_member_request.py +++ b/craftgate/request/update_member_request.py @@ -3,9 +3,10 @@ from craftgate.model.member_type import MemberType from craftgate.model.settlement_earnings_destination import SettlementEarningsDestination +from craftgate.request.common.base_request import BaseRequest -class UpdateMemberRequest(object): +class UpdateMemberRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/update_merchant_pos_commissions_request.py b/craftgate/request/update_merchant_pos_commissions_request.py index 0313c2c..e74b6a6 100644 --- a/craftgate/request/update_merchant_pos_commissions_request.py +++ b/craftgate/request/update_merchant_pos_commissions_request.py @@ -1,8 +1,9 @@ from typing import List, Optional +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.update_merchant_pos_commission import UpdateMerchantPosCommission -class UpdateMerchantPosCommissionsRequest(object): +class UpdateMerchantPosCommissionsRequest(BaseRequest): def __init__(self, commissions: Optional[List[UpdateMerchantPosCommission]] = None) -> None: self.commissions = commissions diff --git a/craftgate/request/update_merchant_pos_request.py b/craftgate/request/update_merchant_pos_request.py index 0d4caec..1db5553 100644 --- a/craftgate/request/update_merchant_pos_request.py +++ b/craftgate/request/update_merchant_pos_request.py @@ -3,10 +3,11 @@ from craftgate.model.card_association import CardAssociation from craftgate.model.payment_authentication_type import PaymentAuthenticationType from craftgate.model.payment_phase import PaymentPhase +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.update_merchant_pos_user import UpdateMerchantPosUser -class UpdateMerchantPosRequest(object): +class UpdateMerchantPosRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/update_merchant_pos_status_request.py b/craftgate/request/update_merchant_pos_status_request.py new file mode 100644 index 0000000..0883f82 --- /dev/null +++ b/craftgate/request/update_merchant_pos_status_request.py @@ -0,0 +1,14 @@ +from typing import Optional + +from craftgate.model.pos_status import PosStatus +from craftgate.request.common.base_request import BaseRequest + + +class UpdateMerchantPosStatusRequest(BaseRequest): + def __init__( + self, + merchant_pos_id: Optional[int] = None, + pos_status: Optional[PosStatus] = None + ) -> None: + self.merchant_pos_id = merchant_pos_id + self.pos_status = pos_status diff --git a/craftgate/request/update_payment_transaction_request.py b/craftgate/request/update_payment_transaction_request.py index 4229eee..21d1371 100644 --- a/craftgate/request/update_payment_transaction_request.py +++ b/craftgate/request/update_payment_transaction_request.py @@ -2,8 +2,10 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class UpdatePaymentTransactionRequest(object): + +class UpdatePaymentTransactionRequest(BaseRequest): def __init__( self, payment_transaction_id: Optional[int] = None, diff --git a/craftgate/request/update_payout_account_request.py b/craftgate/request/update_payout_account_request.py index 25a0056..2457e57 100644 --- a/craftgate/request/update_payout_account_request.py +++ b/craftgate/request/update_payout_account_request.py @@ -1,9 +1,10 @@ from typing import Optional from craftgate.model.payout_account_type import PayoutAccountType +from craftgate.request.common.base_request import BaseRequest -class UpdatePayoutAccountRequest(object): +class UpdatePayoutAccountRequest(BaseRequest): def __init__( self, type: Optional[PayoutAccountType] = None, diff --git a/craftgate/request/update_product_request.py b/craftgate/request/update_product_request.py index 57f7bda..ab13b15 100644 --- a/craftgate/request/update_product_request.py +++ b/craftgate/request/update_product_request.py @@ -4,9 +4,10 @@ from craftgate.model.currency import Currency from craftgate.model.status import Status +from craftgate.request.common.base_request import BaseRequest -class UpdateProductRequest(object): +class UpdateProductRequest(BaseRequest): def __init__( self, name: Optional[str] = None, diff --git a/craftgate/request/update_wallet_request.py b/craftgate/request/update_wallet_request.py index 988d808..e50c762 100644 --- a/craftgate/request/update_wallet_request.py +++ b/craftgate/request/update_wallet_request.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import Optional +from craftgate.request.common.base_request import BaseRequest -class UpdateWalletRequest(object): + +class UpdateWalletRequest(BaseRequest): def __init__(self, negative_amount_limit: Optional[Decimal] = None): self.negative_amount_limit = negative_amount_limit diff --git a/craftgate/request/verify_bnpl_payment_request.py b/craftgate/request/verify_bnpl_payment_request.py new file mode 100644 index 0000000..7fb0752 --- /dev/null +++ b/craftgate/request/verify_bnpl_payment_request.py @@ -0,0 +1,11 @@ +from typing import Optional + +from craftgate.request.common.base_request import BaseRequest + + +class VerifyBnplPaymentRequest(BaseRequest): + def __init__( + self, + payment_id: Optional[int] = None + ) -> None: + self.payment_id = payment_id diff --git a/craftgate/request/verify_card_request.py b/craftgate/request/verify_card_request.py index 001f967..3062c52 100644 --- a/craftgate/request/verify_card_request.py +++ b/craftgate/request/verify_card_request.py @@ -3,10 +3,11 @@ from craftgate.model.card_verification_auth_type import CardVerificationAuthType from craftgate.model.currency import Currency +from craftgate.request.common.base_request import BaseRequest from craftgate.request.dto.verify_card import VerifyCard -class VerifyCardRequest(object): +class VerifyCardRequest(BaseRequest): def __init__( self, card: Optional[VerifyCard] = None, diff --git a/tests/test_bnpl_payment_sample.py b/tests/test_bnpl_payment_sample.py index 09837aa..d2cde58 100644 --- a/tests/test_bnpl_payment_sample.py +++ b/tests/test_bnpl_payment_sample.py @@ -6,7 +6,8 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import ApmType, BnplCartItemType, Currency, PaymentGroup -from craftgate.request import BnplPaymentOfferRequest, InitBnplPaymentRequest +from craftgate.request import ApproveBnplPaymentRequest, BnplPaymentOfferRequest, InitBnplPaymentRequest, \ + VerifyBnplPaymentRequest from craftgate.request.dto import BnplPaymentCartItem, PaymentItem @@ -151,7 +152,7 @@ def test_init_tom_finance_bnpl_payment(self): def test_approve_bnpl_payment(self): payment_id = 1 - response = self.payment.approve_bnpl_payment(payment_id) + response = self.payment.approve_bnpl_payment(ApproveBnplPaymentRequest(payment_id=payment_id)) print(response) self.assertIsNotNone(response.id) @@ -159,7 +160,7 @@ def test_approve_bnpl_payment(self): def test_verify_bnpl_payment(self): payment_id = 1 - response = self.payment.verify_bnpl_payment(payment_id) + response = self.payment.verify_bnpl_payment(VerifyBnplPaymentRequest(payment_id=payment_id)) print(response) self.assertIsNotNone(response.payment_status) diff --git a/tests/test_fraud_adapter.py b/tests/test_fraud_adapter.py index 424dd90..1518cc4 100644 --- a/tests/test_fraud_adapter.py +++ b/tests/test_fraud_adapter.py @@ -6,7 +6,8 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import FraudAction, FraudValueType, FraudCheckStatus from craftgate.model.fraud_operation import FraudOperation -from craftgate.request import FraudValueListRequest, SearchFraudChecksRequest +from craftgate.request import DeleteValueListRequest, FraudValueListRequest, RemoveValueFromValueListRequest, \ + SearchFraudChecksRequest, UpdateFraudCheckStatusRequest from craftgate.request.fraud_add_card_fingerprint_to_list_request import FraudAddCardFingerprintToListRequest from craftgate.request.search_fraud_rule_request import SearchFraudRuleRequest @@ -62,10 +63,11 @@ def test_retrieve_all_value_lists(self): self.assertTrue(resp.items) def test_remove_value_from_value_list(self): - self.fraud.remove_value_from_value_list(list_name="test", value_id="e9bca836-6933-4ca1-a323-cb7e02ae4981") + self.fraud.remove_value_from_value_list(RemoveValueFromValueListRequest( + list_name="test", value_id="e9bca836-6933-4ca1-a323-cb7e02ae4981")) def test_delete_value_list(self): - self.fraud.delete_value_list("ipList") + self.fraud.delete_value_list(DeleteValueListRequest(list_name="ipList")) def test_search_fraud_checks(self): now = datetime.now() @@ -94,7 +96,8 @@ def test_search_fraud_rules(self): self.assertTrue(resp.items) def test_update_fraud_check_status(self): - self.fraud.update_fraud_check_status(int(2613), FraudCheckStatus.FRAUD) + self.fraud.update_fraud_check_status(UpdateFraudCheckStatusRequest( + id=int(2613), check_status=FraudCheckStatus.FRAUD)) if __name__ == "__main__": diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py new file mode 100644 index 0000000..1789a7b --- /dev/null +++ b/tests/test_idempotency.py @@ -0,0 +1,163 @@ +# tests/test_idempotency.py +"""Idempotency key tests. No live credentials needed.""" +import unittest + +from craftgate import RequestOptions +from craftgate.adapter.base_adapter import BaseAdapter +from craftgate.model import FraudCheckStatus, PosStatus +from craftgate.request import ( + CreatePaymentTokenRequest, + DeleteProductRequest, + ExpireCheckoutPaymentRequest, + RemoveValueFromValueListRequest, + SearchProductsRequest, + UpdateFraudCheckStatusRequest, + UpdateMerchantPosStatusRequest, +) +from craftgate.utils.hash_generator import HashGenerator +from craftgate.utils.request_query_params_builder import RequestQueryParamsBuilder +from craftgate.utils.serializer import serialize_request_body + +IDEMPOTENCY_KEY_HEADER_NAME = "x-idempotency-key" +SIGNATURE_HEADER_NAME = "x-signature" +FIXED_RANDOM_KEY = "fixed-random-key" + + +class FixedRandomAdapter(BaseAdapter): + """Pins the random key so signatures are comparable across calls.""" + + def _generate_random_string(self) -> str: + return FIXED_RANDOM_KEY + + +class IdempotencyTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.options = RequestOptions( + api_key="api-key", + secret_key="secret-key", + base_url="https://sandbox-api.craftgate.io" + ) + cls.adapter = FixedRandomAdapter(cls.options) + + def test_key_is_read_back_from_the_request(self): + request = CreatePaymentTokenRequest(value="card-value").with_idempotency_key("idempotency-key-1") + + self.assertEqual("idempotency-key-1", request.idempotency_key) + self.assertEqual("card-value", request.value) + + def test_key_defaults_to_none_and_does_not_leak_between_instances(self): + with_key = CreatePaymentTokenRequest(value="v").with_idempotency_key("idempotency-key-1") + + self.assertEqual("idempotency-key-1", with_key.idempotency_key) + self.assertIsNone(CreatePaymentTokenRequest(value="v").idempotency_key) + + def test_key_is_excluded_from_serialized_body(self): + request = CreatePaymentTokenRequest(value="card-value").with_idempotency_key("idempotency-key-1") + + body = serialize_request_body(request) + + self.assertIn("card-value", body) + self.assertNotIn("idempotencyKey", body) + self.assertNotIn("idempotency_key", body) + self.assertNotIn("idempotency-key-1", body) + + def test_key_is_excluded_from_query_params_of_read_requests(self): + request = SearchProductsRequest(name="A new Product") + request.idempotency_key = "idempotency-key-1" + + query = RequestQueryParamsBuilder.build_query_params(request) + + self.assertIn("name=A", query) + self.assertNotIn("idempotencyKey", query) + self.assertNotIn("idempotency_key", query) + self.assertNotIn("idempotency-key-1", query) + + def test_body_request_sends_the_key_as_a_header(self): + request = CreatePaymentTokenRequest(value="card-value").with_idempotency_key("idempotency-key-1") + + headers = self.adapter._create_headers(request, "/payment/v1/payment-tokens") + + self.assertEqual("idempotency-key-1", headers.get(IDEMPOTENCY_KEY_HEADER_NAME)) + + def test_body_request_without_a_key_sends_no_header(self): + request = CreatePaymentTokenRequest(value="card-value") + + headers = self.adapter._create_headers(request, "/payment/v1/payment-tokens") + + self.assertNotIn(IDEMPOTENCY_KEY_HEADER_NAME, headers) + + def test_bodyless_request_sends_the_key_as_a_header(self): + request = ExpireCheckoutPaymentRequest(token="token-1").with_idempotency_key("idempotency-key-1") + + headers = self.adapter._create_headers( + None, "/payment/v1/checkout-payments/token-1", idempotency_key=request.idempotency_key) + + self.assertEqual("idempotency-key-1", headers.get(IDEMPOTENCY_KEY_HEADER_NAME)) + + def test_bodyless_request_without_a_key_sends_no_header(self): + request = ExpireCheckoutPaymentRequest(token="token-1") + + headers = self.adapter._create_headers( + None, "/payment/v1/checkout-payments/token-1", idempotency_key=request.idempotency_key) + + self.assertNotIn(IDEMPOTENCY_KEY_HEADER_NAME, headers) + + def test_bodyless_signature_is_unchanged_by_the_key(self): + """A regression here rejects every delete/approve/cancel call at the API.""" + path = "/payment/v1/checkout-payments/token-1" + expected = HashGenerator.generate_hash( + base_url=self.options.base_url, + api_key=self.options.api_key, + secret_key=self.options.secret_key, + random_string=FIXED_RANDOM_KEY, + request=None, + path=path, + ) + + with_key = self.adapter._create_headers(None, path, idempotency_key="idempotency-key-1") + without_key = self.adapter._create_headers(None, path) + + self.assertEqual(expected, with_key[SIGNATURE_HEADER_NAME]) + self.assertEqual(expected, without_key[SIGNATURE_HEADER_NAME]) + + def test_body_signature_is_unchanged_by_the_key(self): + path = "/payment/v1/payment-tokens" + expected = HashGenerator.generate_hash( + base_url=self.options.base_url, + api_key=self.options.api_key, + secret_key=self.options.secret_key, + random_string=FIXED_RANDOM_KEY, + request=CreatePaymentTokenRequest(value="card-value"), + path=path, + ) + + with_key = self.adapter._create_headers( + CreatePaymentTokenRequest(value="card-value").with_idempotency_key("idempotency-key-1"), path) + + self.assertEqual(expected, with_key[SIGNATURE_HEADER_NAME]) + + def test_multi_field_wrappers_carry_their_path_variables(self): + remove_value = RemoveValueFromValueListRequest( + list_name="ipList", value_id="value-1").with_idempotency_key("idempotency-key-1") + pos_status = UpdateMerchantPosStatusRequest( + merchant_pos_id=1, pos_status=PosStatus.PASSIVE).with_idempotency_key("idempotency-key-2") + fraud_check = UpdateFraudCheckStatusRequest( + id=2613, check_status=FraudCheckStatus.FRAUD).with_idempotency_key("idempotency-key-3") + + self.assertEqual(("ipList", "value-1", "idempotency-key-1"), + (remove_value.list_name, remove_value.value_id, remove_value.idempotency_key)) + self.assertEqual((1, PosStatus.PASSIVE, "idempotency-key-2"), + (pos_status.merchant_pos_id, pos_status.pos_status, pos_status.idempotency_key)) + self.assertEqual((2613, FraudCheckStatus.FRAUD, "idempotency-key-3"), + (fraud_check.id, fraud_check.check_status, fraud_check.idempotency_key)) + + def test_single_field_wrapper_carries_its_path_variable(self): + request = DeleteProductRequest(id=42).with_idempotency_key("idempotency-key-1") + + self.assertEqual(42, request.id) + self.assertEqual("idempotency-key-1", request.idempotency_key) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_merchant_sample.py b/tests/test_merchant_sample.py index 20cc873..03d89c2 100644 --- a/tests/test_merchant_sample.py +++ b/tests/test_merchant_sample.py @@ -6,8 +6,8 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import CardAssociation, CardBrand, Currency, PaymentAuthenticationType, \ PaymentPhase, PosIntegrator, PosOperationType, PosStatus, PosUserType, Status -from craftgate.request import CreateMerchantPosRequest, SearchMerchantPosRequest, UpdateMerchantPosCommissionsRequest, \ - UpdateMerchantPosRequest +from craftgate.request import CreateMerchantPosRequest, DeleteMerchantPosRequest, SearchMerchantPosRequest, \ + UpdateMerchantPosCommissionsRequest, UpdateMerchantPosRequest, UpdateMerchantPosStatusRequest from craftgate.request.dto import CreateMerchantPosUser, UpdateMerchantPosCommission, UpdateMerchantPosUser @@ -149,7 +149,8 @@ def test_update_merchant_pos(self): def test_update_merchant_pos_status(self): merchant_pos_id = 3353325 - self.merchant.update_merchant_pos_status(merchant_pos_id, PosStatus.PASSIVE) + self.merchant.update_merchant_pos_status(UpdateMerchantPosStatusRequest( + merchant_pos_id=merchant_pos_id, pos_status=PosStatus.PASSIVE)) self.assertTrue(True) def test_retrieve_merchant_pos(self): @@ -162,7 +163,7 @@ def test_retrieve_merchant_pos(self): def test_delete_merchant_pos(self): merchant_pos_id = 3353325 - self.merchant.delete_merchant_pos(merchant_pos_id) + self.merchant.delete_merchant_pos(DeleteMerchantPosRequest(merchant_pos_id=merchant_pos_id)) self.assertTrue(True) def test_search_merchant_poses(self): diff --git a/tests/test_pay_by_link_sample.py b/tests/test_pay_by_link_sample.py index 7867c3b..2584af4 100644 --- a/tests/test_pay_by_link_sample.py +++ b/tests/test_pay_by_link_sample.py @@ -6,7 +6,7 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import Currency, Status -from craftgate.request import CreateProductRequest, SearchProductsRequest, UpdateProductRequest +from craftgate.request import CreateProductRequest, DeleteProductRequest, SearchProductsRequest, UpdateProductRequest class PayByLinkSample(unittest.TestCase): @@ -92,7 +92,7 @@ def test_retrieve_product(self): def test_delete_product(self): product_id = 6807 - self.pay_by_link.delete_product(product_id) + self.pay_by_link.delete_product(DeleteProductRequest(id=product_id)) self.assertTrue(True) def test_search_products(self): diff --git a/tests/test_payment_sample.py b/tests/test_payment_sample.py index 869680c..d77da4f 100644 --- a/tests/test_payment_sample.py +++ b/tests/test_payment_sample.py @@ -12,8 +12,8 @@ from craftgate.request import ApprovePaymentTransactionsRequest, CloneCardRequest, CompleteApmPaymentRequest, \ CompletePosApmPaymentRequest, CompleteThreeDSPaymentRequest, CreateApmPaymentRequest, CreateDepositPaymentRequest, \ CreateFundTransferDepositPaymentRequest, CreatePaymentRequest, DeleteStoredCardRequest, \ - DisapprovePaymentTransactionsRequest, Card, GarantiPayInstallment, InitApmDepositPaymentRequest, \ - InitApmPaymentRequest, InitCheckoutCardVerifyRequest, InitCheckoutPaymentRequest, \ + DisapprovePaymentTransactionsRequest, Card, ExpireCheckoutPaymentRequest, GarantiPayInstallment, \ + InitApmDepositPaymentRequest, InitApmPaymentRequest, InitCheckoutCardVerifyRequest, InitCheckoutPaymentRequest, \ InitGarantiPayPaymentRequest, InitPosApmPaymentRequest, InitThreeDSPaymentRequest, PaymentItem, \ PostAuthPaymentRequest, RefundPaymentRequest, \ RefundPaymentTransactionMarkAsRefundedRequest, RefundPaymentTransactionRequest, RetrieveLoyaltiesRequest, \ @@ -559,7 +559,7 @@ def test_retrieve_checkout_payment(self): def test_expire_checkout_payment(self): token = "a768c57c-5052-4038-857f-1e2cf54253bc" - self.payment.expire_checkout_payment(token) + self.payment.expire_checkout_payment(ExpireCheckoutPaymentRequest(token=token)) def test_create_deposit_payment(self): card = Card() diff --git a/tests/test_payment_token_sample.py b/tests/test_payment_token_sample.py index 764e3d1..b989bf0 100644 --- a/tests/test_payment_token_sample.py +++ b/tests/test_payment_token_sample.py @@ -4,7 +4,7 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import ApmType -from craftgate.request import CreatePaymentTokenRequest +from craftgate.request import CreatePaymentTokenRequest, DeletePaymentTokenRequest class PaymentTokenSample(unittest.TestCase): @@ -37,7 +37,7 @@ def test_create_payment_token(self): def test_delete_payment_token(self): token = "token-to-be-deleted" - self.payment_token.delete_payment_token(token) + self.payment_token.delete_payment_token(DeletePaymentTokenRequest(token=token)) self.assertTrue(True) diff --git a/tests/test_settlement_sample.py b/tests/test_settlement_sample.py index c768167..356dab3 100644 --- a/tests/test_settlement_sample.py +++ b/tests/test_settlement_sample.py @@ -5,7 +5,7 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import AccountOwner, Currency, PayoutAccountType from craftgate.request import CreateInstantWalletSettlementRequest, CreatePayoutAccountRequest, \ - SearchPayoutAccountRequest, UpdatePayoutAccountRequest + DeletePayoutAccountRequest, SearchPayoutAccountRequest, UpdatePayoutAccountRequest class SettlementSample(unittest.TestCase): @@ -75,7 +75,7 @@ def test_search_payout_account(self): self.assertIsNotNone(response.items) def test_delete_payout_account(self): - self.settlement.delete_payout_account(10) + self.settlement.delete_payout_account(DeletePayoutAccountRequest(id=10)) self.assertTrue(True) diff --git a/tests/test_wallet_sample.py b/tests/test_wallet_sample.py index f45cc63..95ff36b 100644 --- a/tests/test_wallet_sample.py +++ b/tests/test_wallet_sample.py @@ -6,9 +6,9 @@ from craftgate import Craftgate, RequestOptions from craftgate.model import Currency, RefundStatus, RemittanceReasonType, RemittanceType, Status, \ TransactionPayoutStatus, WalletTransactionRefundCardTransactionType -from craftgate.request import CreateRemittanceRequest, CreateWalletRequest, CreateWithdrawRequest, \ - RefundWalletTransactionToCardRequest, ResetMerchantMemberWalletBalanceRequest, SearchWalletTransactionsRequest, \ - SearchWithdrawsRequest, UpdateWalletRequest +from craftgate.request import CancelWithdrawRequest, CreateRemittanceRequest, CreateWalletRequest, \ + CreateWithdrawRequest, RefundWalletTransactionToCardRequest, ResetMerchantMemberWalletBalanceRequest, \ + SearchWalletTransactionsRequest, SearchWithdrawsRequest, UpdateWalletRequest class WalletSample(unittest.TestCase): @@ -166,7 +166,7 @@ def test_create_withdraw(self): def test_cancel_withdraw(self): withdraw_id = 1136 - response = self.wallet.cancel_withdraw(withdraw_id) + response = self.wallet.cancel_withdraw(CancelWithdrawRequest(withdraw_id=withdraw_id)) print(response) self.assertIsNotNone(response.id) From 97dfe5eaa8fb24e3dae1e31cc8409e1fe289980d Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Thu, 30 Jul 2026 13:27:13 +0300 Subject: [PATCH 2/9] Idempotency support --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b23b24a..b96588c 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ print(f"Create Payment Result: {resp}") ## Idempotency -Mutating operations (`POST`/`PUT`/`DELETE`) accept an optional idempotency key. Set it on the request object and the +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. @@ -124,6 +124,8 @@ payment.expire_checkout_payment( > 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 From 8dede45bb27c1c681428f15574f77fe0e073acbc Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Thu, 30 Jul 2026 14:42:03 +0300 Subject: [PATCH 3/9] refactor --- craftgate/adapter/base_adapter.py | 23 ++++++++++++++-------- craftgate/adapter/fraud_adapter.py | 6 +++--- craftgate/adapter/merchant_adapter.py | 4 ++-- craftgate/adapter/pay_by_link_adapter.py | 2 +- craftgate/adapter/payment_adapter.py | 6 +++--- craftgate/adapter/payment_token_adapter.py | 2 +- craftgate/adapter/settlement_adapter.py | 2 +- craftgate/adapter/wallet_adapter.py | 2 +- tests/test_idempotency.py | 7 ++++--- 9 files changed, 31 insertions(+), 23 deletions(-) diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index 8502af8..dd8cd85 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -26,19 +26,17 @@ def _create_headers( request_body: Optional[Any], path: str, custom_options: Optional[RequestOptions] = None, - idempotency_key: Optional[str] = None + header_options: Optional[Any] = None ) -> Dict[str, str]: """Builds the request headers. - ``request_body`` is hashed into the signature, so body-less calls must pass ``None`` - and supply the wrapper's key via ``idempotency_key`` instead. + ``request_body`` is hashed into the signature, so body-less calls must pass ``None`` and + supply the request wrapper via ``header_options`` instead. It defaults to the body, so + body-carrying calls need nothing extra. """ options = custom_options or self.request_options random_key = self._generate_random_string() - if idempotency_key is None: - idempotency_key = getattr(request_body, "_idempotency_key", None) - signature = HashGenerator.generate_hash( base_url=options.base_url, api_key=options.api_key, @@ -59,10 +57,19 @@ def _create_headers( if options.language: headers[self.LANGUAGE_HEADER_NAME] = options.language - if idempotency_key is not None: - headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = idempotency_key + self._apply_request_scoped_headers( + headers, header_options if header_options is not None else request_body) return headers + def _apply_request_scoped_headers(self, headers: Dict[str, str], source: Optional[Any]) -> None: + """Applies the options that travel as headers rather than in the payload. + + New request-scoped options are added here and nowhere else. + """ + idempotency_key = getattr(source, "_idempotency_key", None) + if idempotency_key is not None: + headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = idempotency_key + def _generate_random_string(self) -> str: return str(uuid.uuid4()) diff --git a/craftgate/adapter/fraud_adapter.py b/craftgate/adapter/fraud_adapter.py index 92b6d4d..2349a1a 100644 --- a/craftgate/adapter/fraud_adapter.py +++ b/craftgate/adapter/fraud_adapter.py @@ -50,7 +50,7 @@ def update_fraud_check_status(self, request: UpdateFraudCheckStatusRequest) -> N path = "/fraud/v1/fraud-checks/{}/check-status".format(request.id) # The id belongs in the path, so only the status is sent as the body. body = UpdateFraudCheckRequest(check_status=request.check_status) - headers = self._create_headers(body, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(body, path, header_options=request) self._http_client.request( method="PUT", url=self.request_options.base_url + path, @@ -92,7 +92,7 @@ def create_value_list(self, list_name: str, value_type: FraudValueType) -> None: def delete_value_list(self, request: DeleteValueListRequest) -> None: path = "/fraud/v1/value-lists/{}".format(request.list_name) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, @@ -133,7 +133,7 @@ def add_card_fingerprint_to_value_list( 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, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/merchant_adapter.py b/craftgate/adapter/merchant_adapter.py index af6ac11..f11ba19 100644 --- a/craftgate/adapter/merchant_adapter.py +++ b/craftgate/adapter/merchant_adapter.py @@ -42,7 +42,7 @@ def update_merchant_pos(self, merchant_pos_id: int, request: UpdateMerchantPosRe 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, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="PUT", url=self.request_options.base_url + path, @@ -76,7 +76,7 @@ def retrieve(self, merchant_pos_id: int) -> MerchantPosResponse: def delete_merchant_pos(self, request: DeleteMerchantPosRequest) -> None: path = "/merchant/v1/merchant-poses/{}".format(request.merchant_pos_id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/pay_by_link_adapter.py b/craftgate/adapter/pay_by_link_adapter.py index c9d8b88..c9dcfe0 100644 --- a/craftgate/adapter/pay_by_link_adapter.py +++ b/craftgate/adapter/pay_by_link_adapter.py @@ -50,7 +50,7 @@ def retrieve_product(self, product_id: int) -> ProductResponse: def delete_product(self, request: DeleteProductRequest) -> None: path = "/craftlink/v1/products/{}".format(request.id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/payment_adapter.py b/craftgate/adapter/payment_adapter.py index 84ef091..3d10536 100644 --- a/craftgate/adapter/payment_adapter.py +++ b/craftgate/adapter/payment_adapter.py @@ -177,7 +177,7 @@ def retrieve_checkout_payment(self, token: str) -> PaymentResponse: def expire_checkout_payment(self, request: ExpireCheckoutPaymentRequest) -> None: path = "/payment/v1/checkout-payments/{}".format(request.token) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, @@ -528,7 +528,7 @@ def init_bnpl_payment(self, request: InitBnplPaymentRequest) -> InitBnplPaymentR def approve_bnpl_payment(self, request: ApproveBnplPaymentRequest) -> PaymentResponse: path = "/payment/v1/bnpl-payments/{}/approve".format(request.payment_id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) return self._http_client.request( method="POST", url=self.request_options.base_url + path, @@ -539,7 +539,7 @@ def approve_bnpl_payment(self, request: ApproveBnplPaymentRequest) -> PaymentRes def verify_bnpl_payment(self, request: VerifyBnplPaymentRequest) -> BnplPaymentVerifyResponse: path = "/payment/v1/bnpl-payments/{}/verify".format(request.payment_id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) return self._http_client.request( method="POST", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/payment_token_adapter.py b/craftgate/adapter/payment_token_adapter.py index 598b639..3321b98 100644 --- a/craftgate/adapter/payment_token_adapter.py +++ b/craftgate/adapter/payment_token_adapter.py @@ -24,7 +24,7 @@ def create_payment_token(self, request: CreatePaymentTokenRequest) -> PaymentTok def delete_payment_token(self, request: DeletePaymentTokenRequest) -> None: path = "/payment/v1/payment-tokens/{}".format(request.token) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/settlement_adapter.py b/craftgate/adapter/settlement_adapter.py index 78a2cf0..53dd90e 100644 --- a/craftgate/adapter/settlement_adapter.py +++ b/craftgate/adapter/settlement_adapter.py @@ -52,7 +52,7 @@ def update_payout_account(self, id: int, request: UpdatePayoutAccountRequest) -> def delete_payout_account(self, request: DeletePayoutAccountRequest) -> None: path = "/settlement/v1/payout-accounts/{}".format(request.id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) self._http_client.request( method="DELETE", url=self.request_options.base_url + path, diff --git a/craftgate/adapter/wallet_adapter.py b/craftgate/adapter/wallet_adapter.py index 6245a4d..6eed25e 100644 --- a/craftgate/adapter/wallet_adapter.py +++ b/craftgate/adapter/wallet_adapter.py @@ -182,7 +182,7 @@ def create_withdraw(self, request: CreateWithdrawRequest) -> WithdrawResponse: def cancel_withdraw(self, request: CancelWithdrawRequest) -> WithdrawResponse: path = "/wallet/v1/withdraws/{}/cancel".format(request.withdraw_id) - headers = self._create_headers(None, path, idempotency_key=request.idempotency_key) + headers = self._create_headers(None, path, header_options=request) return self._http_client.request( method="POST", url=self.request_options.base_url + path, diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py index 1789a7b..d72e988 100644 --- a/tests/test_idempotency.py +++ b/tests/test_idempotency.py @@ -91,7 +91,7 @@ def test_bodyless_request_sends_the_key_as_a_header(self): request = ExpireCheckoutPaymentRequest(token="token-1").with_idempotency_key("idempotency-key-1") headers = self.adapter._create_headers( - None, "/payment/v1/checkout-payments/token-1", idempotency_key=request.idempotency_key) + None, "/payment/v1/checkout-payments/token-1", header_options=request) self.assertEqual("idempotency-key-1", headers.get(IDEMPOTENCY_KEY_HEADER_NAME)) @@ -99,7 +99,7 @@ def test_bodyless_request_without_a_key_sends_no_header(self): request = ExpireCheckoutPaymentRequest(token="token-1") headers = self.adapter._create_headers( - None, "/payment/v1/checkout-payments/token-1", idempotency_key=request.idempotency_key) + None, "/payment/v1/checkout-payments/token-1", header_options=request) self.assertNotIn(IDEMPOTENCY_KEY_HEADER_NAME, headers) @@ -115,7 +115,8 @@ def test_bodyless_signature_is_unchanged_by_the_key(self): path=path, ) - with_key = self.adapter._create_headers(None, path, idempotency_key="idempotency-key-1") + wrapper = ExpireCheckoutPaymentRequest(token="token-1").with_idempotency_key("idempotency-key-1") + with_key = self.adapter._create_headers(None, path, header_options=wrapper) without_key = self.adapter._create_headers(None, path) self.assertEqual(expected, with_key[SIGNATURE_HEADER_NAME]) From 25f0d6815422e1b188406b98d81499bc737227f2 Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Thu, 30 Jul 2026 14:51:24 +0300 Subject: [PATCH 4/9] refactor and rename --- craftgate/adapter/base_adapter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index dd8cd85..9842c69 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Optional from _version import VERSION +from craftgate.request.common.base_request import BaseRequest from craftgate.request_options import RequestOptions from craftgate.utils.hash_generator import HashGenerator @@ -67,9 +68,11 @@ def _apply_request_scoped_headers(self, headers: Dict[str, str], source: Optiona New request-scoped options are added here and nowhere else. """ - idempotency_key = getattr(source, "_idempotency_key", None) - if idempotency_key is not None: - headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = idempotency_key + if not isinstance(source, BaseRequest): + return + + if source.idempotency_key is not None: + headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = source.idempotency_key def _generate_random_string(self) -> str: return str(uuid.uuid4()) From 33a48cc72404a4c9c09e4c6de24cf0d2533bf31c Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Fri, 31 Jul 2026 15:20:39 +0300 Subject: [PATCH 5/9] Refactor header handling with `ToHeaderOptions` support --- craftgate/adapter/base_adapter.py | 22 +++++++++++++--------- craftgate/adapter/fraud_adapter.py | 6 +++--- craftgate/adapter/merchant_adapter.py | 4 ++-- craftgate/adapter/pay_by_link_adapter.py | 2 +- craftgate/adapter/payment_adapter.py | 6 +++--- craftgate/adapter/payment_token_adapter.py | 2 +- craftgate/adapter/settlement_adapter.py | 2 +- craftgate/adapter/wallet_adapter.py | 2 +- craftgate/request/common/__init__.py | 1 + craftgate/request/common/base_request.py | 6 ++++++ tests/test_idempotency.py | 6 +++--- 11 files changed, 35 insertions(+), 24 deletions(-) diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index 9842c69..f1bfbaa 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -3,6 +3,7 @@ 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 @@ -27,13 +28,13 @@ def _create_headers( request_body: Optional[Any], path: str, custom_options: Optional[RequestOptions] = None, - header_options: Optional[Any] = None + header_options: Optional[HeaderOptions] = None ) -> Dict[str, str]: """Builds the request headers. ``request_body`` is hashed into the signature, so body-less calls must pass ``None`` and - supply the request wrapper via ``header_options`` instead. It defaults to the body, so - body-carrying calls need nothing extra. + supply ``header_options`` instead. It defaults to the body's own options, so body-carrying + calls need nothing extra. """ options = custom_options or self.request_options random_key = self._generate_random_string() @@ -58,21 +59,24 @@ def _create_headers( if options.language: headers[self.LANGUAGE_HEADER_NAME] = options.language - self._apply_request_scoped_headers( - headers, header_options if header_options is not None else request_body) + 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], source: Optional[Any]) -> None: + def _apply_request_scoped_headers( + self, headers: Dict[str, str], header_options: Optional[HeaderOptions] + ) -> None: """Applies the options that travel as headers rather than in the payload. New request-scoped options are added here and nowhere else. """ - if not isinstance(source, BaseRequest): + if header_options is None: return - if source.idempotency_key is not None: - headers[self.IDEMPOTENCY_KEY_HEADER_NAME] = source.idempotency_key + 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()) diff --git a/craftgate/adapter/fraud_adapter.py b/craftgate/adapter/fraud_adapter.py index 2349a1a..96db36c 100644 --- a/craftgate/adapter/fraud_adapter.py +++ b/craftgate/adapter/fraud_adapter.py @@ -50,7 +50,7 @@ def update_fraud_check_status(self, request: UpdateFraudCheckStatusRequest) -> N path = "/fraud/v1/fraud-checks/{}/check-status".format(request.id) # The id belongs in the path, so only the status is sent as the body. body = UpdateFraudCheckRequest(check_status=request.check_status) - headers = self._create_headers(body, path, header_options=request) + 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, @@ -92,7 +92,7 @@ def create_value_list(self, list_name: str, value_type: FraudValueType) -> None: 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) + 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, @@ -133,7 +133,7 @@ def add_card_fingerprint_to_value_list( 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) + 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, diff --git a/craftgate/adapter/merchant_adapter.py b/craftgate/adapter/merchant_adapter.py index f11ba19..268efb9 100644 --- a/craftgate/adapter/merchant_adapter.py +++ b/craftgate/adapter/merchant_adapter.py @@ -42,7 +42,7 @@ def update_merchant_pos(self, merchant_pos_id: int, request: UpdateMerchantPosRe 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) + 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, @@ -76,7 +76,7 @@ def retrieve(self, merchant_pos_id: int) -> MerchantPosResponse: 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) + 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, diff --git a/craftgate/adapter/pay_by_link_adapter.py b/craftgate/adapter/pay_by_link_adapter.py index c9dcfe0..12d3541 100644 --- a/craftgate/adapter/pay_by_link_adapter.py +++ b/craftgate/adapter/pay_by_link_adapter.py @@ -50,7 +50,7 @@ def retrieve_product(self, product_id: int) -> ProductResponse: def delete_product(self, request: DeleteProductRequest) -> None: path = "/craftlink/v1/products/{}".format(request.id) - headers = self._create_headers(None, path, header_options=request) + 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, diff --git a/craftgate/adapter/payment_adapter.py b/craftgate/adapter/payment_adapter.py index 3d10536..fa4414b 100644 --- a/craftgate/adapter/payment_adapter.py +++ b/craftgate/adapter/payment_adapter.py @@ -177,7 +177,7 @@ def retrieve_checkout_payment(self, token: str) -> PaymentResponse: 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) + 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, @@ -528,7 +528,7 @@ def init_bnpl_payment(self, request: InitBnplPaymentRequest) -> InitBnplPaymentR 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) + 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, @@ -539,7 +539,7 @@ def approve_bnpl_payment(self, request: ApproveBnplPaymentRequest) -> PaymentRes 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) + 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, diff --git a/craftgate/adapter/payment_token_adapter.py b/craftgate/adapter/payment_token_adapter.py index 3321b98..f0b5786 100644 --- a/craftgate/adapter/payment_token_adapter.py +++ b/craftgate/adapter/payment_token_adapter.py @@ -24,7 +24,7 @@ def create_payment_token(self, request: CreatePaymentTokenRequest) -> PaymentTok 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) + 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, diff --git a/craftgate/adapter/settlement_adapter.py b/craftgate/adapter/settlement_adapter.py index 53dd90e..d3eedb8 100644 --- a/craftgate/adapter/settlement_adapter.py +++ b/craftgate/adapter/settlement_adapter.py @@ -52,7 +52,7 @@ def update_payout_account(self, id: int, request: UpdatePayoutAccountRequest) -> def delete_payout_account(self, request: DeletePayoutAccountRequest) -> None: path = "/settlement/v1/payout-accounts/{}".format(request.id) - headers = self._create_headers(None, path, header_options=request) + 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, diff --git a/craftgate/adapter/wallet_adapter.py b/craftgate/adapter/wallet_adapter.py index 6eed25e..aa14657 100644 --- a/craftgate/adapter/wallet_adapter.py +++ b/craftgate/adapter/wallet_adapter.py @@ -182,7 +182,7 @@ def create_withdraw(self, request: CreateWithdrawRequest) -> WithdrawResponse: def cancel_withdraw(self, request: CancelWithdrawRequest) -> WithdrawResponse: path = "/wallet/v1/withdraws/{}/cancel".format(request.withdraw_id) - headers = self._create_headers(None, path, header_options=request) + 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, diff --git a/craftgate/request/common/__init__.py b/craftgate/request/common/__init__.py index d6fb92f..4ffa0a5 100644 --- a/craftgate/request/common/__init__.py +++ b/craftgate/request/common/__init__.py @@ -1 +1,2 @@ from .base_request import BaseRequest +from .header_options import HeaderOptions diff --git a/craftgate/request/common/base_request.py b/craftgate/request/common/base_request.py index 909d801..931b9cd 100644 --- a/craftgate/request/common/base_request.py +++ b/craftgate/request/common/base_request.py @@ -1,5 +1,7 @@ from typing import Optional, TypeVar +from craftgate.request.common.header_options import HeaderOptions + T = TypeVar("T", bound="BaseRequest") @@ -22,6 +24,10 @@ def idempotency_key(self) -> Optional[str]: def idempotency_key(self, value: Optional[str]) -> None: self._idempotency_key = value + def to_header_options(self) -> HeaderOptions: + """Narrows this request to just the options the header layer may use.""" + return HeaderOptions(idempotency_key=self._idempotency_key) + def with_idempotency_key(self: T, value: Optional[str]) -> T: """Sets the idempotency key and returns the request, for inline use. diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py index d72e988..7f9d5ff 100644 --- a/tests/test_idempotency.py +++ b/tests/test_idempotency.py @@ -91,7 +91,7 @@ def test_bodyless_request_sends_the_key_as_a_header(self): request = ExpireCheckoutPaymentRequest(token="token-1").with_idempotency_key("idempotency-key-1") headers = self.adapter._create_headers( - None, "/payment/v1/checkout-payments/token-1", header_options=request) + None, "/payment/v1/checkout-payments/token-1", header_options=request.to_header_options()) self.assertEqual("idempotency-key-1", headers.get(IDEMPOTENCY_KEY_HEADER_NAME)) @@ -99,7 +99,7 @@ def test_bodyless_request_without_a_key_sends_no_header(self): request = ExpireCheckoutPaymentRequest(token="token-1") headers = self.adapter._create_headers( - None, "/payment/v1/checkout-payments/token-1", header_options=request) + None, "/payment/v1/checkout-payments/token-1", header_options=request.to_header_options()) self.assertNotIn(IDEMPOTENCY_KEY_HEADER_NAME, headers) @@ -116,7 +116,7 @@ def test_bodyless_signature_is_unchanged_by_the_key(self): ) wrapper = ExpireCheckoutPaymentRequest(token="token-1").with_idempotency_key("idempotency-key-1") - with_key = self.adapter._create_headers(None, path, header_options=wrapper) + with_key = self.adapter._create_headers(None, path, header_options=wrapper.to_header_options()) without_key = self.adapter._create_headers(None, path) self.assertEqual(expected, with_key[SIGNATURE_HEADER_NAME]) From 7f20099f216a2b3c550818a462ea37a09261db15 Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Fri, 31 Jul 2026 15:20:58 +0300 Subject: [PATCH 6/9] Refactor header handling with `ToHeaderOptions` support --- craftgate/request/common/header_options.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 craftgate/request/common/header_options.py diff --git a/craftgate/request/common/header_options.py b/craftgate/request/common/header_options.py new file mode 100644 index 0000000..e4a79ce --- /dev/null +++ b/craftgate/request/common/header_options.py @@ -0,0 +1,12 @@ +from typing import Optional + + +class HeaderOptions(object): + """Carries the request-scoped options that travel as headers rather than in the payload. + + A distinct type from ``BaseRequest`` so the header layer cannot reach path variables or body + fields, and from ``RequestOptions``, which holds client configuration. + """ + + def __init__(self, idempotency_key: Optional[str] = None) -> None: + self.idempotency_key = idempotency_key From b2a4bf338f0db30fa418d37db1dc8bc84591c032 Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Fri, 31 Jul 2026 15:33:47 +0300 Subject: [PATCH 7/9] fix --- craftgate/__init__.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/craftgate/__init__.py b/craftgate/__init__.py index f67c579..1622f30 100644 --- a/craftgate/__init__.py +++ b/craftgate/__init__.py @@ -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) From 333aa8b3ff220ff4f59e487641db11cd8bab6423 Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Fri, 31 Jul 2026 16:37:20 +0300 Subject: [PATCH 8/9] Simplify code by removing redundant comments and documentation --- craftgate/adapter/base_adapter.py | 11 ----------- craftgate/adapter/fraud_adapter.py | 2 -- craftgate/request/common/base_request.py | 13 ------------- craftgate/request/common/header_options.py | 6 ------ tests/test_idempotency.py | 7 ------- 5 files changed, 39 deletions(-) diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index f1bfbaa..e95f05f 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -7,7 +7,6 @@ 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" @@ -30,12 +29,6 @@ def _create_headers( custom_options: Optional[RequestOptions] = None, header_options: Optional[HeaderOptions] = None ) -> Dict[str, str]: - """Builds the request headers. - - ``request_body`` is hashed into the signature, so body-less calls must pass ``None`` and - supply ``header_options`` instead. It defaults to the body's own options, so body-carrying - calls need nothing extra. - """ options = custom_options or self.request_options random_key = self._generate_random_string() @@ -68,10 +61,6 @@ def _create_headers( def _apply_request_scoped_headers( self, headers: Dict[str, str], header_options: Optional[HeaderOptions] ) -> None: - """Applies the options that travel as headers rather than in the payload. - - New request-scoped options are added here and nowhere else. - """ if header_options is None: return diff --git a/craftgate/adapter/fraud_adapter.py b/craftgate/adapter/fraud_adapter.py index 96db36c..82830b9 100644 --- a/craftgate/adapter/fraud_adapter.py +++ b/craftgate/adapter/fraud_adapter.py @@ -16,7 +16,6 @@ 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) @@ -48,7 +47,6 @@ def search_fraud_rules(self, request: SearchFraudRuleRequest) -> FraudRuleRespon def update_fraud_check_status(self, request: UpdateFraudCheckStatusRequest) -> None: path = "/fraud/v1/fraud-checks/{}/check-status".format(request.id) - # The id belongs in the path, so only the status is sent as the body. body = UpdateFraudCheckRequest(check_status=request.check_status) headers = self._create_headers(body, path, header_options=request.to_header_options()) self._http_client.request( diff --git a/craftgate/request/common/base_request.py b/craftgate/request/common/base_request.py index 931b9cd..352e3da 100644 --- a/craftgate/request/common/base_request.py +++ b/craftgate/request/common/base_request.py @@ -4,20 +4,12 @@ T = TypeVar("T", bound="BaseRequest") - class BaseRequest(object): - """Base class for request objects sent to the Craftgate API. - - The backing attribute is underscore-prefixed, which is what keeps it out of the - JSON body, the request signature and query parameters. - """ _idempotency_key: Optional[str] = None @property def idempotency_key(self) -> Optional[str]: - """Optional key, sent as the ``x-idempotency-key`` header so a mutating call can - be safely retried.""" return self._idempotency_key @idempotency_key.setter @@ -25,13 +17,8 @@ def idempotency_key(self, value: Optional[str]) -> None: self._idempotency_key = value def to_header_options(self) -> HeaderOptions: - """Narrows this request to just the options the header layer may use.""" return HeaderOptions(idempotency_key=self._idempotency_key) def with_idempotency_key(self: T, value: Optional[str]) -> T: - """Sets the idempotency key and returns the request, for inline use. - - Returns the concrete type so the result stays assignable under a type checker. - """ self._idempotency_key = value return self diff --git a/craftgate/request/common/header_options.py b/craftgate/request/common/header_options.py index e4a79ce..de8b9d9 100644 --- a/craftgate/request/common/header_options.py +++ b/craftgate/request/common/header_options.py @@ -1,12 +1,6 @@ from typing import Optional - class HeaderOptions(object): - """Carries the request-scoped options that travel as headers rather than in the payload. - - A distinct type from ``BaseRequest`` so the header layer cannot reach path variables or body - fields, and from ``RequestOptions``, which holds client configuration. - """ def __init__(self, idempotency_key: Optional[str] = None) -> None: self.idempotency_key = idempotency_key diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py index 7f9d5ff..771157b 100644 --- a/tests/test_idempotency.py +++ b/tests/test_idempotency.py @@ -1,5 +1,3 @@ -# tests/test_idempotency.py -"""Idempotency key tests. No live credentials needed.""" import unittest from craftgate import RequestOptions @@ -22,14 +20,11 @@ SIGNATURE_HEADER_NAME = "x-signature" FIXED_RANDOM_KEY = "fixed-random-key" - class FixedRandomAdapter(BaseAdapter): - """Pins the random key so signatures are comparable across calls.""" def _generate_random_string(self) -> str: return FIXED_RANDOM_KEY - class IdempotencyTest(unittest.TestCase): @classmethod def setUpClass(cls): @@ -104,7 +99,6 @@ def test_bodyless_request_without_a_key_sends_no_header(self): self.assertNotIn(IDEMPOTENCY_KEY_HEADER_NAME, headers) def test_bodyless_signature_is_unchanged_by_the_key(self): - """A regression here rejects every delete/approve/cancel call at the API.""" path = "/payment/v1/checkout-payments/token-1" expected = HashGenerator.generate_hash( base_url=self.options.base_url, @@ -159,6 +153,5 @@ def test_single_field_wrapper_carries_its_path_variable(self): self.assertEqual(42, request.id) self.assertEqual("idempotency-key-1", request.idempotency_key) - if __name__ == "__main__": unittest.main() From ebee0d73b6be106577ea7f31b95a5b9dae1165ed Mon Sep 17 00:00:00 2001 From: Serhat Tunca Date: Fri, 31 Jul 2026 16:51:02 +0300 Subject: [PATCH 9/9] Add RetrieveCardFromIvrRequest model --- craftgate/request/__init__.py | 1 + craftgate/request/retrieve_card_from_ivr_request.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/craftgate/request/__init__.py b/craftgate/request/__init__.py index 8db42e9..b34ec7b 100644 --- a/craftgate/request/__init__.py +++ b/craftgate/request/__init__.py @@ -61,6 +61,7 @@ from .refund_wallet_transaction_to_card_request import RefundWalletTransactionToCardRequest from .remove_value_from_value_list_request import RemoveValueFromValueListRequest from .reset_merchant_member_wallet_balance_request import ResetMerchantMemberWalletBalanceRequest +from .retrieve_card_from_ivr_request import RetrieveCardFromIvrRequest from .retrieve_daily_payment_report_request import RetrieveDailyPaymentReportRequest from .retrieve_daily_transaction_report_request import RetrieveDailyTransactionReportRequest from .retrieve_loyalties_request import RetrieveLoyaltiesRequest diff --git a/craftgate/request/retrieve_card_from_ivr_request.py b/craftgate/request/retrieve_card_from_ivr_request.py index b1771a2..a48207b 100644 --- a/craftgate/request/retrieve_card_from_ivr_request.py +++ b/craftgate/request/retrieve_card_from_ivr_request.py @@ -1,6 +1,9 @@ from typing import Optional -class RetrieveCardFromIvrRequest(object): +from craftgate.request.common.base_request import BaseRequest + + +class RetrieveCardFromIvrRequest(BaseRequest): def __init__( self, card_user_key: Optional[str] = None,