From 84e5b7aff14e8f332c5c1b44e93bcc53a9ecd694 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 1 Jun 2026 11:03:09 +0200 Subject: [PATCH 1/6] Python payments update --- checkout_sdk/accounts/accounts.py | 4 ++ checkout_sdk/accounts/accounts_client.py | 21 ++++++- checkout_sdk/checkout_api.py | 4 ++ .../instruments/instruments_client.py | 5 ++ checkout_sdk/onboardingsimulator/__init__.py | 0 .../onboarding_simulator.py | 21 +++++++ .../onboarding_simulator_client.py | 51 ++++++++++++++++ checkout_sdk/payments/sessions/sessions.py | 6 ++ checkout_sdk/payments/setups/setups.py | 61 +++++++++++++++++++ checkout_sdk/tokens/tokens_client.py | 6 ++ tests/accounts/accounts_client_test.py | 21 ++++++- tests/instruments/instruments_client_test.py | 6 ++ tests/onboardingsimulator/__init__.py | 0 .../onboarding_simulator_client_test.py | 48 +++++++++++++++ tests/tokens/tokens_client_test.py | 6 ++ 15 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 checkout_sdk/onboardingsimulator/__init__.py create mode 100644 checkout_sdk/onboardingsimulator/onboarding_simulator.py create mode 100644 checkout_sdk/onboardingsimulator/onboarding_simulator_client.py create mode 100644 tests/onboardingsimulator/__init__.py create mode 100644 tests/onboardingsimulator/onboarding_simulator_client_test.py diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 3b9817b0..777e1eb0 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -427,6 +427,10 @@ class EntityFileRequest: purpose: FilePurpose +class EntityRequirementUpdateRequest: + value: object + + class EtagHeader: etag: str diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 5348fa3b..3b0b3c47 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -5,7 +5,7 @@ from checkout_sdk.accounts.accounts import ( EtagHeader, OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, - ReserveRuleRequest, EntityFileRequest + ReserveRuleRequest, EntityFileRequest, EntityRequirementUpdateRequest ) from checkout_sdk.api_client import ApiClient from checkout_sdk.authorization_type import AuthorizationType @@ -24,6 +24,7 @@ class AccountsClient(Client): __PAYMENT_INSTRUMENTS_PATH = 'payment-instruments' __MEMBERS_PATH = 'members' __RESERVE_RULES_PATH = 'reserve-rules' + __REQUIREMENTS_PATH = 'requirements' def __init__(self, api_client: ApiClient, files_client: ApiClient, @@ -148,6 +149,24 @@ def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, u entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id) return self._api_client.put(path, self._sdk_authorization(), update_request, headers=headers) + def get_entity_requirements(self, entity_id: str): + return self._api_client.get( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__REQUIREMENTS_PATH), + self._sdk_authorization()) + + def get_entity_requirement_details(self, entity_id: str, requirement_id: str): + return self._api_client.get( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, + self.__REQUIREMENTS_PATH, requirement_id), + self._sdk_authorization()) + + def resolve_entity_requirement(self, entity_id: str, requirement_id: str, + request: EntityRequirementUpdateRequest): + return self._api_client.put( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, + self.__REQUIREMENTS_PATH, requirement_id), + self._sdk_authorization(), request) + def upload_entity_file(self, entity_id: str, entity_file_request: EntityFileRequest): return self.__files_client.post( self.build_path(self.__ENTITIES_PATH, entity_id, self.__FILES_PATH), diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 7b006132..a41e49af 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.onboardingsimulator.onboarding_simulator_client import OnboardingSimulatorClient from checkout_sdk.paymentmethods.payment_methods_client import PaymentMethodsClient @@ -115,3 +116,6 @@ def __init__(self, configuration: CheckoutConfiguration): configuration=configuration) self.network_tokens = NetworkTokensClient(api_client=base_api_client, configuration=configuration) self.payment_methods = PaymentMethodsClient(api_client=base_api_client, configuration=configuration) + self.onboarding_simulator = OnboardingSimulatorClient(api_client=base_api_client, + configuration=configuration) + self.onboarding_simulator = OnboardingSimulatorClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/instruments/instruments_client.py b/checkout_sdk/instruments/instruments_client.py index 1ebcd402..156bd75e 100644 --- a/checkout_sdk/instruments/instruments_client.py +++ b/checkout_sdk/instruments/instruments_client.py @@ -33,6 +33,11 @@ def update(self, instrument_id: str, update_instrument_request: UpdateInstrument def delete(self, instrument_id: str): return self._api_client.delete(self.build_path(self.__INSTRUMENTS, instrument_id), self._sdk_authorization()) + def revoke(self, instrument_id: str): + return self._api_client.patch( + self.build_path(self.__INSTRUMENTS, instrument_id, 'revoke'), + self._sdk_authorization()) + def get_bank_account_field_formatting(self, country: Country, currency: Currency, bank_account_field_query: BankAccountFieldQuery): return self._api_client.get(self.build_path(self.__BANK_VALIDATION_PATH, country, currency), diff --git a/checkout_sdk/onboardingsimulator/__init__.py b/checkout_sdk/onboardingsimulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/onboardingsimulator/onboarding_simulator.py b/checkout_sdk/onboardingsimulator/onboarding_simulator.py new file mode 100644 index 00000000..96a8f481 --- /dev/null +++ b/checkout_sdk/onboardingsimulator/onboarding_simulator.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import + +from enum import Enum + + +class SimulatorEntityStatus(str, Enum): + DRAFT = 'draft' + REQUIREMENTS_DUE = 'requirements_due' + PENDING = 'pending' + ACTIVE = 'active' + RESTRICTED = 'restricted' + REJECTED = 'rejected' + INACTIVE = 'inactive' + + +class SimulatorSetRequirementsDueRequest: + fields: list # list of str + + +class SimulatorSetStatusRequest: + status: SimulatorEntityStatus diff --git a/checkout_sdk/onboardingsimulator/onboarding_simulator_client.py b/checkout_sdk/onboardingsimulator/onboarding_simulator_client.py new file mode 100644 index 00000000..88481980 --- /dev/null +++ b/checkout_sdk/onboardingsimulator/onboarding_simulator_client.py @@ -0,0 +1,51 @@ +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.onboardingsimulator.onboarding_simulator import ( + SimulatorSetRequirementsDueRequest, + SimulatorSetStatusRequest +) + + +class OnboardingSimulatorClient(Client): + __SIMULATE_PATH = 'simulate' + __ENTITIES_PATH = 'entities' + __REQUIREMENTS_DUE_PATH = 'requirements-due' + __SCENARIOS_PATH = 'scenarios' + __STATUS_PATH = 'status' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.OAUTH) + + def set_requirements_due(self, entity_id: str, request: SimulatorSetRequirementsDueRequest): + return self._api_client.post( + self.build_path(self.__SIMULATE_PATH, self.__ENTITIES_PATH, entity_id, + self.__REQUIREMENTS_DUE_PATH), + self._sdk_authorization(), request) + + def run_scenario(self, entity_id: str, scenario_id: str): + return self._api_client.post( + self.build_path(self.__SIMULATE_PATH, self.__ENTITIES_PATH, entity_id, + self.__SCENARIOS_PATH, scenario_id), + self._sdk_authorization()) + + def set_entity_status(self, entity_id: str, request: SimulatorSetStatusRequest): + return self._api_client.post( + self.build_path(self.__SIMULATE_PATH, self.__ENTITIES_PATH, entity_id, + self.__STATUS_PATH), + self._sdk_authorization(), request) + + def list_available_requirements(self): + return self._api_client.get( + self.build_path(self.__SIMULATE_PATH, self.__REQUIREMENTS_DUE_PATH), + self._sdk_authorization()) + + def list_scenarios(self): + return self._api_client.get( + self.build_path(self.__SIMULATE_PATH, self.__SCENARIOS_PATH), + self._sdk_authorization()) diff --git a/checkout_sdk/payments/sessions/sessions.py b/checkout_sdk/payments/sessions/sessions.py index 7961c516..a204f58c 100644 --- a/checkout_sdk/payments/sessions/sessions.py +++ b/checkout_sdk/payments/sessions/sessions.py @@ -284,6 +284,11 @@ class PaymentSessionWithPaymentRequest: capture_on: datetime +class PaymentInterfacesProcessingBase: + pan_preference: str + provision_network_token: bool + + class SubmitPaymentSessionRequest: session_data: str amount: int @@ -308,3 +313,4 @@ class SubmitPaymentSessionRequest: sender: PaymentSender shipping: ShippingDetails success_url: str + processing: PaymentInterfacesProcessingBase diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index a06b525a..b5345247 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -108,11 +108,16 @@ def __init__(self): self.payment_method_options: PaymentMethodOptions +class Blik(PaymentMethodBase): + partner_code: str + + class PaymentMethods: klarna: Klarna stcpay: Stcpay tabby: Tabby bizum: Bizum + blik: Blik # Settings entity @@ -159,6 +164,61 @@ class PaymentSetupBilling: address: Address +class AccountFundingTransactionIdentificationType(str, Enum): + PASSPORT = 'passport' + DRIVING_LICENSE = 'driving_license' + NATIONAL_ID = 'national_id' + + +class AccountFundingTransactionIdentification: + type: AccountFundingTransactionIdentificationType + number: str + issuing_country: str + + +class AccountFundingTransactionSender: + date_of_birth: str + reference: str + identification: AccountFundingTransactionIdentification + + +class AccountFundingTransactionPurpose(str, Enum): + DONATIONS = 'donations' + EDUCATION = 'education' + EMERGENCY_NEED = 'emergency_need' + EXPATRIATION = 'expatriation' + FAMILY_SUPPORT = 'family_support' + FINANCIAL_SERVICES = 'financial_services' + GIFTS = 'gifts' + INCOME = 'income' + INSURANCE = 'insurance' + INVESTMENT = 'investment' + IT_SERVICES = 'it_services' + LEISURE = 'leisure' + LOAN_PAYMENT = 'loan_payment' + MEDICAL_TREATMENT = 'medical_treatment' + OTHER = 'other' + PENSION = 'pension' + ROYALTIES = 'royalties' + SAVINGS = 'savings' + TRAVEL_AND_TOURISM = 'travel_and_tourism' + + +class AccountFundingTransactionRecipient: + date_of_birth: str + account_number: str + first_name: str + last_name: str + address: Address + + +class PaymentSetupAccountFundingTransaction: + enabled: bool + purpose: AccountFundingTransactionPurpose + sender: AccountFundingTransactionSender + recipient: AccountFundingTransactionRecipient + + # Main Request and Response classes class PaymentSetupsRequest: processing_channel_id: str @@ -173,3 +233,4 @@ class PaymentSetupsRequest: order: Order industry: Industry billing: PaymentSetupBilling + account_funding_transaction: PaymentSetupAccountFundingTransaction diff --git a/checkout_sdk/tokens/tokens_client.py b/checkout_sdk/tokens/tokens_client.py index ed91939a..c55389fc 100644 --- a/checkout_sdk/tokens/tokens_client.py +++ b/checkout_sdk/tokens/tokens_client.py @@ -20,3 +20,9 @@ def request_card_token(self, request: CardTokenRequest): def request_wallet_token(self, request: WalletTokenRequest): return self._api_client.post(self.__TOKENS, self._sdk_authorization(), request) + + def get_token_metadata(self, token_id: str): + return self._api_client.get( + self.build_path(self.__TOKENS, token_id, 'metadata'), + self._sdk_authorization(AuthorizationType.SECRET_KEY_OR_OAUTH) + ) diff --git a/tests/accounts/accounts_client_test.py b/tests/accounts/accounts_client_test.py index 92413bc2..3feb1f1a 100644 --- a/tests/accounts/accounts_client_test.py +++ b/tests/accounts/accounts_client_test.py @@ -3,7 +3,7 @@ from tests._assertions import assert_api_call from checkout_sdk.accounts.accounts import OnboardEntityRequest, AccountsPaymentInstrument, UpdateScheduleRequest, \ PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ - EntityFileRequest, FilePurpose + EntityFileRequest, FilePurpose, EntityRequirementUpdateRequest from checkout_sdk.accounts.accounts_client import AccountsClient from checkout_sdk.common.enums import Currency from checkout_sdk.files.files import FileRequest @@ -149,3 +149,22 @@ def test_should_retrieve_entity_file(self, mocker, client: AccountsClient): assert client.retrieve_entity_file('entity_id', 'file_id') == 'response' assert_api_call(mock, 'entities/entity_id/files/file_id') + + def test_should_get_entity_requirements(self, mocker, client: AccountsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + assert client.get_entity_requirements('entity_id') == 'response' + assert_api_call(mock, 'accounts/entities/entity_id/requirements') + + def test_should_get_entity_requirement_details(self, mocker, client: AccountsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + assert client.get_entity_requirement_details('entity_id', 'requirement_id') == 'response' + assert_api_call(mock, 'accounts/entities/entity_id/requirements/requirement_id') + + def test_should_resolve_entity_requirement(self, mocker, client: AccountsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.put', return_value='response') + body = EntityRequirementUpdateRequest() + + assert client.resolve_entity_requirement('entity_id', 'requirement_id', body) == 'response' + assert_api_call(mock, 'accounts/entities/entity_id/requirements/requirement_id', body) diff --git a/tests/instruments/instruments_client_test.py b/tests/instruments/instruments_client_test.py index a69ec92e..d0ede914 100644 --- a/tests/instruments/instruments_client_test.py +++ b/tests/instruments/instruments_client_test.py @@ -40,6 +40,12 @@ def test_should_delete_instrument(self, mocker, client: InstrumentsClient): assert client.delete('instrument_id') == 'response' assert_api_call(mock, 'instruments/instrument_id') + def test_should_revoke_instrument(self, mocker, client: InstrumentsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + + assert client.revoke('instrument_id') == 'response' + assert_api_call(mock, 'instruments/instrument_id/revoke') + def test_should_get_bank_account_field_formatting(self, mocker, client: InstrumentsClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') query = BankAccountFieldQuery() diff --git a/tests/onboardingsimulator/__init__.py b/tests/onboardingsimulator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/onboardingsimulator/onboarding_simulator_client_test.py b/tests/onboardingsimulator/onboarding_simulator_client_test.py new file mode 100644 index 00000000..638f19b3 --- /dev/null +++ b/tests/onboardingsimulator/onboarding_simulator_client_test.py @@ -0,0 +1,48 @@ +import pytest + +from tests._assertions import assert_api_call +from checkout_sdk.onboardingsimulator.onboarding_simulator import ( + SimulatorSetRequirementsDueRequest, + SimulatorSetStatusRequest +) +from checkout_sdk.onboardingsimulator.onboarding_simulator_client import OnboardingSimulatorClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return OnboardingSimulatorClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestOnboardingSimulatorClient: + + def test_should_set_requirements_due(self, mocker, client: OnboardingSimulatorClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = SimulatorSetRequirementsDueRequest() + + assert client.set_requirements_due('entity_id', body) == 'response' + assert_api_call(mock, 'simulate/entities/entity_id/requirements-due', body) + + def test_should_run_scenario(self, mocker, client: OnboardingSimulatorClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + + assert client.run_scenario('entity_id', 'scenario_id') == 'response' + assert_api_call(mock, 'simulate/entities/entity_id/scenarios/scenario_id') + + def test_should_set_entity_status(self, mocker, client: OnboardingSimulatorClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = SimulatorSetStatusRequest() + + assert client.set_entity_status('entity_id', body) == 'response' + assert_api_call(mock, 'simulate/entities/entity_id/status', body) + + def test_should_list_available_requirements(self, mocker, client: OnboardingSimulatorClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + assert client.list_available_requirements() == 'response' + assert_api_call(mock, 'simulate/requirements-due') + + def test_should_list_scenarios(self, mocker, client: OnboardingSimulatorClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + assert client.list_scenarios() == 'response' + assert_api_call(mock, 'simulate/scenarios') diff --git a/tests/tokens/tokens_client_test.py b/tests/tokens/tokens_client_test.py index 5a29a84b..535c4d24 100644 --- a/tests/tokens/tokens_client_test.py +++ b/tests/tokens/tokens_client_test.py @@ -25,3 +25,9 @@ def test_should_request_wallet_token(self, mocker, client: TokensClient): assert client.request_wallet_token(body) == 'response' assert_api_call(mock, 'tokens', body) + + def test_should_get_token_metadata(self, mocker, client: TokensClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + assert client.get_token_metadata('tok_123') == 'response' + assert_api_call(mock, 'tokens/tok_123/metadata') From d0a336177b6b2bcb2821a9a1f9c38f79a0287d96 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 1 Jun 2026 12:33:29 +0200 Subject: [PATCH 2/6] Small swagger alignments --- checkout_sdk/instruments/instruments.py | 7 ++++--- checkout_sdk/payments/setups/setups.py | 19 ++++++++++++++++++ checkout_sdk/tokens/tokens.py | 26 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/checkout_sdk/instruments/instruments.py b/checkout_sdk/instruments/instruments.py index e982f5b5..f925a0f6 100644 --- a/checkout_sdk/instruments/instruments.py +++ b/checkout_sdk/instruments/instruments.py @@ -81,10 +81,11 @@ class CreateCardInstrumentRequest(CreateInstrumentRequest): number: str expiry_month: int expiry_year: int - network_token: str - processing_channel_id: str - entity_id: str account_holder: AccountHolder + customer: CreateCustomerInstrumentRequest + entity_id: str + processing_channel_id: str + network_token: str def __init__(self): super().__init__(InstrumentType.CARD) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index b5345247..63a54095 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -112,12 +112,31 @@ class Blik(PaymentMethodBase): partner_code: str +class PaypalUserAction(str, Enum): + PAY_NOW = 'pay_now' + CONTINUE = 'continue' + + +class PaypalShippingPreference(str, Enum): + NO_SHIPPING = 'no_shipping' + GET_FROM_FILE = 'get_from_file' + SET_PROVIDED_ADDRESS = 'set_provided_address' + + +class Paypal(PaymentMethodBase): + user_action: PaypalUserAction + brand_name: str + shipping_preference: PaypalShippingPreference + action: PaymentMethodAction + + class PaymentMethods: klarna: Klarna stcpay: Stcpay tabby: Tabby bizum: Bizum blik: Blik + paypal: Paypal # Settings entity diff --git a/checkout_sdk/tokens/tokens.py b/checkout_sdk/tokens/tokens.py index 7d2093bd..8fce84bb 100644 --- a/checkout_sdk/tokens/tokens.py +++ b/checkout_sdk/tokens/tokens.py @@ -9,6 +9,8 @@ class TokenType(str, Enum): CARD = 'card' APPLE_PAY = 'applepay' GOOGLE_PAY = 'googlepay' + CVV = 'cvv' + PIN = 'pin' class CardTokenRequest: @@ -58,3 +60,27 @@ class ApplePayTokenRequest(WalletTokenRequest): def __init__(self): super().__init__(TokenType.APPLE_PAY) + + +class CvvTokenData: + cvv: str + + +class CvvTokenRequest: + type: TokenType + token_data: CvvTokenData + + def __init__(self): + self.type = TokenType.CVV + + +class PinTokenData: + pin: str + + +class PinTokenRequest: + type: TokenType + token_data: PinTokenData + + def __init__(self): + self.type = TokenType.PIN From d443ff48cb673d776545b827adf5ed3572c75367 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 1 Jun 2026 12:45:53 +0200 Subject: [PATCH 3/6] New tests --- checkout_sdk/tokens/tokens_client.py | 8 ++++++- tests/instruments/instruments_client_test.py | 20 ++++++++++++++++-- tests/tokens/tokens_client_test.py | 22 +++++++++++++++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/checkout_sdk/tokens/tokens_client.py b/checkout_sdk/tokens/tokens_client.py index c55389fc..ad4f1f75 100644 --- a/checkout_sdk/tokens/tokens_client.py +++ b/checkout_sdk/tokens/tokens_client.py @@ -4,7 +4,7 @@ from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration from checkout_sdk.client import Client -from checkout_sdk.tokens.tokens import WalletTokenRequest, CardTokenRequest +from checkout_sdk.tokens.tokens import WalletTokenRequest, CardTokenRequest, CvvTokenRequest, PinTokenRequest class TokensClient(Client): @@ -21,6 +21,12 @@ def request_card_token(self, request: CardTokenRequest): def request_wallet_token(self, request: WalletTokenRequest): return self._api_client.post(self.__TOKENS, self._sdk_authorization(), request) + def request_cvv_token(self, request: CvvTokenRequest): + return self._api_client.post(self.__TOKENS, self._sdk_authorization(), request) + + def request_pin_token(self, request: PinTokenRequest): + return self._api_client.post(self.__TOKENS, self._sdk_authorization(), request) + def get_token_metadata(self, token_id: str): return self._api_client.get( self.build_path(self.__TOKENS, token_id, 'metadata'), diff --git a/tests/instruments/instruments_client_test.py b/tests/instruments/instruments_client_test.py index d0ede914..0389a684 100644 --- a/tests/instruments/instruments_client_test.py +++ b/tests/instruments/instruments_client_test.py @@ -1,9 +1,10 @@ import pytest from tests._assertions import assert_api_call +from checkout_sdk.common.common import Phone from checkout_sdk.common.enums import Country, Currency -from checkout_sdk.instruments.instruments import CreateTokenInstrumentRequest, UpdateCardInstrumentRequest, \ - BankAccountFieldQuery +from checkout_sdk.instruments.instruments import CreateTokenInstrumentRequest, CreateCardInstrumentRequest, \ + CreateCustomerInstrumentRequest, UpdateCardInstrumentRequest, BankAccountFieldQuery from checkout_sdk.instruments.instruments_client import InstrumentsClient @@ -27,6 +28,21 @@ def test_should_create_instrument(self, mocker, client: InstrumentsClient): assert client.create(body) == 'response' assert_api_call(mock, 'instruments', body) + def test_should_create_card_instrument_with_customer(self, mocker, client: InstrumentsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + customer = CreateCustomerInstrumentRequest() + customer.email = 'test@example.com' + customer.name = 'Test User' + customer.default = True + body = CreateCardInstrumentRequest() + body.number = '4242424242424242' + body.expiry_month = 6 + body.expiry_year = 2025 + body.customer = customer + + assert client.create(body) == 'response' + assert_api_call(mock, 'instruments', body) + def test_should_update_instrument(self, mocker, client: InstrumentsClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') body = UpdateCardInstrumentRequest() diff --git a/tests/tokens/tokens_client_test.py b/tests/tokens/tokens_client_test.py index 535c4d24..decc38c6 100644 --- a/tests/tokens/tokens_client_test.py +++ b/tests/tokens/tokens_client_test.py @@ -1,7 +1,7 @@ import pytest from tests._assertions import assert_api_call -from checkout_sdk.tokens.tokens import CardTokenRequest, ApplePayTokenRequest +from checkout_sdk.tokens.tokens import CardTokenRequest, ApplePayTokenRequest, CvvTokenRequest, CvvTokenData, PinTokenRequest, PinTokenData from checkout_sdk.tokens.tokens_client import TokensClient @@ -26,6 +26,26 @@ def test_should_request_wallet_token(self, mocker, client: TokensClient): assert client.request_wallet_token(body) == 'response' assert_api_call(mock, 'tokens', body) + def test_should_request_cvv_token(self, mocker, client: TokensClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + token_data = CvvTokenData() + token_data.cvv = '956' + body = CvvTokenRequest() + body.token_data = token_data + + assert client.request_cvv_token(body) == 'response' + assert_api_call(mock, 'tokens', body) + + def test_should_request_pin_token(self, mocker, client: TokensClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + token_data = PinTokenData() + token_data.pin = '12' + body = PinTokenRequest() + body.token_data = token_data + + assert client.request_pin_token(body) == 'response' + assert_api_call(mock, 'tokens', body) + def test_should_get_token_metadata(self, mocker, client: TokensClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') From 33e3b8fb15edd22b9770849c5468f5d3852bfb4e Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 1 Jun 2026 12:51:47 +0200 Subject: [PATCH 4/6] get_access_token failures protect --- checkout_sdk/oauth_credentials.py | 8 ++++++-- tests/oauth_integration_test.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/checkout_sdk/oauth_credentials.py b/checkout_sdk/oauth_credentials.py index 924c7a96..1f3bb5ce 100644 --- a/checkout_sdk/oauth_credentials.py +++ b/checkout_sdk/oauth_credentials.py @@ -77,8 +77,12 @@ def get_access_token(self): auth=(self.__client_id, self.__client_secret)) response.raise_for_status() except HTTPError as err: - errors = json.loads(err.response.text) - message = 'OAuth client_credentials authentication failed with error: ({})'.format(errors['error']) + try: + errors = json.loads(err.response.text) + message = 'OAuth client_credentials authentication failed with error: ({})'.format(errors['error']) + except (json.JSONDecodeError, KeyError): + message = 'OAuth client_credentials authentication failed with status: ({})'.format( + err.response.status_code) raise CheckoutAuthorizationException(message) from err except ConnectionError as err: raise CheckoutAuthorizationException( diff --git a/tests/oauth_integration_test.py b/tests/oauth_integration_test.py index 179eab7c..99a34410 100644 --- a/tests/oauth_integration_test.py +++ b/tests/oauth_integration_test.py @@ -57,4 +57,4 @@ def test_should_fail_oauth_with_subdomain_invalid_credentials(): .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT]) \ .build() except CheckoutException as err: - assert err.args[0] == 'OAuth client_credentials authentication failed with error: (invalid_client)' + assert err.args[0] == 'OAuth client_credentials authentication failed with status: (406)' From 99d7fa1158066459b181b9e3e74d8ee485036a0b Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 1 Jun 2026 13:19:45 +0200 Subject: [PATCH 5/6] Flake8 fixes --- tests/instruments/instruments_client_test.py | 1 - tests/tokens/tokens_client_test.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/instruments/instruments_client_test.py b/tests/instruments/instruments_client_test.py index 0389a684..6a078771 100644 --- a/tests/instruments/instruments_client_test.py +++ b/tests/instruments/instruments_client_test.py @@ -1,7 +1,6 @@ import pytest from tests._assertions import assert_api_call -from checkout_sdk.common.common import Phone from checkout_sdk.common.enums import Country, Currency from checkout_sdk.instruments.instruments import CreateTokenInstrumentRequest, CreateCardInstrumentRequest, \ CreateCustomerInstrumentRequest, UpdateCardInstrumentRequest, BankAccountFieldQuery diff --git a/tests/tokens/tokens_client_test.py b/tests/tokens/tokens_client_test.py index decc38c6..cce5385a 100644 --- a/tests/tokens/tokens_client_test.py +++ b/tests/tokens/tokens_client_test.py @@ -1,7 +1,8 @@ import pytest from tests._assertions import assert_api_call -from checkout_sdk.tokens.tokens import CardTokenRequest, ApplePayTokenRequest, CvvTokenRequest, CvvTokenData, PinTokenRequest, PinTokenData +from checkout_sdk.tokens.tokens import CardTokenRequest, ApplePayTokenRequest, CvvTokenRequest, CvvTokenData, \ + PinTokenRequest, PinTokenData from checkout_sdk.tokens.tokens_client import TokensClient From bd63789d1d53d7c7634f9485a3d5af0e2d83c221 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 2 Jun 2026 17:00:51 +0200 Subject: [PATCH 6/6] Python fields review and payment types update --- checkout_sdk/checkout_api.py | 1 - checkout_sdk/instruments/instruments.py | 6 +- checkout_sdk/payments/sessions/sessions.py | 1 - checkout_sdk/payments/setups/setups.py | 323 +++++++++++++++++- checkout_sdk/tokens/tokens.py | 10 +- .../setups/payment_setups_integration_test.py | 45 ++- .../payment_setups_serialization_test.py | 234 +++++++++++++ 7 files changed, 609 insertions(+), 11 deletions(-) create mode 100644 tests/payments/setups/payment_setups_serialization_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index a41e49af..7ba32cc9 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -118,4 +118,3 @@ def __init__(self, configuration: CheckoutConfiguration): self.payment_methods = PaymentMethodsClient(api_client=base_api_client, configuration=configuration) self.onboarding_simulator = OnboardingSimulatorClient(api_client=base_api_client, configuration=configuration) - self.onboarding_simulator = OnboardingSimulatorClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/instruments/instruments.py b/checkout_sdk/instruments/instruments.py index f925a0f6..4eec663c 100644 --- a/checkout_sdk/instruments/instruments.py +++ b/checkout_sdk/instruments/instruments.py @@ -77,6 +77,10 @@ def __init__(self): super().__init__(InstrumentType.BANK_ACCOUNT) +class ProvisionNetworkToken: + provision: bool + + class CreateCardInstrumentRequest(CreateInstrumentRequest): number: str expiry_month: int @@ -85,7 +89,7 @@ class CreateCardInstrumentRequest(CreateInstrumentRequest): customer: CreateCustomerInstrumentRequest entity_id: str processing_channel_id: str - network_token: str + network_token: ProvisionNetworkToken def __init__(self): super().__init__(InstrumentType.CARD) diff --git a/checkout_sdk/payments/sessions/sessions.py b/checkout_sdk/payments/sessions/sessions.py index a204f58c..1d6d546b 100644 --- a/checkout_sdk/payments/sessions/sessions.py +++ b/checkout_sdk/payments/sessions/sessions.py @@ -294,7 +294,6 @@ class SubmitPaymentSessionRequest: amount: int currency: Currency reference: str - description: str items: list # Item three_ds: ThreeDsRequest ip_address: str diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 63a54095..9dd06846 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -51,6 +51,7 @@ class PaymentMethodAction: type: str client_token: str session_id: str + order_id: str class PaymentMethodOption: @@ -130,13 +131,334 @@ class Paypal(PaymentMethodBase): action: PaymentMethodAction +# Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). +# Distinct from PaymentMethodBase, which also carries the writable `initialization` field +# supported only by klarna, stcpay, tabby and paypal. +class PaymentSetupPaymentMethod: + status: str + flags: list # list of str + + +# Shared payment-method enums +class TerminalType(str, Enum): + WEB = 'web' + WAP = 'wap' + APP = 'app' + + +class OsType(str, Enum): + ANDROID = 'android' + IOS = 'ios' + + +class KnetLanguage(str, Enum): + EN = 'en' + AR = 'ar' + + +class PaymentSetupAccountHolderType(str, Enum): + INDIVIDUAL = 'individual' + CORPORATE = 'corporate' + GOVERNMENT = 'government' + + +# Account holder shared by card, wallet and instrument methods +class PaymentSetupAccountHolder: + type: PaymentSetupAccountHolderType + first_name: str + last_name: str + middle_name: str + company_name: str + account_name_inquiry: bool + + +# Instrument entities +class PaymentSetupInstrument(PaymentSetupPaymentMethod): + id: str + phone: Phone + account_holder: PaymentSetupAccountHolder + allow_update: bool + action: PaymentMethodAction + + +# Status/flags-only methods +class PayNow(PaymentSetupPaymentMethod): + pass + + +class Eps(PaymentSetupPaymentMethod): + pass + + +class Benefit(PaymentSetupPaymentMethod): + pass + + +class Vipps(PaymentSetupPaymentMethod): + pass + + +class Twint(PaymentSetupPaymentMethod): + pass + + +class MobilePay(PaymentSetupPaymentMethod): + pass + + +class Tamara(PaymentSetupPaymentMethod): + pass + + +class MBWay(PaymentSetupPaymentMethod): + pass + + +class WeChatPay(PaymentSetupPaymentMethod): + pass + + +class Octopus(PaymentSetupPaymentMethod): + pass + + +class Alma(PaymentSetupPaymentMethod): + pass + + +class Sequra(PaymentSetupPaymentMethod): + pass + + +# Wallets that share terminal_type/os_type configuration +class TerminalPaymentMethod(PaymentSetupPaymentMethod): + terminal_type: TerminalType + os_type: OsType + + +class AlipayCn(TerminalPaymentMethod): + pass + + +class AlipayHK(TerminalPaymentMethod): + pass + + +class GCash(TerminalPaymentMethod): + pass + + +class Tng(TerminalPaymentMethod): + pass + + +class Dana(TerminalPaymentMethod): + pass + + +class KakaoPay(TerminalPaymentMethod): + pass + + +class TrueMoney(TerminalPaymentMethod): + pass + + +# Methods with specific fields +class Qpay(PaymentSetupPaymentMethod): + national_id: str + description: str + + +class Ideal(PaymentSetupPaymentMethod): + description: str + + +class Knet(PaymentSetupPaymentMethod): + language: KnetLanguage + + +class Bancontact(PaymentSetupPaymentMethod): + account_holder_name: str + + +class Multibanco(PaymentSetupPaymentMethod): + account_holder_name: str + + +class P24AccountHolder: + name: str + email: str + + +class P24(PaymentSetupPaymentMethod): + account_holder: P24AccountHolder + + +class SwishAccountHolder: + first_name: str + last_name: str + + +class Swish(PaymentSetupPaymentMethod): + billing_descriptor: str + account_holder: SwishAccountHolder + + +# ACH entities +class AchAccountType(str, Enum): + SAVINGS = 'savings' + CURRENT = 'current' + CASH = 'cash' + + +class AchAccountHolderIdentification: + type: str + issuing_country: str + number: str + + +class AchAccountHolder: + type: PaymentSetupAccountHolderType + first_name: str + last_name: str + company_name: str + date_of_birth: str + identification: AchAccountHolderIdentification + + +class Ach(PaymentSetupPaymentMethod): + account_type: AchAccountType + account_holder: AchAccountHolder + account_number: str + bank_code: str + country: str + + +# SEPA entities +class SepaMandateType(str, Enum): + CORE = 'core' + B2B = 'b2b' + + +class SepaMandate: + id: str + type: SepaMandateType + date_of_signature: datetime + + +class SepaAccountHolder: + type: PaymentSetupAccountHolderType + first_name: str + last_name: str + company_name: str + + +class Sepa(PaymentSetupPaymentMethod): + account_holder: SepaAccountHolder + account_number: str + country: str + currency: Currency + mandate: SepaMandate + + +# Google Pay entities +class GooglePayTokenData: + protocol_version: str + signature: str + signed_message: str + tokenization_key: str + + +class GooglePay(PaymentSetupPaymentMethod): + token_data: GooglePayTokenData + token: str + expires_on: datetime + store_for_future_use: bool + phone: Phone + account_holder: PaymentSetupAccountHolder + + +# Apple Pay entities +class ApplePayTokenDataHeader: + ephemeral_public_key: str + public_key_hash: str + transaction_id: str + + +class ApplePayTokenData: + version: str + data: str + signature: str + header: ApplePayTokenDataHeader + + +class ApplePay(PaymentSetupPaymentMethod): + token_data: ApplePayTokenData + token: str + expires_on: datetime + store_for_future_use: bool + phone: Phone + account_holder: PaymentSetupAccountHolder + + +# Card entities +class Card(PaymentSetupPaymentMethod): + number: str + last4: str + bin: str + scheme: str + expiry_month: int + expiry_year: int + name: str + cvv: str + stored: bool + expires_on: datetime + store_for_future_use: bool + phone: Phone + account_holder: PaymentSetupAccountHolder + allow_update: bool + + class PaymentMethods: + instrument: PaymentSetupInstrument klarna: Klarna stcpay: Stcpay tabby: Tabby bizum: Bizum + paynow: PayNow + qpay: Qpay + eps: Eps + ideal: Ideal + knet: Knet + bancontact: Bancontact + benefit: Benefit blik: Blik + vipps: Vipps + twint: Twint + alipay_cn: AlipayCn + alipay_hk: AlipayHK + gcash: GCash + tng: Tng + dana: Dana + mobilepay: MobilePay + tamara: Tamara + mbway: MBWay + multibanco: Multibanco + wechatpay: WeChatPay + kakaopay: KakaoPay + truemoney: TrueMoney + octopus: Octopus + p24: P24 + alma: Alma + swish: Swish + sequra: Sequra + ach: Ach + sepa: Sepa paypal: Paypal + googlepay: GooglePay + applepay: ApplePay + card: Card # Settings entity @@ -151,7 +473,6 @@ class Settings: class OrderSubMerchant: id: str product_category: str - number_of_trades: int number_of_sales: int registration_date: datetime diff --git a/checkout_sdk/tokens/tokens.py b/checkout_sdk/tokens/tokens.py index 8fce84bb..2966e2a0 100644 --- a/checkout_sdk/tokens/tokens.py +++ b/checkout_sdk/tokens/tokens.py @@ -66,21 +66,19 @@ class CvvTokenData: cvv: str -class CvvTokenRequest: - type: TokenType +class CvvTokenRequest(WalletTokenRequest): token_data: CvvTokenData def __init__(self): - self.type = TokenType.CVV + super().__init__(TokenType.CVV) class PinTokenData: pin: str -class PinTokenRequest: - type: TokenType +class PinTokenRequest(WalletTokenRequest): token_data: PinTokenData def __init__(self): - self.type = TokenType.PIN + super().__init__(TokenType.PIN) diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index aa523a56..f6629b13 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -8,7 +8,8 @@ from checkout_sdk.payments.payments import PaymentType from checkout_sdk.payments.setups.setups import ( PaymentSetupsRequest, Settings, Customer, CustomerEmail, CustomerDevice, - PaymentMethods, Klarna, KlarnaAccountHolder, PaymentMethodInitialization + PaymentMethods, Klarna, KlarnaAccountHolder, PaymentMethodInitialization, + Ideal, Knet, KnetLanguage, Bancontact, P24, P24AccountHolder ) from tests.checkout_test_utils import assert_response, new_uuid @@ -94,6 +95,48 @@ def test_should_get_payment_setup(default_api): assert response.description == create_request.description +def test_should_create_payment_setup_with_additional_payment_methods(default_api): + """Test creating a payment setup that configures several payment methods. + + Exercises the expanded PaymentMethods coverage (iDEAL, KNET, Bancontact, P24). + The methods must be enabled on the test account for the request to succeed. + """ + # Arrange + request = create_payment_setups_request() + + ideal = Ideal() + ideal.description = "2 t-shirts" + request.payment_methods.ideal = ideal + + knet = Knet() + knet.language = KnetLanguage.EN + request.payment_methods.knet = knet + + bancontact = Bancontact() + bancontact.account_holder_name = "John Smith" + request.payment_methods.bancontact = bancontact + + p24 = P24() + p24_account_holder = P24AccountHolder() + p24_account_holder.name = "John Smith" + p24_account_holder.email = "john.smith@example.com" + p24.account_holder = p24_account_holder + request.payment_methods.p24 = p24 + + # Act + response = default_api.setups.create_payment_setup(request) + + # Assert + assert_response(response, + 'http_metadata', + 'id', + 'amount', + 'currency') + + assert response.amount == request.amount + assert response.currency == request.currency + + @pytest.mark.skip(reason="Integration test - requires valid payment method option") def test_should_confirm_payment_setup(default_api): """Test confirming a payment setup""" diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py new file mode 100644 index 00000000..15c5ba7b --- /dev/null +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -0,0 +1,234 @@ +import json + +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.common.common import Phone +from checkout_sdk.common.enums import Currency +from checkout_sdk.payments.setups.setups import ( + PaymentMethods, PaymentSetupInstrument, PayNow, AlipayCn, TerminalType, OsType, + Qpay, Ideal, Knet, KnetLanguage, Bancontact, Multibanco, P24, P24AccountHolder, + Swish, SwishAccountHolder, Ach, AchAccountType, AchAccountHolder, + AchAccountHolderIdentification, Sepa, SepaAccountHolder, SepaMandate, SepaMandateType, + GooglePay, GooglePayTokenData, ApplePay, ApplePayTokenData, ApplePayTokenDataHeader, + Card, PaymentSetupAccountHolder, PaymentSetupAccountHolderType, +) + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +class TestPaymentSetupsSerialization: + + def test_status_flags_only_method_serializes_without_spurious_fields(self): + # A method that is not configured must not emit status/flags/initialization. + assert _serialize(PayNow()) == {} + + def test_terminal_wallet_serializes_terminal_and_os_type(self): + alipay = AlipayCn() + alipay.terminal_type = TerminalType.WEB + alipay.os_type = OsType.ANDROID + + assert _serialize(alipay) == {'terminal_type': 'web', 'os_type': 'android'} + + def test_qpay_serializes_specific_fields(self): + qpay = Qpay() + qpay.national_id = '21234567890' + qpay.description = 'Order 123' + + assert _serialize(qpay) == {'national_id': '21234567890', 'description': 'Order 123'} + + def test_ideal_serializes_description(self): + ideal = Ideal() + ideal.description = '2 t-shirts' + + assert _serialize(ideal) == {'description': '2 t-shirts'} + + def test_knet_serializes_language_enum(self): + knet = Knet() + knet.language = KnetLanguage.AR + + assert _serialize(knet) == {'language': 'ar'} + + def test_bancontact_and_multibanco_serialize_account_holder_name(self): + bancontact = Bancontact() + bancontact.account_holder_name = 'John Smith' + multibanco = Multibanco() + multibanco.account_holder_name = 'Jane Doe' + + assert _serialize(bancontact) == {'account_holder_name': 'John Smith'} + assert _serialize(multibanco) == {'account_holder_name': 'Jane Doe'} + + def test_p24_serializes_nested_account_holder(self): + p24 = P24() + holder = P24AccountHolder() + holder.name = 'John Smith' + holder.email = 'john@example.com' + p24.account_holder = holder + + assert _serialize(p24) == { + 'account_holder': {'name': 'John Smith', 'email': 'john@example.com'} + } + + def test_swish_serializes_billing_descriptor_and_account_holder(self): + swish = Swish() + swish.billing_descriptor = 'ACME' + holder = SwishAccountHolder() + holder.first_name = 'John' + holder.last_name = 'Smith' + swish.account_holder = holder + + assert _serialize(swish) == { + 'billing_descriptor': 'ACME', + 'account_holder': {'first_name': 'John', 'last_name': 'Smith'}, + } + + def test_ach_serializes_nested_structure(self): + ach = Ach() + ach.account_type = AchAccountType.CURRENT + ach.account_number = '12345678' + ach.bank_code = '01234567' + ach.country = 'US' + + holder = AchAccountHolder() + holder.type = PaymentSetupAccountHolderType.INDIVIDUAL + holder.first_name = 'John' + holder.last_name = 'Smith' + identification = AchAccountHolderIdentification() + identification.type = 'ssn' + identification.issuing_country = 'US' + identification.number = '123456789' + holder.identification = identification + ach.account_holder = holder + + assert _serialize(ach) == { + 'account_type': 'current', + 'account_number': '12345678', + 'bank_code': '01234567', + 'country': 'US', + 'account_holder': { + 'type': 'individual', + 'first_name': 'John', + 'last_name': 'Smith', + 'identification': { + 'type': 'ssn', + 'issuing_country': 'US', + 'number': '123456789', + }, + }, + } + + def test_sepa_serializes_account_holder_and_mandate(self): + sepa = Sepa() + sepa.account_number = 'DE89370400440532013000' + sepa.country = 'DE' + sepa.currency = Currency.EUR + + holder = SepaAccountHolder() + holder.type = PaymentSetupAccountHolderType.CORPORATE + holder.company_name = 'ACME Ltd' + sepa.account_holder = holder + + mandate = SepaMandate() + mandate.id = 'man_123' + mandate.type = SepaMandateType.CORE + sepa.mandate = mandate + + assert _serialize(sepa) == { + 'account_number': 'DE89370400440532013000', + 'country': 'DE', + 'currency': 'EUR', + 'account_holder': {'type': 'corporate', 'company_name': 'ACME Ltd'}, + 'mandate': {'id': 'man_123', 'type': 'core'}, + } + + def test_googlepay_serializes_token_data(self): + googlepay = GooglePay() + googlepay.token = 'tok_x' + token_data = GooglePayTokenData() + token_data.protocol_version = 'ECv2' + token_data.signature = 'sig' + token_data.signed_message = 'msg' + token_data.tokenization_key = 'pk_x' + googlepay.token_data = token_data + + assert _serialize(googlepay) == { + 'token': 'tok_x', + 'token_data': { + 'protocol_version': 'ECv2', + 'signature': 'sig', + 'signed_message': 'msg', + 'tokenization_key': 'pk_x', + }, + } + + def test_applepay_serializes_token_data_with_header(self): + applepay = ApplePay() + token_data = ApplePayTokenData() + token_data.version = 'EC_v1' + token_data.data = 'encrypted' + token_data.signature = 'sig' + header = ApplePayTokenDataHeader() + header.ephemeral_public_key = 'key' + header.public_key_hash = 'hash' + header.transaction_id = 'txn' + token_data.header = header + applepay.token_data = token_data + + assert _serialize(applepay) == { + 'token_data': { + 'version': 'EC_v1', + 'data': 'encrypted', + 'signature': 'sig', + 'header': { + 'ephemeral_public_key': 'key', + 'public_key_hash': 'hash', + 'transaction_id': 'txn', + }, + } + } + + def test_card_serializes_writable_fields_and_account_holder(self): + card = Card() + card.number = '4242424242424242' + card.expiry_month = 12 + card.expiry_year = 2030 + card.name = 'John Smith' + card.cvv = '100' + + holder = PaymentSetupAccountHolder() + holder.type = PaymentSetupAccountHolderType.INDIVIDUAL + holder.first_name = 'John' + holder.last_name = 'Smith' + card.account_holder = holder + + assert _serialize(card) == { + 'number': '4242424242424242', + 'expiry_month': 12, + 'expiry_year': 2030, + 'name': 'John Smith', + 'cvv': '100', + 'account_holder': {'type': 'individual', 'first_name': 'John', 'last_name': 'Smith'}, + } + + def test_instrument_serializes_id_and_phone(self): + instrument = PaymentSetupInstrument() + instrument.id = 'src_wmlfc3zttb4uzmk6snpwb43jbi' + instrument.allow_update = True + phone = Phone() + phone.country_code = '+44' + phone.number = '207 946 0000' + instrument.phone = phone + + assert _serialize(instrument) == { + 'id': 'src_wmlfc3zttb4uzmk6snpwb43jbi', + 'allow_update': True, + 'phone': {'country_code': '+44', 'number': '207 946 0000'}, + } + + def test_payment_methods_container_serializes_only_set_methods(self): + payment_methods = PaymentMethods() + ideal = Ideal() + ideal.description = 'order' + payment_methods.ideal = ideal + + assert _serialize(payment_methods) == {'ideal': {'description': 'order'}}