From c2479aeb62ebfdbe9b9a68e7d59c0f81b3e9de39 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 11 May 2026 16:11:56 +0200 Subject: [PATCH 1/8] Oauth scopes update New payments client methods + unit and integration tests --- checkout_sdk/oauth_scopes.py | 102 +++++++++--------- checkout_sdk/payments/payments.py | 19 +++- checkout_sdk/payments/payments_client.py | 18 +++- tests/conftest.py | 3 +- ...cancel_scheduled_retry_integration_test.py | 50 +++++++++ tests/payments/payments_client_test.py | 24 ++++- .../reverse_payments_integration_test.py | 55 ++++++++++ .../search_payments_integration_test.py | 46 ++++++++ 8 files changed, 263 insertions(+), 54 deletions(-) create mode 100644 tests/payments/cancel_scheduled_retry_integration_test.py create mode 100644 tests/payments/reverse_payments_integration_test.py create mode 100644 tests/payments/search_payments_integration_test.py diff --git a/checkout_sdk/oauth_scopes.py b/checkout_sdk/oauth_scopes.py index b1583534..163fdf55 100644 --- a/checkout_sdk/oauth_scopes.py +++ b/checkout_sdk/oauth_scopes.py @@ -4,71 +4,73 @@ class OAuthScopes(str, Enum): - VAULT = 'vault' - VAULT_INSTRUMENTS = 'vault:instruments' - VAULT_TOKENIZATION = 'vault:tokenization' - VAULT_CUSTOMERS = 'vault:customers' - VAULT_REAL_TIME_ACCOUNT_UPDATER = 'vault:real-time-account-updater' - VAULT_APME_ENROLLMENT = 'vault:apme-enrollment' - VAULT_CARD_METADATA = 'vault:card-metadata' - VAULT_NETWORK_TOKENS = 'vault:network-tokens' - VAULT_GPAYME_ENROLLMENT = 'vault:gpayme-enrollment' - GATEWAY = 'gateway' - GATEWAY_PAYMENT = 'gateway:payment' - GATEWAY_PAYMENT_DETAILS = 'gateway:payment-details' - GATEWAY_PAYMENT_AUTHORIZATION = 'gateway:payment-authorizations' - GATEWAY_PAYMENT_VOIDS = 'gateway:payment-voids' - GATEWAY_PAYMENT_CAPTURES = 'gateway:payment-captures' - GATEWAY_PAYMENT_REFUNDS = 'gateway:payment-refunds' - GATEWAY_PAYMENT_CANCELLATIONS = 'gateway:payment-cancellations' - GATEWAY_PAYMENT_CONTEXTS = 'gateway:payment-contexts' - FX = 'fx' - PAYOUTS_BANK_DETAILS = 'payouts:bank-details' - SESSIONS_APP = 'sessions:app' - SESSIONS_BROWSER = 'sessions:browser' + ACCOUNTS = 'accounts' + BALANCES = 'balances' + BALANCES_VIEW = 'balances:view' + CARD_MANAGEMENT = 'card-management' DISPUTES = 'disputes' - DISPUTES_VIEW = 'disputes:view' - DISPUTES_PROVIDE_EVIDENCE = 'disputes:provide-evidence' DISPUTES_ACCEPT = 'disputes:accept' + DISPUTES_PROVIDE_EVIDENCE = 'disputes:provide-evidence' DISPUTES_SCHEME_FILES = 'disputes:scheme-files' - MARKETPLACE = 'marketplace' - ACCOUNTS = 'accounts' - TRANSFERS = 'transfers' - TRANSFERS_CREATE = 'transfers:create' - TRANSFERS_VIEW = 'transfers:view' - FLOW = 'flow' - FLOW_WORKFLOWS = 'flow:workflows' - FLOW_EVENTS = 'flow:events' - FLOW_REFLOW = 'flow:reflow' + DISPUTES_VIEW = 'disputes:view' FILES = 'files' + FILES_DOWNLOAD = 'files:download' FILES_RETRIEVE = 'files:retrieve' FILES_UPLOAD = 'files:upload' - FILES_DOWNLOAD = 'files:download' - BALANCES = 'balances' - BALANCES_VIEW = 'balances:view' - MIDDLEWARE = 'middleware' - MIDDLEWARE_MERCHANTS_SECRET = 'middleware:merchants-secret' - MIDDLEWARE_MERCHANTS_PUBLIC = 'middleware:merchants-public' - REPORTS = 'reports' - REPORTS_VIEW = 'reports:view' FINANCIAL_ACTIONS = 'financial-actions' FINANCIAL_ACTIONS_VIEW = 'financial-actions:view' - CARD_MANAGEMENT = 'card-management' - ISSUING_CARD_MGMT = 'issuing:card-mgmt' + FLOW = 'flow' + FLOW_EVENTS = 'flow:events' + FLOW_REFLOW = 'flow:reflow' + FLOW_WORKFLOWS = 'flow:workflows' + FORWARD = 'forward' + FORWARD_SECRETS = 'forward:secrets' + FX = 'fx' + GATEWAY = 'gateway' + GATEWAY_PAYMENT = 'gateway:payment' + GATEWAY_PAYMENT_AUTHORIZATION = 'gateway:payment-authorizations' + GATEWAY_PAYMENT_CANCELLATIONS = 'gateway:payment-cancellations' + GATEWAY_PAYMENT_CAPTURES = 'gateway:payment-captures' + GATEWAY_PAYMENT_CONTEXTS = 'gateway:payment-contexts' + GATEWAY_PAYMENT_DETAILS = 'gateway:payment-details' + GATEWAY_PAYMENT_REFUNDS = 'gateway:payment-refunds' + GATEWAY_PAYMENT_VOIDS = 'gateway:payment-voids' + IDENTITY_VERIFICATION = 'identity-verification' ISSUING_CARD_MANAGEMENT_READ = 'issuing:card-management-read' ISSUING_CARD_MANAGEMENT_WRITE = 'issuing:card-management-write' + ISSUING_CARD_MGMT = 'issuing:card-mgmt' ISSUING_CLIENT = 'issuing:client' ISSUING_CONTROLS_READ = 'issuing:controls-read' ISSUING_CONTROLS_WRITE = 'issuing:controls-write' - ISSUING_TRANSACTIONS_READ = 'issuing:transactions-read' - ISSUING_TRANSACTIONS_WRITE = 'issuing:transactions-write' ISSUING_DISPUTES = 'issuing-disputes' ISSUING_DISPUTES_READ = 'issuing:disputes-read' ISSUING_DISPUTES_WRITE = 'issuing:disputes-write' - TRANSACTIONS = 'transactions' - IDENTITY_VERIFICATION = 'identity-verification' + ISSUING_TRANSACTIONS_READ = 'issuing:transactions-read' + ISSUING_TRANSACTIONS_WRITE = 'issuing:transactions-write' + MARKETPLACE = 'marketplace' + MIDDLEWARE = 'middleware' + MIDDLEWARE_GATEWAY = 'middleware:gateway' + MIDDLEWARE_MERCHANTS_PUBLIC = 'middleware:merchants-public' + MIDDLEWARE_MERCHANTS_SECRET = 'middleware:merchants-secret' + MIDDLEWARE_PAYMENT_CONTEXT = 'middleware:payment-context' + PAYMENTS_SEARCH = 'payments:search' PAYMENT_CONTEXT = 'Payment Context' - FORWARD = 'forward' - FORWARD_SECRETS = 'forward:secrets' PAYMENT_SESSIONS = 'payment-sessions' - PAYMENTS_SEARCH = 'payments:search' + PAYOUTS_BANK_DETAILS = 'payouts:bank-details' + REPORTS = 'reports' + REPORTS_VIEW = 'reports:view' + SESSIONS_APP = 'sessions:app' + SESSIONS_BROWSER = 'sessions:browser' + TRANSACTIONS = 'transactions' + TRANSFERS = 'transfers' + TRANSFERS_CREATE = 'transfers:create' + TRANSFERS_VIEW = 'transfers:view' + VAULT = 'vault' + VAULT_APME_ENROLLMENT = 'vault:apme-enrollment' + VAULT_CARD_METADATA = 'vault:card-metadata' + VAULT_CUSTOMERS = 'vault:customers' + VAULT_GPAYME_ENROLLMENT = 'vault:gpayme-enrollment' + VAULT_INSTRUMENTS = 'vault:instruments' + VAULT_NETWORK_TOKENS = 'vault:network-tokens' + VAULT_REAL_TIME_ACCOUNT_UPDATER = 'vault:real-time-account-updater' + VAULT_TOKENIZATION = 'vault:tokenization' diff --git a/checkout_sdk/payments/payments.py b/checkout_sdk/payments/payments.py index f0a1dfa7..6538e7fd 100644 --- a/checkout_sdk/payments/payments.py +++ b/checkout_sdk/payments/payments.py @@ -4,7 +4,7 @@ from enum import Enum from checkout_sdk.common.common import AccountHolder, BankDetails, MarketplaceData, Address, Phone, CustomerRequest, \ - AccountHolderIdentification + AccountHolderIdentification, QueryFilterDateRange from checkout_sdk.common.enums import PaymentSourceType, Currency, Country, AccountType, ChallengeIndicator from checkout_sdk.sessions.sessions import DeliveryTimeframe @@ -664,6 +664,23 @@ class VoidRequest: metadata: dict +# Cancellations +class CancelScheduledRetryRequest: + reference: str + + +# Reversals +class ReversePaymentRequest: + reference: str + metadata: dict + + +# Search +class PaymentsSearchRequest(QueryFilterDateRange): + query: str + limit: int + + class BillingPlan: type: BillingPlanType skip_shipping_address: bool diff --git a/checkout_sdk/payments/payments_client.py b/checkout_sdk/payments/payments_client.py index 528b8846..6b03e49b 100644 --- a/checkout_sdk/payments/payments_client.py +++ b/checkout_sdk/payments/payments_client.py @@ -5,7 +5,8 @@ from checkout_sdk.checkout_configuration import CheckoutConfiguration from checkout_sdk.client import Client from checkout_sdk.payments.payments import PaymentRequest, PayoutRequest, CaptureRequest, AuthorizationRequest, \ - RefundRequest, VoidRequest, PaymentsQueryFilter + RefundRequest, VoidRequest, PaymentsQueryFilter, CancelScheduledRetryRequest, ReversePaymentRequest, \ + PaymentsSearchRequest class PaymentsClient(Client): @@ -32,6 +33,12 @@ def get_payment_actions(self, payment_id: str): return self._api_client.get(self.build_path(self.__PAYMENTS_PATH, payment_id, 'actions'), self._sdk_authorization()) + def cancel_scheduled_retry(self, payment_id: str, + cancel_scheduled_retry_request: CancelScheduledRetryRequest, + idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'cancellations'), + self._sdk_authorization(), cancel_scheduled_retry_request, idempotency_key) + def capture_payment(self, payment_id: str, capture_request: CaptureRequest = None, idempotency_key: str = None): return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'captures'), self._sdk_authorization(), capture_request, idempotency_key) @@ -40,6 +47,11 @@ def refund_payment(self, payment_id: str, refund_request: RefundRequest = None, return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'refunds'), self._sdk_authorization(), refund_request, idempotency_key) + def reverse_payment(self, payment_id: str, reverse_payment_request: ReversePaymentRequest = None, + idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'reversals'), + self._sdk_authorization(), reverse_payment_request, idempotency_key) + def void_payment(self, payment_id: str, void_request: VoidRequest = None, idempotency_key: str = None): return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'voids'), self._sdk_authorization(), void_request, idempotency_key) @@ -48,3 +60,7 @@ def increment_payment_authorization(self, payment_id: str, authorization_request idempotency_key: str = None): return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, payment_id, 'authorizations'), self._sdk_authorization(), authorization_request, idempotency_key) + + def search_payments(self, search_request: PaymentsSearchRequest): + return self._api_client.post(self.build_path(self.__PAYMENTS_PATH, 'search'), + self._sdk_authorization(), search_request) diff --git a/tests/conftest.py b/tests/conftest.py index 4405a7b1..fd0953f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,8 @@ def oauth_api(): OAuthScopes.SESSIONS_APP, OAuthScopes.SESSIONS_BROWSER, OAuthScopes.FX, OAuthScopes.ACCOUNTS, OAuthScopes.FILES, OAuthScopes.TRANSFERS, OAuthScopes.BALANCES_VIEW, OAuthScopes.VAULT_CARD_METADATA, OAuthScopes.FINANCIAL_ACTIONS, - OAuthScopes.VAULT_REAL_TIME_ACCOUNT_UPDATER]) \ + OAuthScopes.VAULT_REAL_TIME_ACCOUNT_UPDATER, OAuthScopes.PAYMENTS_SEARCH, + OAuthScopes.GATEWAY_PAYMENT_CANCELLATIONS]) \ .build() diff --git a/tests/payments/cancel_scheduled_retry_integration_test.py b/tests/payments/cancel_scheduled_retry_integration_test.py new file mode 100644 index 00000000..bb510f6c --- /dev/null +++ b/tests/payments/cancel_scheduled_retry_integration_test.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import + +import pytest + +from checkout_sdk.payments.payments import CancelScheduledRetryRequest +from tests.checkout_test_utils import assert_response, new_idempotency_key +from tests.payments.payments_test_utils import make_card_payment + + +# tests +@pytest.mark.skip(reason='use cancel scheduled retry on demand, only works on payments with a pending scheduled retry') +def test_should_cancel_scheduled_retry(default_api, oauth_api): + payment_response = make_card_payment(default_api) + + cancel_request = build_cancel_scheduled_retry_request(payment_response.reference) + + cancel_response = oauth_api.payments.cancel_scheduled_retry(payment_response.id, cancel_request) + assert_cancel_scheduled_retry_response(cancel_response) + + +@pytest.mark.skip(reason='use cancel scheduled retry on demand, only works on payments with a pending scheduled retry') +def test_should_cancel_scheduled_retry_idempotently(default_api, oauth_api): + payment_response = make_card_payment(default_api) + + cancel_request = build_cancel_scheduled_retry_request(payment_response.reference) + idempotency_key = new_idempotency_key() + + cancel_response_1 = oauth_api.payments.cancel_scheduled_retry(payment_response.id, cancel_request, idempotency_key) + assert_response(cancel_response_1) + + cancel_response_2 = oauth_api.payments.cancel_scheduled_retry(payment_response.id, cancel_request, idempotency_key) + assert_response(cancel_response_2) + + assert cancel_response_1.action_id == cancel_response_2.action_id + + +# common methods + +def build_cancel_scheduled_retry_request(reference: str) -> CancelScheduledRetryRequest: + request = CancelScheduledRetryRequest() + request.reference = reference + return request + + +def assert_cancel_scheduled_retry_response(response): + assert_response(response, + 'http_metadata', + 'reference', + 'action_id', + '_links') diff --git a/tests/payments/payments_client_test.py b/tests/payments/payments_client_test.py index 881ffaf4..c1f5441f 100644 --- a/tests/payments/payments_client_test.py +++ b/tests/payments/payments_client_test.py @@ -3,7 +3,8 @@ from checkout_sdk.common.common import AccountHolder from checkout_sdk.payments.payments_client import PaymentsClient from checkout_sdk.payments.payments import PaymentRequest, PayoutRequest, CaptureRequest, AuthorizationRequest, \ - RequestProviderTokenSource, RefundRequest, VoidRequest + RequestProviderTokenSource, RefundRequest, VoidRequest, CancelScheduledRetryRequest, ReversePaymentRequest, \ + PaymentsSearchRequest @pytest.fixture(scope='class') @@ -37,6 +38,15 @@ def test_get_payment_actions(self, mocker, client: PaymentsClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_payment_actions('payment_id') == 'response' + def test_cancel_scheduled_retry(self, mocker, client: PaymentsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.cancel_scheduled_retry('payment_id', CancelScheduledRetryRequest()) == 'response' + + def test_cancel_scheduled_retry_idempotency_key(self, mocker, client: PaymentsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.cancel_scheduled_retry('payment_id', CancelScheduledRetryRequest(), + 'idempotency_key') == 'response' + def test_capture_payment(self, mocker, client: PaymentsClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.capture_payment('payment_id', CaptureRequest()) == 'response' @@ -53,6 +63,14 @@ def test_refund_payment_idempotency_key(self, mocker, client: PaymentsClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.refund_payment('payment_id', None, 'idempotency_key') == 'response' + def test_reverse_payment(self, mocker, client: PaymentsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.reverse_payment('payment_id', ReversePaymentRequest()) == 'response' + + def test_reverse_payment_idempotency_key(self, mocker, client: PaymentsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.reverse_payment('payment_id', None, 'idempotency_key') == 'response' + def test_void_payment(self, mocker, client: PaymentsClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.void_payment('payment_id', VoidRequest()) == 'response' @@ -70,6 +88,10 @@ def test_increment_payment_authorization_idempotency_key(self, mocker, client: P assert client.increment_payment_authorization('payment_id', AuthorizationRequest(), 'idempotency_key') == 'response' + def test_search_payments(self, mocker, client: PaymentsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.search_payments(PaymentsSearchRequest()) == 'response' + # sources def test_should_request_provider_token_source_payment(self, mocker, client: PaymentsClient): source = RequestProviderTokenSource() diff --git a/tests/payments/reverse_payments_integration_test.py b/tests/payments/reverse_payments_integration_test.py new file mode 100644 index 00000000..a6a580a1 --- /dev/null +++ b/tests/payments/reverse_payments_integration_test.py @@ -0,0 +1,55 @@ +from __future__ import absolute_import + +from checkout_sdk.payments.payments import ReversePaymentRequest +from tests.checkout_test_utils import new_uuid, assert_response, new_idempotency_key, retriable +from tests.payments.payments_test_utils import make_card_payment + + +# tests + +def test_should_reverse_card_payment(default_api): + payment_response = make_card_payment(default_api) + + reverse_request = build_reverse_payment_request() + + reverse_response = retriable(callback=default_api.payments.reverse_payment, + payment_id=payment_response.id, + reverse_payment_request=reverse_request) + assert_reverse_response(reverse_response) + + +def test_should_reverse_card_payment_idempotently(default_api): + payment_response = make_card_payment(default_api) + + reverse_request = build_reverse_payment_request() + idempotency_key = new_idempotency_key() + + reverse_response_1 = retriable(callback=default_api.payments.reverse_payment, + payment_id=payment_response.id, + reverse_payment_request=reverse_request, + idempotency_key=idempotency_key) + assert_response(reverse_response_1) + + reverse_response_2 = retriable(callback=default_api.payments.reverse_payment, + payment_id=payment_response.id, + reverse_payment_request=reverse_request, + idempotency_key=idempotency_key) + assert_response(reverse_response_2) + + assert reverse_response_1.action_id == reverse_response_2.action_id + + +# common methods + +def build_reverse_payment_request() -> ReversePaymentRequest: + request = ReversePaymentRequest() + request.reference = new_uuid() + return request + + +def assert_reverse_response(response): + assert_response(response, + 'http_metadata', + 'reference', + 'action_id', + '_links') diff --git a/tests/payments/search_payments_integration_test.py b/tests/payments/search_payments_integration_test.py new file mode 100644 index 00000000..3cd1e617 --- /dev/null +++ b/tests/payments/search_payments_integration_test.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import + +from datetime import datetime, timezone, timedelta + +import pytest + +from checkout_sdk.payments.payments import PaymentsSearchRequest +from tests.checkout_test_utils import assert_response, retriable +from tests.payments.payments_test_utils import make_card_payment + + +# tests +@pytest.mark.skip(reason='use search payments when needed, skipped because of the time it takes to execute') +def test_should_search_payments(default_api, oauth_api): + payment_response = make_card_payment(default_api) + + search_request = build_payments_search_request(payment_response.id) + + response = retriable(callback=oauth_api.payments.search_payments, + predicate=there_are_search_results, + timeout=5, + search_request=search_request) + assert_search_response(response, payment_response.id) + + +# common methods + +def build_payments_search_request(payment_id: str) -> PaymentsSearchRequest: + request = PaymentsSearchRequest() + request.query = "id:'" + payment_id + "'" + request.limit = 10 + request.from_ = datetime.now(timezone.utc) - timedelta(minutes=5) + request.to = datetime.now(timezone.utc) + timedelta(minutes=5) + return request + + +def assert_search_response(response, payment_id: str): + assert_response(response, + 'http_metadata', + 'data') + assert response.data[0].id == payment_id + + +def there_are_search_results(response) -> bool: + data = getattr(response, 'data', None) + return data is not None and len(data) > 0 From 0b9b54157d56d93897e0b062c70e2b30f4fd7346 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 11 May 2026 16:51:06 +0200 Subject: [PATCH 2/8] New NetworkTokensClient + unit and integration tests --- checkout_sdk/checkout_api.py | 2 + checkout_sdk/networktokens/__init__.py | 0 checkout_sdk/networktokens/network_tokens.py | 63 ++++++++++++++++ .../networktokens/network_tokens_client.py | 42 +++++++++++ tests/networktokens/__init__.py | 0 .../network_tokens_client_test.py | 29 ++++++++ .../network_tokens_integration_test.py | 73 +++++++++++++++++++ 7 files changed, 209 insertions(+) create mode 100644 checkout_sdk/networktokens/__init__.py create mode 100644 checkout_sdk/networktokens/network_tokens.py create mode 100644 checkout_sdk/networktokens/network_tokens_client.py create mode 100644 tests/networktokens/__init__.py create mode 100644 tests/networktokens/network_tokens_client_test.py create mode 100644 tests/networktokens/network_tokens_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 07230fb4..86e820e0 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -35,6 +35,7 @@ from checkout_sdk.identities.iddocumentverification.iddocumentverification_client import IdDocumentVerificationClient from checkout_sdk.identities.applicants.applicants_client import ApplicantsClient from checkout_sdk.identities.identityverification.identityverification_client import IdentityVerificationClient +from checkout_sdk.networktokens.network_tokens_client import NetworkTokensClient def _base_api_client(configuration: CheckoutConfiguration) -> ApiClient: @@ -111,3 +112,4 @@ def __init__(self, configuration: CheckoutConfiguration): self.applicants = ApplicantsClient(api_client=identity_api_client, configuration=configuration) self.identity_verification = IdentityVerificationClient(api_client=identity_api_client, configuration=configuration) + self.network_tokens = NetworkTokensClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/networktokens/__init__.py b/checkout_sdk/networktokens/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/networktokens/network_tokens.py b/checkout_sdk/networktokens/network_tokens.py new file mode 100644 index 00000000..05276016 --- /dev/null +++ b/checkout_sdk/networktokens/network_tokens.py @@ -0,0 +1,63 @@ +from __future__ import absolute_import + +from enum import Enum + +from checkout_sdk.common.enums import PaymentSourceType + + +class NetworkTokenTransactionType(str, Enum): + ECOM = 'ecom' + RECURRING = 'recurring' + POS = 'pos' + AFT = 'aft' + + +class NetworkTokenInitiatedBy(str, Enum): + CARDHOLDER = 'cardholder' + TOKEN_REQUESTOR = 'token_requestor' + + +class NetworkTokenDeleteReason(str, Enum): + FRAUD = 'fraud' + OTHER = 'other' + + +# Network Token Request Source +class NetworkTokenRequestSource: + type: PaymentSourceType + + def __init__(self, type_p: PaymentSourceType): + self.type = type_p + + +class NetworkTokenRequestCardSource(NetworkTokenRequestSource): + number: str + expiry_month: str + expiry_year: str + cvv: str + + def __init__(self): + super().__init__(PaymentSourceType.CARD) + + +class NetworkTokenRequestIdSource(NetworkTokenRequestSource): + id: str + + def __init__(self): + super().__init__(PaymentSourceType.ID) + + +# Provision Network Token +class ProvisionNetworkTokenRequest: + source: NetworkTokenRequestSource + + +# Provision Cryptogram +class RequestCryptogramRequest: + transaction_type: NetworkTokenTransactionType + + +# Delete Network Token +class DeleteNetworkTokenRequest: + initiated_by: NetworkTokenInitiatedBy + reason: NetworkTokenDeleteReason diff --git a/checkout_sdk/networktokens/network_tokens_client.py b/checkout_sdk/networktokens/network_tokens_client.py new file mode 100644 index 00000000..418aec79 --- /dev/null +++ b/checkout_sdk/networktokens/network_tokens_client.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import + +from checkout_sdk.api_client import ApiClient +from checkout_sdk.authorization_type import AuthorizationType +from checkout_sdk.checkout_configuration import CheckoutConfiguration +from checkout_sdk.client import Client +from checkout_sdk.networktokens.network_tokens import ProvisionNetworkTokenRequest, RequestCryptogramRequest, \ + DeleteNetworkTokenRequest + + +class NetworkTokensClient(Client): + __NETWORK_TOKENS_PATH = 'network-tokens' + __CRYPTOGRAMS_PATH = 'cryptograms' + __DELETE_PATH = 'delete' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.OAUTH) + + def provision_network_token(self, provision_network_token_request: ProvisionNetworkTokenRequest): + return self._api_client.post(self.__NETWORK_TOKENS_PATH, + self._sdk_authorization(), + provision_network_token_request) + + def get_network_token(self, network_token_id: str): + return self._api_client.get(self.build_path(self.__NETWORK_TOKENS_PATH, network_token_id), + self._sdk_authorization()) + + def request_cryptogram(self, network_token_id: str, + request_cryptogram_request: RequestCryptogramRequest): + return self._api_client.post( + self.build_path(self.__NETWORK_TOKENS_PATH, network_token_id, self.__CRYPTOGRAMS_PATH), + self._sdk_authorization(), + request_cryptogram_request) + + def delete_network_token(self, network_token_id: str, + delete_network_token_request: DeleteNetworkTokenRequest): + return self._api_client.patch( + self.build_path(self.__NETWORK_TOKENS_PATH, network_token_id, self.__DELETE_PATH), + self._sdk_authorization(), + delete_network_token_request) diff --git a/tests/networktokens/__init__.py b/tests/networktokens/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/networktokens/network_tokens_client_test.py b/tests/networktokens/network_tokens_client_test.py new file mode 100644 index 00000000..0bd635e2 --- /dev/null +++ b/tests/networktokens/network_tokens_client_test.py @@ -0,0 +1,29 @@ +import pytest + +from checkout_sdk.networktokens.network_tokens import ProvisionNetworkTokenRequest, RequestCryptogramRequest, \ + DeleteNetworkTokenRequest +from checkout_sdk.networktokens.network_tokens_client import NetworkTokensClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return NetworkTokensClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestNetworkTokensClient: + + def test_provision_network_token(self, mocker, client: NetworkTokensClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.provision_network_token(ProvisionNetworkTokenRequest()) == 'response' + + def test_get_network_token(self, mocker, client: NetworkTokensClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_network_token('network_token_id') == 'response' + + def test_request_cryptogram(self, mocker, client: NetworkTokensClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.request_cryptogram('network_token_id', RequestCryptogramRequest()) == 'response' + + def test_delete_network_token(self, mocker, client: NetworkTokensClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.delete_network_token('network_token_id', DeleteNetworkTokenRequest()) == 'response' diff --git a/tests/networktokens/network_tokens_integration_test.py b/tests/networktokens/network_tokens_integration_test.py new file mode 100644 index 00000000..946db1ca --- /dev/null +++ b/tests/networktokens/network_tokens_integration_test.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +import pytest + +from checkout_sdk.networktokens.network_tokens import ProvisionNetworkTokenRequest, RequestCryptogramRequest, \ + DeleteNetworkTokenRequest, NetworkTokenRequestIdSource, NetworkTokenTransactionType, NetworkTokenInitiatedBy, \ + NetworkTokenDeleteReason +from tests.checkout_test_utils import assert_response + +PLACEHOLDER_NETWORK_TOKEN_ID = 'nt_xgu3isllqfyu7ktpk5z2yxbwna' +PLACEHOLDER_INSTRUMENT_ID = 'src_wmlfc3zyhqzehihu7giusaaawu' + + +# tests +@pytest.mark.skip(reason='use network token endpoints on demand, requires preexisting instrument and network token ids') +def test_should_provision_network_token(oauth_api): + request = build_provision_network_token_request() + + response = oauth_api.network_tokens.provision_network_token(request) + assert_network_token_response(response) + + +@pytest.mark.skip(reason='use network token endpoints on demand, requires preexisting instrument and network token ids') +def test_should_get_network_token(oauth_api): + response = oauth_api.network_tokens.get_network_token(PLACEHOLDER_NETWORK_TOKEN_ID) + assert_network_token_response(response) + + +@pytest.mark.skip(reason='use network token endpoints on demand, requires preexisting instrument and network token ids') +def test_should_request_cryptogram(oauth_api): + request = build_request_cryptogram_request() + + response = oauth_api.network_tokens.request_cryptogram(PLACEHOLDER_NETWORK_TOKEN_ID, request) + assert_response(response, 'http_metadata', 'cryptogram') + + +@pytest.mark.skip(reason='use network token endpoints on demand, requires preexisting instrument and network token ids') +def test_should_delete_network_token(oauth_api): + request = build_delete_network_token_request() + + response = oauth_api.network_tokens.delete_network_token(PLACEHOLDER_NETWORK_TOKEN_ID, request) + assert_response(response, 'http_metadata') + + +# common methods + +def build_provision_network_token_request() -> ProvisionNetworkTokenRequest: + id_source = NetworkTokenRequestIdSource() + id_source.id = PLACEHOLDER_INSTRUMENT_ID + + request = ProvisionNetworkTokenRequest() + request.source = id_source + return request + + +def build_request_cryptogram_request() -> RequestCryptogramRequest: + request = RequestCryptogramRequest() + request.transaction_type = NetworkTokenTransactionType.ECOM + return request + + +def build_delete_network_token_request() -> DeleteNetworkTokenRequest: + request = DeleteNetworkTokenRequest() + request.initiated_by = NetworkTokenInitiatedBy.TOKEN_REQUESTOR + request.reason = NetworkTokenDeleteReason.OTHER + return request + + +def assert_network_token_response(response): + assert_response(response, + 'http_metadata', + 'card', + 'network_token') From d5541cf85bf0d97bebf2ccd7498f30b714a5889c Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 11 May 2026 17:09:40 +0200 Subject: [PATCH 3/8] New PaymentMethods Client + unit and integration tests --- checkout_sdk/checkout_api.py | 2 + checkout_sdk/paymentmethods/__init__.py | 0 .../paymentmethods/payment_methods.py | 2 + .../paymentmethods/payment_methods_client.py | 21 +++++++++ tests/paymentmethods/__init__.py | 0 .../payment_methods_client_test.py | 15 ++++++ .../payment_methods_integration_test.py | 46 +++++++++++++++++++ 7 files changed, 86 insertions(+) create mode 100644 checkout_sdk/paymentmethods/__init__.py create mode 100644 checkout_sdk/paymentmethods/payment_methods.py create mode 100644 checkout_sdk/paymentmethods/payment_methods_client.py create mode 100644 tests/paymentmethods/__init__.py create mode 100644 tests/paymentmethods/payment_methods_client_test.py create mode 100644 tests/paymentmethods/payment_methods_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 86e820e0..7b006132 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -36,6 +36,7 @@ from checkout_sdk.identities.applicants.applicants_client import ApplicantsClient from checkout_sdk.identities.identityverification.identityverification_client import IdentityVerificationClient from checkout_sdk.networktokens.network_tokens_client import NetworkTokensClient +from checkout_sdk.paymentmethods.payment_methods_client import PaymentMethodsClient def _base_api_client(configuration: CheckoutConfiguration) -> ApiClient: @@ -113,3 +114,4 @@ def __init__(self, configuration: CheckoutConfiguration): self.identity_verification = IdentityVerificationClient(api_client=identity_api_client, configuration=configuration) self.network_tokens = NetworkTokensClient(api_client=base_api_client, configuration=configuration) + self.payment_methods = PaymentMethodsClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/paymentmethods/__init__.py b/checkout_sdk/paymentmethods/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/paymentmethods/payment_methods.py b/checkout_sdk/paymentmethods/payment_methods.py new file mode 100644 index 00000000..876527dd --- /dev/null +++ b/checkout_sdk/paymentmethods/payment_methods.py @@ -0,0 +1,2 @@ +class PaymentMethodsQueryFilter: + processing_channel_id: str diff --git a/checkout_sdk/paymentmethods/payment_methods_client.py b/checkout_sdk/paymentmethods/payment_methods_client.py new file mode 100644 index 00000000..c0e633f7 --- /dev/null +++ b/checkout_sdk/paymentmethods/payment_methods_client.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import + +from checkout_sdk.api_client import ApiClient +from checkout_sdk.authorization_type import AuthorizationType +from checkout_sdk.checkout_configuration import CheckoutConfiguration +from checkout_sdk.client import Client +from checkout_sdk.paymentmethods.payment_methods import PaymentMethodsQueryFilter + + +class PaymentMethodsClient(Client): + __PAYMENT_METHODS_PATH = 'payment-methods' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def get_available_payment_methods(self, processing_channel_id: str): + query = PaymentMethodsQueryFilter() + query.processing_channel_id = processing_channel_id + return self._api_client.get(self.__PAYMENT_METHODS_PATH, self._sdk_authorization(), query) diff --git a/tests/paymentmethods/__init__.py b/tests/paymentmethods/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/paymentmethods/payment_methods_client_test.py b/tests/paymentmethods/payment_methods_client_test.py new file mode 100644 index 00000000..60d876e6 --- /dev/null +++ b/tests/paymentmethods/payment_methods_client_test.py @@ -0,0 +1,15 @@ +import pytest + +from checkout_sdk.paymentmethods.payment_methods_client import PaymentMethodsClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return PaymentMethodsClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestPaymentMethodsClient: + + def test_get_available_payment_methods(self, mocker, client: PaymentMethodsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_available_payment_methods('pc_test_123456') == 'response' diff --git a/tests/paymentmethods/payment_methods_integration_test.py b/tests/paymentmethods/payment_methods_integration_test.py new file mode 100644 index 00000000..f96425d8 --- /dev/null +++ b/tests/paymentmethods/payment_methods_integration_test.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import + +import os + +import pytest + +from checkout_sdk.exception import CheckoutApiException +from tests.checkout_test_utils import assert_response + +INVALID_PROCESSING_CHANNEL_ID = 'pc_test_invalid_channel_id' + + +# tests + +def test_should_get_available_payment_methods(oauth_api): + response = oauth_api.payment_methods.get_available_payment_methods( + os.environ.get('CHECKOUT_PROCESSING_CHANNEL_ID')) + assert_get_available_payment_methods_response(response) + + +def test_should_get_available_payment_methods_with_specific_processing_channel(oauth_api): + response = oauth_api.payment_methods.get_available_payment_methods( + os.environ.get('CHECKOUT_PROCESSING_CHANNEL_ID')) + assert_get_available_payment_methods_response(response) + assert_methods_have_valid_structure(response) + + +def test_should_throw_with_invalid_processing_channel_id(oauth_api): + with pytest.raises(CheckoutApiException) as exc_info: + oauth_api.payment_methods.get_available_payment_methods(INVALID_PROCESSING_CHANNEL_ID) + assert 'processing_channel_id_invalid' in exc_info.value.error_details + + +# common methods + +def assert_get_available_payment_methods_response(response): + assert_response(response, + 'http_metadata', + 'methods') + assert len(response.methods) > 0 + + +def assert_methods_have_valid_structure(response): + for method in response.methods: + assert hasattr(method, 'type') + assert method.type is not None From ebaabfb6b8c7f80f36dc562db114a10456c7995f Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 11 May 2026 17:30:24 +0200 Subject: [PATCH 4/8] Disputes client new endpoints + unit and integration tests --- checkout_sdk/disputes/disputes_client.py | 16 ++++++++++++ tests/disputes/disputes_client_test.py | 8 ++++++ tests/disputes/disputes_integration_test.py | 29 +++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/checkout_sdk/disputes/disputes_client.py b/checkout_sdk/disputes/disputes_client.py index 382782b4..ce4c6426 100644 --- a/checkout_sdk/disputes/disputes_client.py +++ b/checkout_sdk/disputes/disputes_client.py @@ -11,6 +11,7 @@ class DisputesClient(FilesClient): __DISPUTES_PATH = 'disputes' __ACCEPT_PATH = 'accept' __EVIDENCE_PATH = 'evidence' + __ARBITRATION_PATH = 'arbitration' __SUBMITTED_PATH = 'submitted' __SCHEME_FILES_PATH = "schemefiles" @@ -42,6 +43,13 @@ def submit_evidence(self, dispute_id: str): return self._api_client.post(self.build_path(self.__DISPUTES_PATH, dispute_id, self.__EVIDENCE_PATH), self._sdk_authorization()) + def submit_arbitration_evidence(self, dispute_id: str): + return self._api_client.post(self.build_path( + self.__DISPUTES_PATH, dispute_id, + self.__EVIDENCE_PATH, + self.__ARBITRATION_PATH), + self._sdk_authorization()) + def get_compiled_submitted_evidence(self, dispute_id: str): return self._api_client.get(self.build_path( self.__DISPUTES_PATH, dispute_id, @@ -49,6 +57,14 @@ def get_compiled_submitted_evidence(self, dispute_id: str): self.__SUBMITTED_PATH), self._sdk_authorization()) + def get_compiled_submitted_arbitration_evidence(self, dispute_id: str): + return self._api_client.get(self.build_path( + self.__DISPUTES_PATH, dispute_id, + self.__EVIDENCE_PATH, + self.__ARBITRATION_PATH, + self.__SUBMITTED_PATH), + self._sdk_authorization()) + def get_dispute_scheme_files(self, dispute_id: str): return self._api_client.get(self.build_path(self.__DISPUTES_PATH, dispute_id, self.__SCHEME_FILES_PATH), self._sdk_authorization()) diff --git a/tests/disputes/disputes_client_test.py b/tests/disputes/disputes_client_test.py index a40a57d2..ba83f7ed 100644 --- a/tests/disputes/disputes_client_test.py +++ b/tests/disputes/disputes_client_test.py @@ -36,10 +36,18 @@ def test_should_submit_evidence(self, mocker, client: DisputesClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.submit_evidence('dispute_id') == 'response' + def test_should_submit_arbitration_evidence(self, mocker, client: DisputesClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.submit_arbitration_evidence('dispute_id') == 'response' + def test_should_get_compiled_submitted_evidence(self, mocker, client: DisputesClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_compiled_submitted_evidence('dispute_id') == 'response' + def test_should_get_compiled_submitted_arbitration_evidence(self, mocker, client: DisputesClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_compiled_submitted_arbitration_evidence('dispute_id') == 'response' + def test_should_upload_file(self, mocker, client: DisputesClient): mocker.patch('checkout_sdk.api_client.ApiClient.submit_file', return_value='response') assert client.upload_file(FileRequest()) == 'response' diff --git a/tests/disputes/disputes_integration_test.py b/tests/disputes/disputes_integration_test.py index 788d9c48..215d1af1 100644 --- a/tests/disputes/disputes_integration_test.py +++ b/tests/disputes/disputes_integration_test.py @@ -12,6 +12,9 @@ from tests.payments.previous.payments_previous_test_utils import make_card_payment +PLACEHOLDER_DISPUTE_ID = 'dsp_test_placeholder' + + def test_should_query_disputes(default_api): query = DisputesQueryFilter() now = datetime.now(timezone.utc) @@ -123,3 +126,29 @@ def test_should_disputes_scheme_files(default_api): assert_response(scheme_files, 'id', 'files') + + +@pytest.mark.skip(reason='use submit arbitration evidence on demand, requires a dispute with prior submitted evidence') +def test_should_submit_arbitration_evidence(default_api): + response = default_api.disputes.submit_arbitration_evidence(PLACEHOLDER_DISPUTE_ID) + assert_response(response, 'http_metadata') + + +@pytest.mark.skip(reason='use get compiled submitted evidence on demand, requires a dispute with submitted evidence') +def test_should_get_compiled_submitted_evidence(default_api): + response = default_api.disputes.get_compiled_submitted_evidence(PLACEHOLDER_DISPUTE_ID) + assert_compiled_submitted_evidence_response(response) + + +@pytest.mark.skip(reason='use get compiled submitted arbitration evidence on demand, ' + 'requires a dispute with submitted arbitration evidence') +def test_should_get_compiled_submitted_arbitration_evidence(default_api): + response = default_api.disputes.get_compiled_submitted_arbitration_evidence(PLACEHOLDER_DISPUTE_ID) + assert_compiled_submitted_evidence_response(response) + + +def assert_compiled_submitted_evidence_response(response): + assert_response(response, + 'file_id', + '_links', + '_links.self') From ca6a7ed9870a2e21efcccd82998787be47a11f6d Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 13 May 2026 09:52:53 +0200 Subject: [PATCH 5/8] Issuing client update + unit and integration tests --- checkout_sdk/issuing/cards.py | 44 ++++- checkout_sdk/issuing/controls.py | 61 +++++++ checkout_sdk/issuing/disputes.py | 25 +++ checkout_sdk/issuing/issuing_client.py | 158 +++++++++++++++++- checkout_sdk/issuing/testing.py | 16 ++ checkout_sdk/issuing/transactions.py | 21 +++ .../cardholders_issuing_integration_test.py | 30 +++- .../issuing/cards_issuing_integration_test.py | 73 +++++++- tests/issuing/conftest.py | 42 ++++- ...control_groups_issuing_integration_test.py | 55 ++++++ ...ntrol_profiles_issuing_integration_test.py | 81 +++++++++ .../disputes_issuing_integration_test.py | 90 ++++++++++ tests/issuing/issuing_client_test.py | 111 +++++++++++- .../testing_issuing_integration_test.py | 36 +++- .../transactions_issuing_integration_test.py | 40 +++++ 15 files changed, 871 insertions(+), 12 deletions(-) create mode 100644 checkout_sdk/issuing/disputes.py create mode 100644 checkout_sdk/issuing/transactions.py create mode 100644 tests/issuing/control_groups_issuing_integration_test.py create mode 100644 tests/issuing/control_profiles_issuing_integration_test.py create mode 100644 tests/issuing/disputes_issuing_integration_test.py create mode 100644 tests/issuing/transactions_issuing_integration_test.py diff --git a/checkout_sdk/issuing/cards.py b/checkout_sdk/issuing/cards.py index d905f97c..cb943157 100644 --- a/checkout_sdk/issuing/cards.py +++ b/checkout_sdk/issuing/cards.py @@ -30,11 +30,19 @@ class CardLifetime: class ShippingInstructions: - recipient_address: str + shipping_recipient: str shipping_address: Address additional_comment: str +class CardMetadata: + udf1: str + udf2: str + udf3: str + udf4: str + udf5: str + + class CardRequest: type: CardType cardholder_id: str @@ -43,6 +51,7 @@ class CardRequest: card_product_id: str display_name: str activate_card: bool + metadata: CardMetadata def __init__(self, type_p: CardType): self.type = type_p @@ -62,6 +71,39 @@ def __init__(self): super().__init__(CardType.VIRTUAL) +class UpdateCardRequest: + reference: str + metadata: CardMetadata + expiry_month: int + expiry_year: int + + +class RenewCardRequest: + type: CardType + display_name: str + reference: str + metadata: CardMetadata + + def __init__(self, type_p: CardType): + self.type = type_p + + +class PhysicalCardRenewRequest(RenewCardRequest): + shipping_instructions: ShippingInstructions + + def __init__(self): + super().__init__(CardType.PHYSICAL) + + +class VirtualCardRenewRequest(RenewCardRequest): + def __init__(self): + super().__init__(CardType.VIRTUAL) + + +class ScheduleCardRevocationRequest: + revocation_date: str + + class SecurityPair: question: str answer: str diff --git a/checkout_sdk/issuing/controls.py b/checkout_sdk/issuing/controls.py index 8be845cc..e2122704 100644 --- a/checkout_sdk/issuing/controls.py +++ b/checkout_sdk/issuing/controls.py @@ -4,6 +4,7 @@ class ControlType(str, Enum): VELOCITY_LIMIT = 'velocity_limit' MCC_LIMIT = 'mcc_limit' + MID_LIMIT = 'mid_limit' class VelocityWindowType(str, Enum): @@ -18,6 +19,16 @@ class MccLimitType(str, Enum): BLOCK = 'block' +class MidLimitType(str, Enum): + ALLOW = 'allow' + BLOCK = 'block' + + +class FailIfType(str, Enum): + ALL_FAIL = 'all_fail' + ANY_FAIL = 'any_fail' + + class VelocityWindow: type: VelocityWindowType @@ -26,6 +37,7 @@ class VelocityLimit: amount_limit: int velocity_window: VelocityWindow mcc_list: list # str + mid_list: list # str class MccLimit: @@ -33,6 +45,11 @@ class MccLimit: mcc_list: list # str +class MidLimit: + type: MidLimitType + mid_list: list # str + + class CardControlRequest: description: str control_type: ControlType @@ -64,3 +81,47 @@ class UpdateCardControlRequest: description: str velocity_limit: VelocityLimit mcc_limit: MccLimit + + +class ControlGroupControl: + control_type: ControlType + description: str + + def __init__(self, control_type: ControlType): + self.control_type = control_type + + +class MccControlGroupControl(ControlGroupControl): + mcc_limit: MccLimit + + def __init__(self): + super().__init__(ControlType.MCC_LIMIT) + + +class MidControlGroupControl(ControlGroupControl): + mid_limit: MidLimit + + def __init__(self): + super().__init__(ControlType.MID_LIMIT) + + +class VelocityControlGroupControl(ControlGroupControl): + velocity_limit: VelocityLimit + + def __init__(self): + super().__init__(ControlType.VELOCITY_LIMIT) + + +class CreateControlGroupRequest: + target_id: str + fail_if: FailIfType + controls: list # ControlGroupControl + description: str + + +class ControlGroupQueryTarget: + target_id: str + + +class ControlProfileRequest: + name: str diff --git a/checkout_sdk/issuing/disputes.py b/checkout_sdk/issuing/disputes.py new file mode 100644 index 00000000..76d73945 --- /dev/null +++ b/checkout_sdk/issuing/disputes.py @@ -0,0 +1,25 @@ +class DisputeEvidence: + name: str + content: str + description: str + + +class DisputeReasonChange: + reason: str + justification: str + + +class CreateDisputeRequest: + transaction_id: str + reason: str + evidence: list # DisputeEvidence + amount: int + presentment_message_id: str + justification: str + + +class EscalateDisputeRequest: + justification: str + additional_evidence: list # DisputeEvidence + amount: int + reason_change: DisputeReasonChange diff --git a/checkout_sdk/issuing/issuing_client.py b/checkout_sdk/issuing/issuing_client.py index 0148aeea..50149e9f 100644 --- a/checkout_sdk/issuing/issuing_client.py +++ b/checkout_sdk/issuing/issuing_client.py @@ -6,9 +6,14 @@ from checkout_sdk.client import Client from checkout_sdk.issuing.cardholders import CardholderRequest from checkout_sdk.issuing.cards import CardRequest, ThreeDsEnrollmentRequest, UpdateThreeDsEnrollmentRequest, \ - CardCredentialsQuery, RevokeRequest, SuspendRequest -from checkout_sdk.issuing.controls import CardControlRequest, CardControlsQuery, UpdateCardControlRequest -from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest + CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, RenewCardRequest, \ + ScheduleCardRevocationRequest +from checkout_sdk.issuing.controls import CardControlRequest, CardControlsQuery, UpdateCardControlRequest, \ + CreateControlGroupRequest, ControlGroupQueryTarget, ControlProfileRequest +from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest +from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest, \ + CardRefundAuthorizationRequest, SimulateOobAuthenticationRequest +from checkout_sdk.issuing.transactions import TransactionsQueryFilter class IssuingClient(Client): @@ -18,13 +23,27 @@ class IssuingClient(Client): __THREE_DS = '3ds-enrollment' __ACTIVATE = 'activate' __CREDENTIALS = 'credentials' + __RENEW = 'renew' __REVOKE = 'revoke' + __SCHEDULE_REVOCATION = 'schedule-revocation' __SUSPEND = 'suspend' __CONTROLS = 'controls' + __CONTROL_GROUPS = 'control-groups' + __CONTROL_PROFILES = 'control-profiles' + __ADD = 'add' + __REMOVE = 'remove' + __DIGITAL_CARDS = 'digital-cards' + __TRANSACTIONS = 'transactions' + __DISPUTES = 'disputes' + __CANCEL = 'cancel' + __ESCALATE = 'escalate' __SIMULATE = 'simulate' __AUTHORIZATIONS = 'authorizations' __PRESENTMENTS = 'presentments' __REVERSALS = 'reversals' + __REFUNDS = 'refunds' + __OOB = 'oob' + __AUTHENTICATION = 'authentication' def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): super().__init__(api_client=api_client, @@ -40,6 +59,11 @@ def get_cardholder(self, cardholder_id: str): return self._api_client.get(self.build_path(self.__ISSUING, self.__CARDHOLDERS, cardholder_id), self._sdk_authorization()) + def update_cardholder(self, cardholder_id: str, cardholder_request: CardholderRequest): + return self._api_client.patch(self.build_path(self.__ISSUING, self.__CARDHOLDERS, cardholder_id), + self._sdk_authorization(), + cardholder_request) + def get_cardholder_cards(self, cardholder_id: str): return self._api_client.get(self.build_path(self.__ISSUING, self.__CARDHOLDERS, cardholder_id, self.__CARDS), self._sdk_authorization()) @@ -53,6 +77,11 @@ def get_card_details(self, card_id: str): return self._api_client.get(self.build_path(self.__ISSUING, self.__CARDS, card_id), self._sdk_authorization()) + def update_card(self, card_id: str, update_card_request: UpdateCardRequest): + return self._api_client.patch(self.build_path(self.__ISSUING, self.__CARDS, card_id), + self._sdk_authorization(), + update_card_request) + def enroll_three_ds(self, card_id: str, three_ds_enrollment_request: ThreeDsEnrollmentRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__THREE_DS), self._sdk_authorization(), @@ -76,16 +105,45 @@ def get_card_credentials(self, card_id: str, card_credentials_query: CardCredent self._sdk_authorization(), card_credentials_query) + def renew_card(self, card_id: str, renew_card_request: RenewCardRequest): + return self._api_client.post(self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__RENEW), + self._sdk_authorization(), + renew_card_request) + def revoke_card(self, card_id: str, revoke_request: RevokeRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__REVOKE), self._sdk_authorization(), revoke_request) + def schedule_card_revocation(self, card_id: str, schedule_request: ScheduleCardRevocationRequest): + return self._api_client.post( + self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SCHEDULE_REVOCATION), + self._sdk_authorization(), + schedule_request) + + def delete_card_revocation(self, card_id: str): + return self._api_client.delete( + self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SCHEDULE_REVOCATION), + self._sdk_authorization()) + def suspend_card(self, card_id: str, suspend_request: SuspendRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SUSPEND), self._sdk_authorization(), suspend_request) + def get_digital_card(self, digital_card_id: str): + return self._api_client.get(self.build_path(self.__ISSUING, self.__DIGITAL_CARDS, digital_card_id), + self._sdk_authorization()) + + def get_list_transactions(self, query: TransactionsQueryFilter): + return self._api_client.get(self.build_path(self.__ISSUING, self.__TRANSACTIONS), + self._sdk_authorization(), + query) + + def get_single_transaction(self, transaction_id: str): + return self._api_client.get(self.build_path(self.__ISSUING, self.__TRANSACTIONS, transaction_id), + self._sdk_authorization()) + def create_control(self, control_request: CardControlRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__CONTROLS), self._sdk_authorization(), @@ -109,6 +167,87 @@ def remove_control(self, control_id: str): return self._api_client.delete(self.build_path(self.__ISSUING, self.__CONTROLS, control_id), self._sdk_authorization()) + def create_control_group(self, create_control_group_request: CreateControlGroupRequest): + return self._api_client.post(self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_GROUPS), + self._sdk_authorization(), + create_control_group_request) + + def get_target_control_groups(self, query: ControlGroupQueryTarget): + return self._api_client.get(self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_GROUPS), + self._sdk_authorization(), + query) + + def get_control_group_details(self, control_group_id: str): + return self._api_client.get( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_GROUPS, control_group_id), + self._sdk_authorization()) + + def delete_control_group(self, control_group_id: str): + return self._api_client.delete( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_GROUPS, control_group_id), + self._sdk_authorization()) + + def create_control_profile(self, control_profile_request: ControlProfileRequest): + return self._api_client.post(self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES), + self._sdk_authorization(), + control_profile_request) + + def get_all_control_profiles(self, query: ControlGroupQueryTarget): + return self._api_client.get(self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES), + self._sdk_authorization(), + query) + + def get_control_profile_details(self, control_profile_id: str): + return self._api_client.get( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES, control_profile_id), + self._sdk_authorization()) + + def update_control_profile(self, control_profile_id: str, control_profile_request: ControlProfileRequest): + return self._api_client.patch( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES, control_profile_id), + self._sdk_authorization(), + control_profile_request) + + def delete_control_profile(self, control_profile_id: str): + return self._api_client.delete( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES, control_profile_id), + self._sdk_authorization()) + + def add_target_to_control_profile(self, control_profile_id: str, target_id: str): + return self._api_client.post( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES, control_profile_id, + self.__ADD, target_id), + self._sdk_authorization()) + + def remove_target_from_control_profile(self, control_profile_id: str, target_id: str): + return self._api_client.post( + self.build_path(self.__ISSUING, self.__CONTROLS, self.__CONTROL_PROFILES, control_profile_id, + self.__REMOVE, target_id), + self._sdk_authorization()) + + def create_dispute(self, create_dispute_request: CreateDisputeRequest, idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES), + self._sdk_authorization(), + create_dispute_request, + idempotency_key) + + def get_dispute_details(self, dispute_id: str): + return self._api_client.get(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id), + self._sdk_authorization()) + + def cancel_dispute(self, dispute_id: str, idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id, self.__CANCEL), + self._sdk_authorization(), + None, + idempotency_key) + + def escalate_dispute(self, dispute_id: str, escalate_dispute_request: EscalateDisputeRequest, + idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id, self.__ESCALATE), + self._sdk_authorization(), + escalate_dispute_request, + idempotency_key) + def simulate_authorization(self, authorization_request: CardAuthorizationRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__SIMULATE, self.__AUTHORIZATIONS), self._sdk_authorization(), @@ -134,3 +273,16 @@ def simulate_reversal(self, transaction_id: str, reversal_request: SimulationReq self.__ISSUING, self.__SIMULATE, self.__AUTHORIZATIONS, transaction_id, self.__REVERSALS), self._sdk_authorization(), reversal_request) + + def simulate_refund(self, transaction_id: str, refund_request: CardRefundAuthorizationRequest): + return self._api_client.post( + self.build_path( + self.__ISSUING, self.__SIMULATE, self.__AUTHORIZATIONS, transaction_id, self.__REFUNDS), + self._sdk_authorization(), + refund_request) + + def simulate_oob_authentication(self, simulate_oob_request: SimulateOobAuthenticationRequest): + return self._api_client.post( + self.build_path(self.__ISSUING, self.__SIMULATE, self.__OOB, self.__AUTHENTICATION), + self._sdk_authorization(), + simulate_oob_request) diff --git a/checkout_sdk/issuing/testing.py b/checkout_sdk/issuing/testing.py index 6142d148..68f7b6ff 100644 --- a/checkout_sdk/issuing/testing.py +++ b/checkout_sdk/issuing/testing.py @@ -37,3 +37,19 @@ class CardAuthorizationRequest: class SimulationRequest: amount: int + + +class CardRefundAuthorizationRequest: + amount: int + + +class OobSimulateTransactionDetails: + last_four: str + merchant_name: str + purchase_amount: int + purchase_currency: Currency + + +class SimulateOobAuthenticationRequest: + card_id: str + transaction_details: OobSimulateTransactionDetails diff --git a/checkout_sdk/issuing/transactions.py b/checkout_sdk/issuing/transactions.py new file mode 100644 index 00000000..38ed8887 --- /dev/null +++ b/checkout_sdk/issuing/transactions.py @@ -0,0 +1,21 @@ +from enum import Enum + +from checkout_sdk.common.common import QueryFilterDateRange + + +class TransactionStatus(str, Enum): + AUTHORIZED = 'authorized' + DECLINED = 'declined' + CANCELED = 'canceled' + CLEARED = 'cleared' + REFUNDED = 'refunded' + DISPUTED = 'disputed' + + +class TransactionsQueryFilter(QueryFilterDateRange): + limit: int + skip: int + cardholder_id: str + card_id: str + entity_id: str + status: TransactionStatus diff --git a/tests/issuing/cardholders_issuing_integration_test.py b/tests/issuing/cardholders_issuing_integration_test.py index da632b2f..1ce18796 100644 --- a/tests/issuing/cardholders_issuing_integration_test.py +++ b/tests/issuing/cardholders_issuing_integration_test.py @@ -1,9 +1,11 @@ from __future__ import absolute_import -from checkout_sdk.issuing.cardholders import CardholderType -from tests.checkout_test_utils import assert_response +from checkout_sdk.issuing.cardholders import CardholderType, CardholderRequest +from tests.checkout_test_utils import assert_response, phone, address +# tests + def test_should_create_cardholder(issuing_checkout_api, cardholder): assert_response(cardholder, 'id', @@ -30,6 +32,15 @@ def test_should_get_cardholder(issuing_checkout_api, cardholder): assert 'X-123456-N11' == cardholder_response.reference +def test_should_update_cardholder(issuing_checkout_api, cardholder): + request = build_update_cardholder_request() + + response = issuing_checkout_api.issuing.update_cardholder(cardholder.id, request) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_get_cardholder_cards(issuing_checkout_api, cardholder): cardholder_response = issuing_checkout_api.issuing.get_cardholder_cards(cardholder.id) @@ -37,3 +48,18 @@ def test_should_get_cardholder_cards(issuing_checkout_api, cardholder): for card in cardholder_response.cards: assert cardholder.id == card.cardholder_id + + +# common methods + +def build_update_cardholder_request() -> CardholderRequest: + request = CardholderRequest() + request.first_name = 'John' + request.middle_name = 'Fitzgerald' + request.last_name = 'Kennedy' + request.email = 'john.kennedy@myemaildomain.com' + request.phone_number = phone() + request.date_of_birth = '1985-05-15' + request.billing_address = address() + request.residency_address = address() + return request diff --git a/tests/issuing/cards_issuing_integration_test.py b/tests/issuing/cards_issuing_integration_test.py index e0539627..c80df2f5 100644 --- a/tests/issuing/cards_issuing_integration_test.py +++ b/tests/issuing/cards_issuing_integration_test.py @@ -1,13 +1,18 @@ +from datetime import datetime, timedelta + import pytest from checkout_sdk.issuing.cards import PasswordEnrollmentRequest, SecurityPair, UpdateThreeDsEnrollmentRequest, \ - CardCredentialsQuery, RevokeRequest, RevokeReason, SuspendRequest, SuspendReason + CardCredentialsQuery, RevokeRequest, RevokeReason, SuspendRequest, SuspendReason, UpdateCardRequest, CardMetadata, \ + VirtualCardRenewRequest, ScheduleCardRevocationRequest from tests.checkout_test_utils import assert_response, phone @pytest.mark.skip("Avoid creating cards all the time") @pytest.mark.usefixtures("card") class TestCardsIssuing: + # tests + def test_should_create_card(self, card): assert_response(card, 'id', @@ -44,6 +49,54 @@ def test_should_get_card_details(self, issuing_checkout_api, cardholder, card): assert response.card_product_id == 'pro_3fn6pv2ikshurn36dbd3iysyha' assert response.reference == 'X-123456-N11' + def test_should_update_card(self, issuing_checkout_api, card): + request = build_update_card_request() + + response = issuing_checkout_api.issuing.update_card(card.id, request) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_renew_card(self, issuing_checkout_api, card): + request = VirtualCardRenewRequest() + request.reference = 'RENEW-REF-123' + + response = issuing_checkout_api.issuing.renew_card(card.id, request) + + assert_response(response, 'id') + assert response.http_metadata.status_code == 201 + + def test_should_schedule_card_revocation(self, issuing_checkout_api, active_card): + request = ScheduleCardRevocationRequest() + request.revocation_date = (datetime.utcnow() + timedelta(days=7)).strftime('%Y-%m-%d') + + response = issuing_checkout_api.issuing.schedule_card_revocation(active_card.id, request) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_delete_card_revocation(self, issuing_checkout_api, active_card): + request = ScheduleCardRevocationRequest() + request.revocation_date = (datetime.utcnow() + timedelta(days=7)).strftime('%Y-%m-%d') + issuing_checkout_api.issuing.schedule_card_revocation(active_card.id, request) + + response = issuing_checkout_api.issuing.delete_card_revocation(active_card.id) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_get_digital_card(self, issuing_checkout_api): + digital_card_id = 'dcr_5ngxzsynm2me3oxf73esbhda6q' + + response = issuing_checkout_api.issuing.get_digital_card(digital_card_id) + + assert_response(response, + 'id', + 'card_id', + 'last_four', + 'status') + assert response.id == digital_card_id + def test_should_enroll_into_three_ds(self, issuing_checkout_api, card): request = PasswordEnrollmentRequest() request.password = self.__get_pass() @@ -143,3 +196,21 @@ def test_should_suspend_card(self, issuing_checkout_api, active_card): def __get_pass(self): return 'Xtui43FvfiZ' + + +# common methods + +def build_update_card_request() -> UpdateCardRequest: + metadata = CardMetadata() + metadata.udf1 = 'UDF1' + metadata.udf2 = 'UDF2' + metadata.udf3 = 'UDF3' + metadata.udf4 = 'UDF4' + metadata.udf5 = 'UDF5' + + request = UpdateCardRequest() + request.reference = 'UPDATED-REF-123' + request.metadata = metadata + request.expiry_month = 12 + request.expiry_year = 2030 + return request diff --git a/tests/issuing/conftest.py b/tests/issuing/conftest.py index d349db92..d0b4c85a 100644 --- a/tests/issuing/conftest.py +++ b/tests/issuing/conftest.py @@ -5,7 +5,8 @@ from checkout_sdk.common.enums import DocumentType, Currency from checkout_sdk.issuing.cardholders import CardholderRequest, CardholderDocument, CardholderType from checkout_sdk.issuing.cards import CardLifetime, LifetimeUnit, VirtualCardRequest -from checkout_sdk.issuing.controls import VelocityControlRequest, VelocityLimit, VelocityWindow, VelocityWindowType +from checkout_sdk.issuing.controls import VelocityControlRequest, VelocityLimit, VelocityWindow, VelocityWindowType, \ + CreateControlGroupRequest, FailIfType, MccControlGroupControl, MccLimit, MccLimitType, ControlProfileRequest from checkout_sdk.issuing.testing import CardSimulation, Merchant, TransactionSimulation, TransactionType, \ AuthorizationType, CardAuthorizationRequest from checkout_sdk.oauth_scopes import OAuthScopes @@ -121,6 +122,27 @@ def transaction(issuing_checkout_api, active_card): return transaction +@pytest.fixture(scope='class') +def control_group(issuing_checkout_api, card): + request = get_control_group_request(card.id) + + control_group = issuing_checkout_api.issuing.create_control_group(request) + + assert_response(control_group, 'id') + return control_group + + +@pytest.fixture(scope='class') +def control_profile(issuing_checkout_api): + request = ControlProfileRequest() + request.name = 'Test Control Profile' + + control_profile = issuing_checkout_api.issuing.create_control_profile(request) + + assert_response(control_profile, 'id') + return control_profile + + def get_card_request(cardholder): lifetime = CardLifetime() lifetime.unit = LifetimeUnit.MONTHS @@ -135,3 +157,21 @@ def get_card_request(cardholder): request.is_single_use = False return request + + +def get_control_group_request(target_id: str): + mcc_limit = MccLimit() + mcc_limit.type = MccLimitType.BLOCK + mcc_limit.mcc_list = ['5411', '5422'] + + mcc_control = MccControlGroupControl() + mcc_control.description = 'Block grocery stores' + mcc_control.mcc_limit = mcc_limit + + request = CreateControlGroupRequest() + request.target_id = target_id + request.fail_if = FailIfType.ALL_FAIL + request.description = 'Integration test control group' + request.controls = [mcc_control] + + return request diff --git a/tests/issuing/control_groups_issuing_integration_test.py b/tests/issuing/control_groups_issuing_integration_test.py new file mode 100644 index 00000000..9322c5bc --- /dev/null +++ b/tests/issuing/control_groups_issuing_integration_test.py @@ -0,0 +1,55 @@ +import pytest + +from checkout_sdk.issuing.controls import ControlGroupQueryTarget, FailIfType +from tests.checkout_test_utils import assert_response +from tests.issuing.conftest import get_control_group_request + + +@pytest.mark.skip("Avoid creating cards all the time") +@pytest.mark.usefixtures("card", "control_group") +class TestControlGroupsIssuing: + # tests + + def test_should_create_control_group(self, card, control_group): + assert_response(control_group, + 'id', + 'target_id', + 'fail_if', + 'description', + 'controls') + + assert control_group.id.startswith('cgr_') + assert control_group.target_id == card.id + assert control_group.fail_if == FailIfType.ALL_FAIL + assert control_group.description == 'Integration test control group' + + def test_should_get_target_control_groups(self, issuing_checkout_api, card, control_group): + query = ControlGroupQueryTarget() + query.target_id = card.id + + response = issuing_checkout_api.issuing.get_target_control_groups(query) + + assert_response(response, 'control_groups') + assert any(cg.id == control_group.id for cg in response.control_groups) + + def test_should_get_control_group_details(self, issuing_checkout_api, control_group): + response = issuing_checkout_api.issuing.get_control_group_details(control_group.id) + + assert_response(response, + 'id', + 'target_id', + 'fail_if', + 'description') + + assert response.id == control_group.id + assert response.target_id == control_group.target_id + assert response.fail_if == control_group.fail_if + + def test_should_delete_control_group(self, issuing_checkout_api, card): + request = get_control_group_request(card.id) + created = issuing_checkout_api.issuing.create_control_group(request) + + response = issuing_checkout_api.issuing.delete_control_group(created.id) + + assert_response(response) + assert response.http_metadata.status_code == 200 diff --git a/tests/issuing/control_profiles_issuing_integration_test.py b/tests/issuing/control_profiles_issuing_integration_test.py new file mode 100644 index 00000000..0e4d7815 --- /dev/null +++ b/tests/issuing/control_profiles_issuing_integration_test.py @@ -0,0 +1,81 @@ +import pytest + +from checkout_sdk.issuing.controls import ControlGroupQueryTarget, ControlProfileRequest +from tests.checkout_test_utils import assert_response + + +@pytest.mark.skip("Avoid creating cards all the time") +@pytest.mark.usefixtures("control_profile") +class TestControlProfilesIssuing: + # tests + + def test_should_create_control_profile(self, control_profile): + assert_response(control_profile, + 'id', + 'name') + + assert control_profile.name == 'Test Control Profile' + + def test_should_get_all_control_profiles(self, issuing_checkout_api, control_profile): + query = ControlGroupQueryTarget() + query.target_id = control_profile.id + + response = issuing_checkout_api.issuing.get_all_control_profiles(query) + + assert_response(response, 'control_profiles') + assert any(cp.id == control_profile.id for cp in response.control_profiles) + + def test_should_get_control_profile_details(self, issuing_checkout_api, control_profile): + response = issuing_checkout_api.issuing.get_control_profile_details(control_profile.id) + + assert_response(response, + 'id', + 'name') + + assert response.id == control_profile.id + assert response.name == control_profile.name + + def test_should_update_control_profile(self, issuing_checkout_api, control_profile): + request = build_update_control_profile_request() + + response = issuing_checkout_api.issuing.update_control_profile(control_profile.id, request) + + assert_response(response, 'id', 'name') + assert response.id == control_profile.id + assert response.name == 'Updated Control Profile' + + def test_should_add_target_to_control_profile(self, issuing_checkout_api, active_card, control_profile): + response = issuing_checkout_api.issuing.add_target_to_control_profile(control_profile.id, active_card.id) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_remove_target_from_control_profile(self, issuing_checkout_api, active_card, control_profile): + issuing_checkout_api.issuing.add_target_to_control_profile(control_profile.id, active_card.id) + + response = issuing_checkout_api.issuing.remove_target_from_control_profile(control_profile.id, active_card.id) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_delete_control_profile(self, issuing_checkout_api): + created = issuing_checkout_api.issuing.create_control_profile(build_create_control_profile_request()) + + response = issuing_checkout_api.issuing.delete_control_profile(created.id) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + +# common methods + +def build_create_control_profile_request() -> ControlProfileRequest: + request = ControlProfileRequest() + request.name = 'Test Control Profile' + return request + + +def build_update_control_profile_request() -> ControlProfileRequest: + request = ControlProfileRequest() + request.name = 'Updated Control Profile' + return request diff --git a/tests/issuing/disputes_issuing_integration_test.py b/tests/issuing/disputes_issuing_integration_test.py new file mode 100644 index 00000000..1b741496 --- /dev/null +++ b/tests/issuing/disputes_issuing_integration_test.py @@ -0,0 +1,90 @@ +import uuid + +import pytest + +from checkout_sdk.issuing.disputes import CreateDisputeRequest, DisputeEvidence, EscalateDisputeRequest +from tests.checkout_test_utils import assert_response + + +@pytest.mark.skip("Requires permissions to create disputes and simulate transactions") +class TestDisputesIssuing: + # tests + + def test_should_create_dispute(self, issuing_checkout_api, transaction): + request = build_create_dispute_request(transaction.id) + idempotency_key = str(uuid.uuid4()) + + response = issuing_checkout_api.issuing.create_dispute(request, idempotency_key) + + assert_response(response, + 'id', + 'transaction_id', + 'reason', + 'status') + assert response.id.startswith('idsp_') + assert response.transaction_id == request.transaction_id + assert response.reason == request.reason + + def test_should_get_dispute_details(self, issuing_checkout_api, transaction): + request = build_create_dispute_request(transaction.id) + created = issuing_checkout_api.issuing.create_dispute(request, str(uuid.uuid4())) + + response = issuing_checkout_api.issuing.get_dispute_details(created.id) + + assert_response(response, + 'id', + 'transaction_id', + 'reason') + assert response.id == created.id + assert response.transaction_id == created.transaction_id + + def test_should_cancel_dispute(self, issuing_checkout_api, transaction): + request = build_create_dispute_request(transaction.id) + idempotency_key = str(uuid.uuid4()) + created = issuing_checkout_api.issuing.create_dispute(request, idempotency_key) + + response = issuing_checkout_api.issuing.cancel_dispute(created.id, idempotency_key) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_escalate_dispute(self, issuing_checkout_api, transaction): + request = build_create_dispute_request(transaction.id) + idempotency_key = str(uuid.uuid4()) + created = issuing_checkout_api.issuing.create_dispute(request, idempotency_key) + + response = issuing_checkout_api.issuing.escalate_dispute( + created.id, build_escalate_dispute_request(), idempotency_key) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + +# common methods + +def build_create_dispute_request(transaction_id: str) -> CreateDisputeRequest: + evidence = DisputeEvidence() + evidence.name = 'receipt.pdf' + evidence.content = 'SGVsbG8gV29ybGQ=' + evidence.description = 'Transaction receipt showing unauthorized charge' + + request = CreateDisputeRequest() + request.transaction_id = transaction_id + request.reason = '4837' + request.evidence = [evidence] + request.amount = 1000 + request.justification = 'Customer reports unauthorized transaction' + return request + + +def build_escalate_dispute_request() -> EscalateDisputeRequest: + evidence = DisputeEvidence() + evidence.name = 'location_evidence.pdf' + evidence.content = 'TG9jYXRpb24gRXZpZGVuY2U=' + evidence.description = 'GPS data showing customer location during transaction' + + request = EscalateDisputeRequest() + request.justification = 'Merchant response was insufficient. Escalating to pre-arbitration.' + request.additional_evidence = [evidence] + request.amount = 800 + return request diff --git a/tests/issuing/issuing_client_test.py b/tests/issuing/issuing_client_test.py index 59303bc3..b110e908 100644 --- a/tests/issuing/issuing_client_test.py +++ b/tests/issuing/issuing_client_test.py @@ -2,10 +2,15 @@ from checkout_sdk.issuing.cardholders import CardholderRequest from checkout_sdk.issuing.cards import PhysicalCardRequest, PasswordEnrollmentRequest, UpdateThreeDsEnrollmentRequest, \ - CardCredentialsQuery, RevokeRequest, SuspendRequest -from checkout_sdk.issuing.controls import MccControlRequest, CardControlsQuery, UpdateCardControlRequest + CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, VirtualCardRenewRequest, \ + ScheduleCardRevocationRequest +from checkout_sdk.issuing.controls import MccControlRequest, CardControlsQuery, UpdateCardControlRequest, \ + CreateControlGroupRequest, ControlGroupQueryTarget, ControlProfileRequest +from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest from checkout_sdk.issuing.issuing_client import IssuingClient -from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest +from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest, \ + CardRefundAuthorizationRequest, SimulateOobAuthenticationRequest +from checkout_sdk.issuing.transactions import TransactionsQueryFilter @pytest.fixture(scope='class') @@ -23,6 +28,10 @@ def test_should_get_cardholder(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_cardholder('cardholder_id') == 'response' + def test_should_update_cardholder(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.update_cardholder('cardholder_id', CardholderRequest()) == 'response' + def test_should_get_cardholder_cards(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_cardholder_cards('cardholder_id') == 'response' @@ -35,6 +44,10 @@ def test_should_get_card_details(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_card_details('card_id') == 'response' + def test_should_update_card(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.update_card('card_id', UpdateCardRequest()) == 'response' + def test_should_enroll_three_ds(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.enroll_three_ds('card_id', PasswordEnrollmentRequest()) == 'response' @@ -55,14 +68,38 @@ def test_should_get_card_credentials(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get_card_credentials('card_id', CardCredentialsQuery()) == 'response' + def test_should_renew_card(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.renew_card('card_id', VirtualCardRenewRequest()) == 'response' + def test_should_revoke_card(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.revoke_card('card_id', RevokeRequest()) == 'response' + def test_should_schedule_card_revocation(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.schedule_card_revocation('card_id', ScheduleCardRevocationRequest()) == 'response' + + def test_should_delete_card_revocation(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') + assert client.delete_card_revocation('card_id') == 'response' + def test_should_suspend_card(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.suspend_card('card_id', SuspendRequest()) == 'response' + def test_should_get_digital_card(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_digital_card('digital_card_id') == 'response' + + def test_should_get_list_transactions(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_list_transactions(TransactionsQueryFilter()) == 'response' + + def test_should_get_single_transaction(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_single_transaction('transaction_id') == 'response' + def test_should_create_control(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.create_control(MccControlRequest()) == 'response' @@ -83,6 +120,66 @@ def test_should_remove_control(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') assert client.remove_control('control_id') == 'response' + def test_should_create_control_group(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_control_group(CreateControlGroupRequest()) == 'response' + + def test_should_get_target_control_groups(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_target_control_groups(ControlGroupQueryTarget()) == 'response' + + def test_should_get_control_group_details(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_control_group_details('control_group_id') == 'response' + + def test_should_delete_control_group(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') + assert client.delete_control_group('control_group_id') == 'response' + + def test_should_create_control_profile(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_control_profile(ControlProfileRequest()) == 'response' + + def test_should_get_all_control_profiles(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_all_control_profiles(ControlGroupQueryTarget()) == 'response' + + def test_should_get_control_profile_details(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_control_profile_details('control_profile_id') == 'response' + + def test_should_update_control_profile(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.update_control_profile('control_profile_id', ControlProfileRequest()) == 'response' + + def test_should_delete_control_profile(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') + assert client.delete_control_profile('control_profile_id') == 'response' + + def test_should_add_target_to_control_profile(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.add_target_to_control_profile('control_profile_id', 'target_id') == 'response' + + def test_should_remove_target_from_control_profile(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.remove_target_from_control_profile('control_profile_id', 'target_id') == 'response' + + def test_should_create_dispute(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_dispute(CreateDisputeRequest()) == 'response' + + def test_should_get_dispute_details(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_dispute_details('dispute_id') == 'response' + + def test_should_cancel_dispute(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.cancel_dispute('dispute_id') == 'response' + + def test_should_escalate_dispute(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.escalate_dispute('dispute_id', EscalateDisputeRequest()) == 'response' + def test_should_simulate_authorization(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.simulate_authorization(CardAuthorizationRequest()) == 'response' @@ -98,3 +195,11 @@ def test_should_simulate_clearing(self, mocker, client: IssuingClient): def test_should_simulate_reversal(self, mocker, client: IssuingClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') assert client.simulate_reversal('transaction_id', SimulationRequest()) == 'response' + + def test_should_simulate_refund(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.simulate_refund('transaction_id', CardRefundAuthorizationRequest()) == 'response' + + def test_should_simulate_oob_authentication(self, mocker, client: IssuingClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.simulate_oob_authentication(SimulateOobAuthenticationRequest()) == 'response' diff --git a/tests/issuing/testing_issuing_integration_test.py b/tests/issuing/testing_issuing_integration_test.py index 692117a7..ca8e1988 100644 --- a/tests/issuing/testing_issuing_integration_test.py +++ b/tests/issuing/testing_issuing_integration_test.py @@ -2,12 +2,15 @@ from checkout_sdk.common.enums import Currency from checkout_sdk.issuing.testing import CardSimulation, TransactionSimulation, TransactionType, AuthorizationType, \ - CardAuthorizationRequest, Merchant, SimulationRequest + CardAuthorizationRequest, Merchant, SimulationRequest, CardRefundAuthorizationRequest, \ + SimulateOobAuthenticationRequest, OobSimulateTransactionDetails from tests.checkout_test_utils import assert_response @pytest.mark.skip("Avoid creating cards all the time") class TestTestingIssuing: + # tests + def test_should_simulate_authorization(self, issuing_checkout_api, active_card): card_simulation = CardSimulation() card_simulation.id = active_card.id @@ -65,3 +68,34 @@ def test_should_simulate_reversal(self, issuing_checkout_api, transaction): 'status') assert response.status == 'Reversed' + + def test_should_simulate_refund(self, issuing_checkout_api, transaction): + request = CardRefundAuthorizationRequest() + request.amount = 100 + + response = issuing_checkout_api.issuing.simulate_refund(transaction.id, request) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + def test_should_simulate_oob_authentication(self, issuing_checkout_api, active_card): + request = build_oob_authentication_request(active_card.id) + + response = issuing_checkout_api.issuing.simulate_oob_authentication(request) + + assert_response(response) + assert response.http_metadata.status_code == 200 + + +# common methods + +def build_oob_authentication_request(card_id: str) -> SimulateOobAuthenticationRequest: + details = OobSimulateTransactionDetails() + details.merchant_name = 'Acme Ltd' + details.purchase_amount = 100 + details.purchase_currency = Currency.GBP + + request = SimulateOobAuthenticationRequest() + request.card_id = card_id + request.transaction_details = details + return request diff --git a/tests/issuing/transactions_issuing_integration_test.py b/tests/issuing/transactions_issuing_integration_test.py new file mode 100644 index 00000000..eeafda50 --- /dev/null +++ b/tests/issuing/transactions_issuing_integration_test.py @@ -0,0 +1,40 @@ +import pytest + +from checkout_sdk.issuing.transactions import TransactionsQueryFilter +from tests.checkout_test_utils import assert_response + + +@pytest.mark.skip("Avoid creating cards all the time") +class TestTransactionsIssuing: + # tests + + def test_should_get_list_transactions(self, issuing_checkout_api): + query = TransactionsQueryFilter() + + response = issuing_checkout_api.issuing.get_list_transactions(query) + + assert_response(response, + 'limit', + 'skip', + 'total_count', + 'data') + assert len(response.data) > 0 + + def test_should_get_single_transaction(self, issuing_checkout_api): + query = TransactionsQueryFilter() + list_response = issuing_checkout_api.issuing.get_list_transactions(query) + + response = issuing_checkout_api.issuing.get_single_transaction(list_response.data[0].id) + + assert_response(response, + 'id', + 'created_on', + 'status', + 'transaction_type', + 'client', + 'entity', + 'card', + 'cardholder', + 'amounts', + 'merchant', + 'messages') From 59cd3a8f5e1b32d59141603c8c36604ad76424ba Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 13 May 2026 10:16:23 +0200 Subject: [PATCH 6/8] Pylint flake8 warnings fixed --- checkout_sdk/issuing/issuing_client.py | 2 +- checkout_sdk/networktokens/network_tokens_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/checkout_sdk/issuing/issuing_client.py b/checkout_sdk/issuing/issuing_client.py index 50149e9f..af506053 100644 --- a/checkout_sdk/issuing/issuing_client.py +++ b/checkout_sdk/issuing/issuing_client.py @@ -242,7 +242,7 @@ def cancel_dispute(self, dispute_id: str, idempotency_key: str = None): idempotency_key) def escalate_dispute(self, dispute_id: str, escalate_dispute_request: EscalateDisputeRequest, - idempotency_key: str = None): + idempotency_key: str = None): return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id, self.__ESCALATE), self._sdk_authorization(), escalate_dispute_request, diff --git a/checkout_sdk/networktokens/network_tokens_client.py b/checkout_sdk/networktokens/network_tokens_client.py index 418aec79..d4537641 100644 --- a/checkout_sdk/networktokens/network_tokens_client.py +++ b/checkout_sdk/networktokens/network_tokens_client.py @@ -28,7 +28,7 @@ def get_network_token(self, network_token_id: str): self._sdk_authorization()) def request_cryptogram(self, network_token_id: str, - request_cryptogram_request: RequestCryptogramRequest): + request_cryptogram_request: RequestCryptogramRequest): return self._api_client.post( self.build_path(self.__NETWORK_TOKENS_PATH, network_token_id, self.__CRYPTOGRAMS_PATH), self._sdk_authorization(), From 0a0dde28012bb9576433f5f4e9e36ecc47798ff8 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 14 May 2026 17:07:30 +0200 Subject: [PATCH 7/8] Fileds and classes adjustment with last specifications --- checkout_sdk/accounts/accounts.py | 2 +- checkout_sdk/balances/balances.py | 16 ++ checkout_sdk/common/common.py | 3 + checkout_sdk/common/enums.py | 9 + checkout_sdk/disputes/disputes.py | 29 +++ checkout_sdk/forex/forex.py | 2 +- checkout_sdk/forex/forex_client.py | 5 + checkout_sdk/forward/forward.py | 7 + checkout_sdk/instruments/instruments.py | 22 ++ checkout_sdk/issuing/cards.py | 1 + checkout_sdk/metadata/metadata.py | 1 + checkout_sdk/payments/contexts/contexts.py | 5 + .../payments/hosted/hosted_payments.py | 7 +- checkout_sdk/payments/links/payments_links.py | 7 +- checkout_sdk/payments/payment_apm.py | 55 +++++ checkout_sdk/payments/payments.py | 202 +++++++++++++++++- .../payments/payments_apm_previous.py | 3 + checkout_sdk/payments/sessions/sessions.py | 17 ++ checkout_sdk/payments/setups/setups.py | 9 + checkout_sdk/sessions/sessions.py | 37 +++- checkout_sdk/tokens/tokens.py | 1 + 21 files changed, 421 insertions(+), 19 deletions(-) diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 503bbd94..3b9817b0 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -331,7 +331,7 @@ class AccountsPaymentInstrument: class PaymentInstrumentRequest: label: str - type = InstrumentType + type: InstrumentType currency: Currency country: Country default: bool diff --git a/checkout_sdk/balances/balances.py b/checkout_sdk/balances/balances.py index 495f337f..9c1c97b1 100644 --- a/checkout_sdk/balances/balances.py +++ b/checkout_sdk/balances/balances.py @@ -1,2 +1,18 @@ class BalancesQuery: query: str + with_currency_account_id: str + balances_at: str + + # The /balances/{id} endpoint uses camelCase query params, inconsistent with + # the rest of the API. Handle the mapping locally so the quirk stays scoped + # to this class — if the API ever normalizes to snake_case, deleting this + # method is the entire fix. + def to_json(self): + out = {} + if getattr(self, 'query', None) is not None: + out['query'] = self.query + if getattr(self, 'with_currency_account_id', None) is not None: + out['withCurrencyAccountId'] = self.with_currency_account_id + if getattr(self, 'balances_at', None) is not None: + out['balancesAt'] = self.balances_at + return out diff --git a/checkout_sdk/common/common.py b/checkout_sdk/common/common.py index 63f998bd..4cef37d4 100644 --- a/checkout_sdk/common/common.py +++ b/checkout_sdk/common/common.py @@ -23,6 +23,9 @@ class CustomerRequest: email: str name: str phone: Phone + default: str + metadata: dict + tax_number: str class CustomerRetry: diff --git a/checkout_sdk/common/enums.py b/checkout_sdk/common/enums.py index fb751399..d060c2eb 100644 --- a/checkout_sdk/common/enums.py +++ b/checkout_sdk/common/enums.py @@ -476,8 +476,16 @@ class PaymentSourceType(str, Enum): OCTOPUS = 'octopus' PLAID = 'plaid' SEQURA = 'sequra' + MOBILEPAY = 'mobilepay' + PAYNOW = 'paynow' + SWISH = 'swish' + TWINT = 'twint' + VIPPS = 'vipps' + BLIK = 'blik' +# Used by ThreeDsRequest (in payments). The /sessions endpoint accepts +# additional exemption-like values — see SessionChallengeIndicator. class ChallengeIndicator(str, Enum): NO_PREFERENCE = 'no_preference' NO_CHALLENGE_REQUESTED = 'no_challenge_requested' @@ -490,6 +498,7 @@ class InstrumentType(str, Enum): TOKEN = 'token' CARD = 'card' SEPA = 'sepa' + ACH = 'ach' CARD_TOKEN = 'card_token' diff --git a/checkout_sdk/disputes/disputes.py b/checkout_sdk/disputes/disputes.py index e825db0c..33539ed3 100644 --- a/checkout_sdk/disputes/disputes.py +++ b/checkout_sdk/disputes/disputes.py @@ -15,6 +15,30 @@ class DisputesQueryFilter: entity_ids: str sub_entity_ids: str payment_mcc: str + processing_channel_ids: str + segment_ids: str + + +class CompellingEvidenceShippingAddress: + address1: str + address2: str + city: str + state: str + postal_code: str + country: str + + +class CompellingEvidence: + merchandise_or_service: str + merchandise_or_service_desc: str + merchandise_or_service_provided_date: datetime + shipping_delivery_status: str + tracking_information: str + user_id: str + ip_address: str + device_id: str + shipping_address: CompellingEvidenceShippingAddress + historical_transactions: list # CompellingEvidenceHistoricalTransaction class DisputeEvidenceRequest: @@ -34,3 +58,8 @@ class DisputeEvidenceRequest: additional_evidence_text: str proof_of_delivery_or_service_date_file: str proof_of_delivery_or_service_date_text: str + arbitration_no_review_files: list # list[str] + arbitration_no_review_text: str + arbitration_review_required_files: list # list[str] + arbitration_review_required_text: str + compelling_evidence: CompellingEvidence diff --git a/checkout_sdk/forex/forex.py b/checkout_sdk/forex/forex.py index c5f70304..c8056515 100644 --- a/checkout_sdk/forex/forex.py +++ b/checkout_sdk/forex/forex.py @@ -20,4 +20,4 @@ class RatesQueryFilter: product: str source: ForexSource currency_pairs: str - process_channel_id: str + processing_channel_id: str diff --git a/checkout_sdk/forex/forex_client.py b/checkout_sdk/forex/forex_client.py index 1ab8cf87..bb637a5a 100644 --- a/checkout_sdk/forex/forex_client.py +++ b/checkout_sdk/forex/forex_client.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +from warnings import warn + from checkout_sdk.api_client import ApiClient from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration @@ -18,6 +20,9 @@ def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): authorization_type=AuthorizationType.OAUTH) def request_quote(self, quote_request: QuoteRequest): + # Deprecated: 2023-05-31 The /forex/quotes endpoint was removed from the API. Use get_rates instead. + warn('Deprecated: /forex/quotes endpoint was removed from the API. Use get_rates instead.', + DeprecationWarning, stacklevel=2) return self._api_client.post(self.build_path(self.__FOREX_PATH, self.__QUOTES_PATH), self._sdk_authorization(), quote_request) diff --git a/checkout_sdk/forward/forward.py b/checkout_sdk/forward/forward.py index aba285ea..7c579819 100644 --- a/checkout_sdk/forward/forward.py +++ b/checkout_sdk/forward/forward.py @@ -66,12 +66,19 @@ class Headers: encrypted: str = None +class ForwardKeyValue: + name: str + value: str + + class DestinationRequest: url: str method: MethodType headers: Headers body: str signature: DlocalSignature = None + query: list # ForwardKeyValue + variables: list # ForwardKeyValue class NetworkToken: diff --git a/checkout_sdk/instruments/instruments.py b/checkout_sdk/instruments/instruments.py index 6c8a6f45..eb6fed55 100644 --- a/checkout_sdk/instruments/instruments.py +++ b/checkout_sdk/instruments/instruments.py @@ -58,6 +58,7 @@ class CreateBankAccountInstrumentRequest(CreateInstrumentRequest): bban: str swift_bic: str currency: Currency + country: Country processing_channel_id: str account_holder: AccountHolder bank_details: BankDetails @@ -67,6 +68,27 @@ def __init__(self): super().__init__(InstrumentType.BANK_ACCOUNT) +class CreateCardInstrumentRequest(CreateInstrumentRequest): + number: str + expiry_month: int + expiry_year: int + network_token: str + processing_channel_id: str + entity_id: str + account_holder: AccountHolder + + def __init__(self): + super().__init__(InstrumentType.CARD) + + +class CreateAchInstrumentRequest(CreateInstrumentRequest): + instrument_data: InstrumentData + account_holder: AccountHolder + + def __init__(self): + super().__init__(InstrumentType.ACH) + + # Update class UpdateInstrumentRequest: type: InstrumentType diff --git a/checkout_sdk/issuing/cards.py b/checkout_sdk/issuing/cards.py index cb943157..aaceab50 100644 --- a/checkout_sdk/issuing/cards.py +++ b/checkout_sdk/issuing/cards.py @@ -52,6 +52,7 @@ class CardRequest: display_name: str activate_card: bool metadata: CardMetadata + revocation_date: str def __init__(self, type_p: CardType): self.type = type_p diff --git a/checkout_sdk/metadata/metadata.py b/checkout_sdk/metadata/metadata.py index b8fa4e08..17e24122 100644 --- a/checkout_sdk/metadata/metadata.py +++ b/checkout_sdk/metadata/metadata.py @@ -51,3 +51,4 @@ def __init__(self): class CardMetadataRequest: source: CardMetadataRequestSource format: CardMetadataFormatType + reference: str diff --git a/checkout_sdk/payments/contexts/contexts.py b/checkout_sdk/payments/contexts/contexts.py index 63cbf8d9..0842ba24 100644 --- a/checkout_sdk/payments/contexts/contexts.py +++ b/checkout_sdk/payments/contexts/contexts.py @@ -57,6 +57,10 @@ class PaymentContextsProcessing: user_action: UserAction partner_customer_risk_data: list # payment.contexts.PaymentContextsPartnerCustomerRiskData airline_data: list # payment.contexts.PaymentContextsAirlineData + accommodation_data: list # AccommodationData + custom_payment_method_ids: list # list of str + discount_amount: int + tax_amount: int class PaymentContextsItems: @@ -87,6 +91,7 @@ class PaymentContextsRequest: success_url: str failure_url: str items: list # payments.contexts.PaymentContextsItems + metadata: dict @deprecated("This class will be removed in the future. Use PaymentContextPaypalSource instead") diff --git a/checkout_sdk/payments/hosted/hosted_payments.py b/checkout_sdk/payments/hosted/hosted_payments.py index 8ef895bb..c4f5ed3a 100644 --- a/checkout_sdk/payments/hosted/hosted_payments.py +++ b/checkout_sdk/payments/hosted/hosted_payments.py @@ -2,9 +2,10 @@ from checkout_sdk.common.common import CustomerRequest, CustomerRetry from checkout_sdk.common.enums import Currency -from checkout_sdk.payments.payments import BillingDescriptor, PaymentType, ShippingDetails, ThreeDsRequest, \ - RiskRequest, PaymentRecipient, ProcessingSettings, PaymentSender +from checkout_sdk.payments.payments import BillingDescriptor, PaymentInstruction, PaymentType, ShippingDetails, \ + ThreeDsRequest, RiskRequest, PaymentRecipient, ProcessingSettings, PaymentSender from checkout_sdk.payments.payments_previous import BillingInformation +from checkout_sdk.payments.sessions.sessions import SessionPaymentMethodConfiguration class HostedPaymentsSessionRequest: @@ -37,3 +38,5 @@ class HostedPaymentsSessionRequest: three_ds: ThreeDsRequest capture: bool capture_on: datetime + instruction: PaymentInstruction + payment_method_configuration: SessionPaymentMethodConfiguration diff --git a/checkout_sdk/payments/links/payments_links.py b/checkout_sdk/payments/links/payments_links.py index 24628fb0..c2a3623d 100644 --- a/checkout_sdk/payments/links/payments_links.py +++ b/checkout_sdk/payments/links/payments_links.py @@ -2,9 +2,10 @@ from checkout_sdk.common.common import CustomerRequest, CustomerRetry from checkout_sdk.common.enums import Currency -from checkout_sdk.payments.payments import BillingDescriptor, PaymentType, ShippingDetails, ThreeDsRequest, \ - RiskRequest, PaymentRecipient, ProcessingSettings, PaymentSender +from checkout_sdk.payments.payments import BillingDescriptor, PaymentInstruction, PaymentType, ShippingDetails, \ + ThreeDsRequest, RiskRequest, PaymentRecipient, ProcessingSettings, PaymentSender from checkout_sdk.payments.payments_previous import BillingInformation +from checkout_sdk.payments.sessions.sessions import SessionPaymentMethodConfiguration class PaymentLinkRequest: @@ -36,3 +37,5 @@ class PaymentLinkRequest: locale: str capture: bool capture_on: datetime + instruction: PaymentInstruction + payment_method_configuration: SessionPaymentMethodConfiguration diff --git a/checkout_sdk/payments/payment_apm.py b/checkout_sdk/payments/payment_apm.py index 1e475979..f95bd4af 100644 --- a/checkout_sdk/payments/payment_apm.py +++ b/checkout_sdk/payments/payment_apm.py @@ -16,6 +16,7 @@ def __init__(self): super().__init__(PaymentSourceType.IDEAL) +# Deprecated: Sofort was removed from the Checkout.com API. This source no longer functions. class RequestSofortSource(PaymentRequestSource): countryCode: Country languageCode: str @@ -111,6 +112,7 @@ def __init__(self): super().__init__(PaymentSourceType.ILLICADO) +# Deprecated: Giropay was removed from the Checkout.com API. This source no longer functions. class RequestGiropaySource(PaymentRequestSource): account_holder: AccountHolder @@ -283,3 +285,56 @@ class RequestSequraSource(PaymentRequestSource): def __init__(self): super().__init__(PaymentSourceType.SEQURA) + + +class RequestMobilePaySource(PaymentRequestSource): + + def __init__(self): + super().__init__(PaymentSourceType.MOBILEPAY) + + +class RequestPayNowSource(PaymentRequestSource): + + def __init__(self): + super().__init__(PaymentSourceType.PAYNOW) + + +class RequestSwishSource(PaymentRequestSource): + payment_country: Country + account_holder: AccountHolder + billing_descriptor: str + + def __init__(self): + super().__init__(PaymentSourceType.SWISH) + + +class RequestTwintSource(PaymentRequestSource): + + def __init__(self): + super().__init__(PaymentSourceType.TWINT) + + +class RequestVippsSource(PaymentRequestSource): + + def __init__(self): + super().__init__(PaymentSourceType.VIPPS) + + +class RequestSepaV4Source(PaymentRequestSource): + country: Country + account_number: str + currency: Currency + mandate_id: str + mandate_type: str + date_of_signature: str + account_holder: AccountHolder + + def __init__(self): + super().__init__(PaymentSourceType.SEPA) + + +class RequestBlikSource(PaymentRequestSource): + partner_agreement_id: str + + def __init__(self): + super().__init__(PaymentSourceType.BLIK) diff --git a/checkout_sdk/payments/payments.py b/checkout_sdk/payments/payments.py index 6538e7fd..1f95fd14 100644 --- a/checkout_sdk/payments/payments.py +++ b/checkout_sdk/payments/payments.py @@ -33,6 +33,15 @@ class PaymentSenderType(str, Enum): GOVERNMENT = 'government' +class SourceOfFunds(str, Enum): + CREDIT = 'credit' + DEBIT = 'debit' + PREPAID = 'prepaid' + DEPOSIT_ACCOUNT = 'deposit_account' + MOBILE_MONEY_ACCOUNT = 'mobile_money_account' + CASH = 'cash' + + class PayoutSourceType(str, Enum): CURRENCY_ACCOUNT = 'currency_account' ENTITY = 'entity' @@ -131,10 +140,60 @@ class PanPreference(str, Enum): DPAN = 'dpan' +class CardType(str, Enum): + CREDIT = 'credit' + DEBIT = 'debit' + + +class ServiceType(str, Enum): + SAME_DAY = 'same_day' + STANDARD = 'standard' + + +class AmountVariability(str, Enum): + FIXED = 'Fixed' + VARIABLE = 'Variable' + + +class AuthenticationExperience(str, Enum): + GOOGLE_SPA = 'google_spa' + THREE_DS = '3ds' + + +class RoutingScheme(str, Enum): + ACCEL = 'accel' + AMEX = 'amex' + CARTES_BANCAIRES = 'cartes_bancaires' + DINERS = 'diners' + DISCOVER = 'discover' + JCB = 'jcb' + MADA = 'mada' + MAESTRO = 'maestro' + MASTERCARD = 'mastercard' + NYCE = 'nyce' + OMANNET = 'omannet' + PULSE = 'pulse' + SHAZAM = 'shazam' + STAR = 'star' + UPI = 'upi' + VISA = 'visa' + + +class LocalCharacterSets(str, Enum): + KANJI = 'kanji' + KATAKANA = 'katakana' + + +class LocalBillingDescriptor: + name: str + character_set: LocalCharacterSets + + class BillingDescriptor: name: str city: str reference: str + local_descriptors: list # LocalBillingDescriptor class Remitance: @@ -157,6 +216,7 @@ class PayoutBillingDescriptor: # Payment Sender class PaymentSender: type: PaymentSenderType + reference: str def __init__(self, type_p: PaymentSenderType): self.type = type_p @@ -167,25 +227,31 @@ class PaymentCorporateSender(PaymentSender): address: Address reference: str reference_type: str - source_of_funds: str + source_of_funds: SourceOfFunds identification: AccountHolderIdentification def __init__(self): super().__init__(PaymentSenderType.CORPORATE) -class PaymentGovermentSender(PaymentSender): +class PaymentGovernmentSender(PaymentSender): company_name: str address: Address reference: str reference_type: str - source_of_funds: str + source_of_funds: SourceOfFunds identification: AccountHolderIdentification def __init__(self): super().__init__(PaymentSenderType.GOVERNMENT) +# Backward-compat alias for the misspelled class name shipped in earlier SDK +# versions. Prefer PaymentGovernmentSender in new code; this alias will be +# removed in a future major version. +PaymentGovermentSender = PaymentGovernmentSender + + class PaymentIndividualSender(PaymentSender): first_name: str middle_name: str @@ -195,7 +261,7 @@ class PaymentIndividualSender(PaymentSender): identification: AccountHolderIdentification reference: str reference_type: str - source_of_funds: str + source_of_funds: SourceOfFunds date_of_birth: str country_of_birth: Country nationality: Country @@ -229,6 +295,7 @@ class PaymentRequestCardSource(PaymentRequestSource): billing_address: Address phone: Phone account_holder: AccountHolder + allow_update: bool def __init__(self): super().__init__(PaymentSourceType.CARD) @@ -272,6 +339,9 @@ class PaymentRequestIdSource(PaymentRequestSource): stored: bool store_for_future_use: bool account_holder: AccountHolder + billing_address: Address + phone: Phone + allow_update: bool def __init__(self): super().__init__(PaymentSourceType.ID) @@ -301,6 +371,9 @@ def __init__(self): class RequestCustomerSource(PaymentRequestSource): id: str account_holder: AccountHolder + billing_address: Address + phone: Phone + allow_update: bool def __init__(self): super().__init__(PaymentSourceType.CUSTOMER) @@ -325,6 +398,14 @@ class ShippingDetails: delay: int +class InitialAuthentication: + acs_transaction_id: str + authentication_method: str + authentication_timestamp: str + authentication_data: str + initial_session_id: str + + class ThreeDsRequest: enabled: bool attempt_n3d: bool @@ -344,11 +425,39 @@ class ThreeDsRequest: score: str cryptogram_algorithm: str authentication_id: str + initial_authentication: InitialAuthentication + + +class DeviceProvider: + id: str + name: str + + +class DeviceDetails: + user_agent: str + network: dict # Network + provider: DeviceProvider + timestamp: str + timezone: str + virtual_machine: bool + incognito: bool + jailbroken: bool + rooted: bool + java_enabled: bool + javascript_enabled: bool + language: str + color_depth: str + screen_height: str + screen_width: str + user_agent_client_hint: str + iframe_payment_allowed: bool + accept_header: str class RiskRequest: enabled: bool device_session_id: str + device: DeviceDetails class PaymentRecipient: @@ -379,15 +488,15 @@ class DLocalProcessingSettings: class SenderInformation: reference: str - firstName: str - lastName: str + first_name: str + last_name: str dob: str address: str city: str state: str country: str - postalCode: str - sourceOfFunds: str + postal_code: str + source_of_funds: str class PartnerCustomerRiskData: @@ -457,7 +566,7 @@ class ProcessingSettings: shipping_delay: int shipping_info: list # ShippingInfo dlocal: DLocalProcessingSettings - senderInformation: SenderInformation + sender_information: SenderInformation purpose: str partner_customer_risk_data: list # PartnerCustomerRiskData accommodation_data: list # AccommodationData @@ -466,6 +575,12 @@ class ProcessingSettings: provision_network_token: bool affiliate_id: str affiliate_url: str + aggregator: Aggregator + card_type: CardType + foreign_retailer_amount: int + reconciliation_id: str + service_type: ServiceType + partner_code: str class ProductSubType (str, Enum): @@ -504,10 +619,22 @@ class PaymentSegment: market: str +class DowntimeRetryRequest: + enabled: bool + + +class DunningRetryRequest: + enabled: bool + max_attempts: int + end_after_days: int + + class PaymentRetryRequest: enabled: bool max_attempts: int end_after_days: int + downtime: DowntimeRetryRequest + dunning: DunningRetryRequest # Request Payment @@ -515,9 +642,51 @@ class PartialAuthorization: enabled: bool +class Aggregator: + sub_merchant_id: str + aggregator_id_visa: str + aggregator_id_mc: str + + +class PaymentAuthenticationRequest: + preferred_experiences: list # AuthenticationExperience + + +class Attempt: + scheme: RoutingScheme + + +class PaymentRouting: + attempts: list # Attempt + + +class Subscription: + id: str + + +class PaymentPlan: + days_between_payments: int + total_number_of_payments: int + current_payment_number: int + expiry: str + amount: int + name: str + start_date: str + + +class PlanInstallment(PaymentPlan): + financing: bool + amount: str + + +class PlanRecurring(PaymentPlan): + amount_variability: AmountVariability + + class PaymentRequest: payment_context_id: str source: PaymentRequestSource + fallback_source: PaymentRequestSource amount: int currency: Currency payment_type: PaymentType @@ -528,11 +697,13 @@ class PaymentRequest: partial_authorization: PartialAuthorization capture: bool capture_on: datetime + expire_on: datetime customer: PaymentCustomerRequest billing_descriptor: BillingDescriptor shipping: ShippingDetails segment: PaymentSegment three_ds: ThreeDsRequest + authentication: PaymentAuthenticationRequest processing_channel_id: str previous_payment_id: str risk: RiskRequest @@ -549,6 +720,9 @@ class PaymentRequest: items: list # payments.Product retry: PaymentRetryRequest instruction: PaymentInstruction + payment_plan: PaymentPlan + routing: PaymentRouting + subscription: Subscription # Payout Request Source @@ -585,6 +759,7 @@ class PaymentBankAccountDestination(PaymentRequestDestination): account_type: AccountType account_number: str bank_code: str + bban: str branch_code: str iban: str swift_bic: str @@ -616,6 +791,11 @@ class PayoutRequest: sender: PaymentSender instruction: PaymentInstruction processing_channel_id: str + segment: PaymentSegment + items: list # payments.Product + metadata: dict + previous_payment_id: str + processing: ProcessingSettings # Query @@ -656,6 +836,9 @@ class RefundRequest: metadata: dict # Not available on Previous amount_allocations: list # values of AmountAllocations + capture_action_id: str + destination: PaymentBankAccountDestination + items: list # payments.Product # Voids @@ -673,6 +856,7 @@ class CancelScheduledRetryRequest: class ReversePaymentRequest: reference: str metadata: dict + amount: int # Search diff --git a/checkout_sdk/payments/payments_apm_previous.py b/checkout_sdk/payments/payments_apm_previous.py index cacb90c3..69b95f96 100644 --- a/checkout_sdk/payments/payments_apm_previous.py +++ b/checkout_sdk/payments/payments_apm_previous.py @@ -85,6 +85,7 @@ def __init__(self): super().__init__(PaymentSourceType.FAWRY) +# Deprecated: Giropay was removed from the Checkout.com API. This source no longer functions. class RequestGiropaySource(RequestSource): purpose: str bic: str @@ -189,6 +190,7 @@ def __init__(self): super().__init__(PaymentSourceType.PAYPAL) +# Deprecated: POLi was removed from the Checkout.com API. This source no longer functions. class RequestPoliSource(RequestSource): def __init__(self): super().__init__(PaymentSourceType.POLI) @@ -221,6 +223,7 @@ def __init__(self): super().__init__(PaymentSourceType.ID) +# Deprecated: Sofort was removed from the Checkout.com API. This source no longer functions. class RequestSofortSource(RequestSource): countryCode: Country languageCode: str diff --git a/checkout_sdk/payments/sessions/sessions.py b/checkout_sdk/payments/sessions/sessions.py index a4c5f47b..7961c516 100644 --- a/checkout_sdk/payments/sessions/sessions.py +++ b/checkout_sdk/payments/sessions/sessions.py @@ -4,6 +4,7 @@ from checkout_sdk.common.enums import Currency from checkout_sdk.payments.payments import PaymentType, BillingDescriptor, ShippingDetails, \ PaymentRecipient, ProcessingSettings, RiskRequest, ThreeDsRequest, PaymentSender +from checkout_sdk.sessions.sessions import SessionsBillingDescriptor class PaymentMethodsType(str, Enum): @@ -286,8 +287,24 @@ class PaymentSessionWithPaymentRequest: class SubmitPaymentSessionRequest: session_data: str amount: int + currency: Currency reference: str + description: str items: list # Item three_ds: ThreeDsRequest ip_address: str payment_type: PaymentType + billing: SessionBilling + billing_descriptor: SessionsBillingDescriptor + capture: bool + capture_on: datetime + customer: SessionPaymentCustomerRequest + failure_url: str + instruction: Instruction + metadata: dict + payment_method_configuration: SessionPaymentMethodConfiguration + processing_channel_id: str + recipient: PaymentRecipient + sender: PaymentSender + shipping: ShippingDetails + success_url: str diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 935faeea..b385802f 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -91,6 +91,8 @@ def __init__(self): # Tabby entities class Tabby(PaymentMethodBase): + payment_types: list # list of str + def __init__(self): super().__init__() self.payment_method_options: PaymentMethodOptions @@ -121,6 +123,7 @@ class OrderSubMerchant: id: str product_category: str number_of_trades: int + number_of_sales: int registration_date: datetime @@ -143,6 +146,11 @@ class Industry: accommodation_data: list # list of AccommodationData +# Billing entity +class PaymentSetupBilling: + address: Address + + # Main Request and Response classes class PaymentSetupsRequest: processing_channel_id: str @@ -156,3 +164,4 @@ class PaymentSetupsRequest: customer: Customer order: Order industry: Industry + billing: PaymentSetupBilling diff --git a/checkout_sdk/sessions/sessions.py b/checkout_sdk/sessions/sessions.py index 9d0e441b..15a33dde 100644 --- a/checkout_sdk/sessions/sessions.py +++ b/checkout_sdk/sessions/sessions.py @@ -2,7 +2,7 @@ from enum import Enum from checkout_sdk.common.common import Phone, Address -from checkout_sdk.common.enums import Currency, ChallengeIndicator, CardholderAccountAgeIndicatorType, \ +from checkout_sdk.common.enums import Currency, CardholderAccountAgeIndicatorType, \ AccountChangeIndicatorType, AccountPasswordChangeIndicatorType, AccountTypeCardProductType @@ -46,7 +46,22 @@ class AuthenticationType(str, Enum): class Category(str, Enum): PAYMENT = 'payment' - NON_PAYMENT = 'nonPayment' + NON_PAYMENT = 'non_payment' + + +# Wider variant of common.enums.ChallengeIndicator. Only used by SessionRequest +# (the /sessions 3DS endpoint), which folds exemption requests into this field +# instead of having a separate `exemption` field like ThreeDsRequest does. +class SessionChallengeIndicator(str, Enum): + NO_PREFERENCE = 'no_preference' + NO_CHALLENGE_REQUESTED = 'no_challenge_requested' + CHALLENGE_REQUESTED = 'challenge_requested' + CHALLENGE_REQUESTED_MANDATE = 'challenge_requested_mandate' + LOW_VALUE = 'low_value' + TRUSTED_LISTING = 'trusted_listing' + TRUSTED_LISTING_PROMPT = 'trusted_listing_prompt' + TRANSACTION_RISK_ASSESSMENT = 'transaction_risk_assessment' + DATA_SHARE = 'data_share' class TransactionType(str, Enum): @@ -54,7 +69,7 @@ class TransactionType(str, Enum): CHECK_ACCEPTANCE = 'check_acceptance' GOODS_SERVICE = 'goods_service' PREPAID_ACTIVATION_AND_LOAD = 'prepaid_activation_and_load' - QUASHI_CARD_TRANSACTION = 'quashi_card_transaction' + QUASI_CARD_TRANSACTION = 'quasi_card_transaction' class UIElements(str, Enum): @@ -111,6 +126,8 @@ class SessionMarketplaceData: class SessionsBillingDescriptor: name: str + city: str + reference: str # Channel @@ -346,6 +363,15 @@ class InitialTransaction: initial_session_id: str +class DeviceInformation: + device_id: str + device_session_id: str + + +class GoogleSpa: + continue_url: str + + class SessionRequest: source: SessionSource = SessionCardSource() amount: int @@ -355,7 +381,7 @@ class SessionRequest: authentication_type: AuthenticationType = AuthenticationType.REGULAR authentication_category: Category = Category.PAYMENT account_info: CardholderAccountInfo - challenge_indicator: ChallengeIndicator = ChallengeIndicator.NO_PREFERENCE + challenge_indicator: SessionChallengeIndicator = SessionChallengeIndicator.NO_PREFERENCE billing_descriptor: SessionsBillingDescriptor reference: str merchant_risk_info: MerchantRiskInfo @@ -369,6 +395,9 @@ class SessionRequest: installment: Installment optimization: Optimization initial_transaction: InitialTransaction + device_information: DeviceInformation + google_spa: GoogleSpa + preferred_experiences: list # AuthenticationExperience ('3ds' | 'google_spa') class ThreeDsMethodCompletionRequest: diff --git a/checkout_sdk/tokens/tokens.py b/checkout_sdk/tokens/tokens.py index 67d1521b..7d2093bd 100644 --- a/checkout_sdk/tokens/tokens.py +++ b/checkout_sdk/tokens/tokens.py @@ -18,6 +18,7 @@ class CardTokenRequest: expiry_year: int name: str cvv: str + pin: str billing_address: Address phone: Phone From ba37032f8e88edcba3f85407e050b17df5c4d0d9 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 14 May 2026 17:17:05 +0200 Subject: [PATCH 8/8] Fixed pylint issues --- checkout_sdk/payments/payments.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/checkout_sdk/payments/payments.py b/checkout_sdk/payments/payments.py index 1f95fd14..8f134b72 100644 --- a/checkout_sdk/payments/payments.py +++ b/checkout_sdk/payments/payments.py @@ -534,6 +534,12 @@ class AccommodationData: room: list # AccommodationRoom +class Aggregator: + sub_merchant_id: str + aggregator_id_visa: str + aggregator_id_mc: str + + class ProcessingSettings: order_id: str tax_amount: int @@ -642,12 +648,6 @@ class PartialAuthorization: enabled: bool -class Aggregator: - sub_merchant_id: str - aggregator_id_visa: str - aggregator_id_mc: str - - class PaymentAuthenticationRequest: preferred_experiences: list # AuthenticationExperience