From 3ad942388ecee9b9b94c5c8d3aefba471c0bcd4f Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 16 Apr 2026 16:43:46 +0200 Subject: [PATCH 01/16] - Accounts client update (members, reserve-rules) + integration and unit tests - cursor rules gitignore --- .gitignore | 5 +- checkout_sdk/accounts/accounts.py | 19 ++ checkout_sdk/accounts/accounts_client.py | 41 +++- checkout_sdk/api_client.py | 19 +- tests/accounts/accounts_client_test.py | 27 ++- tests/accounts/accounts_integration_test.py | 199 ++++++++++++++++++-- 6 files changed, 284 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index a951e551..be5b848e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ venv/ dist build checkout_sdk.egg-info -htmlcov \ No newline at end of file +htmlcov +.cursor/rules/ +.cursor/skills/ +.vscode/settings.json diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 82caf521..3e609d88 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -382,3 +382,22 @@ class UpdateScheduleRequest: class PaymentInstrumentsQuery: status: str + + +class ReserveRuleType(str, Enum): + ROLLING = 'rolling' + + +class ReserveRuleRequest: + type: ReserveRuleType + rolling: RollingReserveRule + valid_from: str + + +class RollingReserveRule: + percentage: float + holding_duration: HoldingDuration + + +class HoldingDuration: + weeks: int # Single integer, not list (matches C# structure) diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 86b6bd84..75ae62b6 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -3,7 +3,7 @@ from warnings import warn from checkout_sdk.accounts.accounts import OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest from checkout_sdk.api_client import ApiClient from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration @@ -19,6 +19,8 @@ class AccountsClient(Client): __FILES_PATH = 'files' __PAYOUT_SCHEDULES_PATH = 'payout-schedules' __PAYMENT_INSTRUMENTS_PATH = 'payment-instruments' + __MEMBERS_PATH = 'members' + __RESERVE_RULES_PATH = 'reserve-rules' def __init__(self, api_client: ApiClient, files_client: ApiClient, @@ -106,3 +108,40 @@ def update_payout_schedule(self, entity_id: str, currency: Currency, self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__PAYOUT_SCHEDULES_PATH), self._sdk_authorization(), {currency: update_schedule_request}) + + def get_sub_entity_members(self, entity_id: str): + return self._api_client.get( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__MEMBERS_PATH), + self._sdk_authorization()) + + def reinvite_sub_entity_member(self, entity_id: str, user_id: str): + return self._api_client.put( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__MEMBERS_PATH, user_id), + self._sdk_authorization()) + + def create_reserve_rule(self, entity_id: str, create_request: ReserveRuleRequest): + return self._api_client.post( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH), + self._sdk_authorization(), + create_request) + + def get_reserve_rules(self, entity_id: str): + return self._api_client.get( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH), + self._sdk_authorization()) + + def get_reserve_rule_details(self, entity_id: str, reserve_rule_id: str): + return self._api_client.get( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), + self._sdk_authorization()) + + def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, update_request: ReserveRuleRequest): + headers = None + if(etag is not None): + headers = {'If-Match': etag} + + return self._api_client.put( + self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), + self._sdk_authorization(), + update_request, + headers=headers) \ No newline at end of file diff --git a/checkout_sdk/api_client.py b/checkout_sdk/api_client.py index 34a6fb92..02c2c10b 100644 --- a/checkout_sdk/api_client.py +++ b/checkout_sdk/api_client.py @@ -50,8 +50,9 @@ def post(self, def put(self, path, authorization: SdkAuthorization, - request=None): - return self.invoke(method='PUT', path=path, authorization=authorization, body=request) + request=None, + headers=None): + return self.invoke(method='PUT', path=path, authorization=authorization, body=request, headers=headers) def patch(self, path, @@ -80,16 +81,20 @@ def invoke(self, idempotency_key: str = None, params=None, file_request: FileRequest = None, - multipart_file=None): + multipart_file=None, + headers=None): - headers = { + request_headers = { 'User-Agent': 'checkout-sdk-python/' + VERSION, 'Accept': 'application/json', 'Authorization': authorization.get_authorization_header(), 'Content-Type': 'application/json'} if idempotency_key is not None: - headers['Cko-Idempotency-Key'] = idempotency_key + request_headers['Cko-Idempotency-Key'] = idempotency_key + + if headers is not None: + request_headers.update(headers) base_uri = self._base_uri + path @@ -103,14 +108,14 @@ def invoke(self, elif params is not None: params_dict = json.loads(json.dumps(params, cls=JsonSerializer)) elif file_request is not None: - headers.pop('Content-Type') + request_headers.pop('Content-Type') files, json_body = get_file_request(file_request, multipart_file) self._logger.info(method + ' ' + path) response = self._http_client.request(method=method, url=base_uri, - headers=headers, + headers=request_headers, params=params_dict, data=json_body, files=files) diff --git a/tests/accounts/accounts_client_test.py b/tests/accounts/accounts_client_test.py index adaf7769..356d7ee1 100644 --- a/tests/accounts/accounts_client_test.py +++ b/tests/accounts/accounts_client_test.py @@ -1,7 +1,7 @@ import pytest from checkout_sdk.accounts.accounts import OnboardEntityRequest, AccountsPaymentInstrument, UpdateScheduleRequest, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest from checkout_sdk.accounts.accounts_client import AccountsClient from checkout_sdk.common.enums import Currency from checkout_sdk.files.files import FileRequest @@ -60,3 +60,28 @@ def test_should_update_payout_schedule(self, mocker, client: AccountsClient): def test_should_retrieve_payout_schedule(self, mocker, client: AccountsClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.retrieve_payout_schedule('entity_id') == 'response' + + def test_should_get_sub_entity_members(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_sub_entity_members('entity_id') == 'response' + + def test_should_reinvite_sub_entity_member(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.put', return_value='response') + assert client.reinvite_sub_entity_member('entity_id', 'user_id') == 'response' + + def test_should_create_reserve_rule(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_reserve_rule('entity_id', ReserveRuleRequest()) == 'response' + + def test_should_get_reserve_rules(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_reserve_rules('entity_id') == 'response' + + def test_should_get_reserve_rule_details(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_reserve_rule_details('entity_id', 'reserve_rule_id') == 'response' + + def test_should_update_reserve_rule(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.put', return_value='response') + assert client.update_reserve_rule('entity_id', 'reserve_rule_id', 'etag_value', ReserveRuleRequest()) == 'response' + diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index 9c1d623f..8276c6b1 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -1,13 +1,15 @@ from __future__ import absolute_import import os +from datetime import datetime, timedelta, timezone import pytest from checkout_sdk import CheckoutSdk from checkout_sdk.accounts.accounts import OnboardEntityRequest, ContactDetails, Profile, Individual, \ DateOfBirth, Identification, EntityEmailAddresses, Company, EntityRepresentative, PaymentInstrumentRequest, \ - InstrumentDocument, InstrumentDetailsFasterPayments + InstrumentDocument, InstrumentDetailsFasterPayments, ReserveRuleRequest, RollingReserveRule, \ + HoldingDuration from checkout_sdk.checkout_api import CheckoutApi from checkout_sdk.common.enums import Currency, Country, InstrumentType from checkout_sdk.files.files import FileRequest @@ -26,16 +28,7 @@ def accounts_checkout_api(): .build() -def upload_file(oauth_api: CheckoutApi): - request = FileRequest() - request.file = os.path.join(get_project_root(), 'tests', 'resources', 'checkout.jpeg') - request.purpose = 'bank_verification' - response = oauth_api.accounts.upload_file(request) - assert_response(response, 'id', '_links') - return response - - -def test_should_create_get_and_update_onboard_entity(oauth_api): +def test_should_create_get_and_update_onboard_entity(accounts_checkout_api): onboard_entity_request = OnboardEntityRequest() onboard_entity_request.reference = new_uuid()[:14] email_addresses = EntityEmailAddresses() @@ -59,11 +52,11 @@ def test_should_create_get_and_update_onboard_entity(oauth_api): onboard_entity_request.individual.identification = Identification() onboard_entity_request.individual.identification.national_id_number = 'AB123456C' - create_entity_response = oauth_api.accounts.create_entity(onboard_entity_request) + create_entity_response = accounts_checkout_api.accounts.create_entity(onboard_entity_request) assert_response(create_entity_response, 'id', 'reference') - get_entity_response = oauth_api.accounts.get_entity(create_entity_response.id) + get_entity_response = accounts_checkout_api.accounts.get_entity(create_entity_response.id) assert_response(get_entity_response, 'id', @@ -80,15 +73,15 @@ def test_should_create_get_and_update_onboard_entity(oauth_api): onboard_entity_request.individual.first_name = 'John' - update_response = oauth_api.accounts.update_entity(create_entity_response.id, onboard_entity_request) + update_response = accounts_checkout_api.accounts.update_entity(create_entity_response.id, onboard_entity_request) assert_response(update_response, 'id') assert create_entity_response.id == update_response.id -def test_should_upload_file(oauth_api): - upload_file(oauth_api) +def test_should_upload_file(accounts_checkout_api): + upload_file(accounts_checkout_api) def test_should_create_and_retrieve_payment_instrument(accounts_checkout_api): @@ -150,3 +143,177 @@ def test_should_create_and_retrieve_payment_instrument(accounts_checkout_api): query_response = accounts_checkout_api.accounts.query_payment_instruments(entity_response.id) assert_response(query_response, 'data') + +def test_should_get_sub_entity_members(accounts_checkout_api): + entity_id = create_test_entity(accounts_checkout_api) + + # Get members (may be empty for new entity) + members_response = accounts_checkout_api.accounts.get_sub_entity_members(entity_id) + + # Response should have structure even if empty + assert members_response is not None + + +def test_create_reserve_rule_should_return_valid_response(accounts_checkout_api): + # Arrange + entity_id = create_test_entity(accounts_checkout_api) + reserve_rule_request = create_valid_reserve_rule_request() + + # Act + response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) + + # Assert + validate_reserve_rule_id_response(response) + + +def test_get_reserve_rules_should_return_valid_response(accounts_checkout_api): + # Arrange + entity_id = create_test_entity(accounts_checkout_api) + reserve_rule_request = create_valid_reserve_rule_request() + create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) + validate_reserve_rule_id_response(create_response) + + # Act + response = accounts_checkout_api.accounts.get_reserve_rules(entity_id) + + # Assert + validate_reserve_rules_response(response) + + +def test_get_reserve_rule_details_should_return_valid_response(accounts_checkout_api): + # Arrange + entity_id = create_test_entity(accounts_checkout_api) + reserve_rule_request = create_valid_reserve_rule_request() + create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) + validate_reserve_rule_id_response(create_response) + + # Act + response = accounts_checkout_api.accounts.get_reserve_rule_details(entity_id, create_response.id) + + # Assert + validate_reserve_rule_response(response, reserve_rule_request) + + +def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api): + # Arrange + entity_id = create_test_entity(accounts_checkout_api) + original_request = create_valid_reserve_rule_request() + create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, original_request) + validate_reserve_rule_id_response(create_response) + + update_request = create_valid_reserve_rule_request() + update_request.rolling.percentage = 15.0 + update_request.rolling.holding_duration.weeks = 16 + + # Get ETag from the creation response headers + etag = None + if hasattr(create_response, 'http_metadata') and hasattr(create_response.http_metadata, 'headers'): + headers = create_response.http_metadata.headers + if 'etag' in headers: + etag = headers['etag'] + elif 'ETag' in headers: + etag = headers['ETag'] + + # Act (will set the If-Match header when using the etag) + response = accounts_checkout_api.accounts.update_reserve_rule( + entity_id, + create_response.id, + etag, + update_request + ) + + # Assert + validate_reserve_rule_id_response(response) + assert response.id == create_response.id + + +# Common methods +def upload_file(api): + request = FileRequest() + request.file = os.path.join(get_project_root(), 'tests', 'resources', 'checkout.jpeg') + request.purpose = 'bank_verification' + response = api.accounts.upload_file(request) + assert_response(response, 'id', '_links') + return response + +def create_test_entity(api): + entity_request = OnboardEntityRequest() + entity_request.reference = new_uuid()[:15] + entity_request.contact_details = build_contact_details() + entity_request.profile = build_profile() + entity_request.company = Company() + entity_request.company.business_registration_number = '01234567' + entity_request.company.legal_name = 'Reserve Rules Test Inc.' + entity_request.company.trading_name = 'Reserve Rules Test' + entity_request.company.principal_address = address() + entity_request.company.registered_address = address() + representative = EntityRepresentative() + representative.first_name = 'John' + representative.last_name = 'Doe' + representative.address = address() + entity_request.company.representatives = [representative] + + entity_response = api.accounts.create_entity(entity_request) + assert_response(entity_response, 'id') + + return entity_response.id + + +def build_contact_details(): + contact_details = ContactDetails() + contact_details.phone = phone() + contact_details.email_addresses = EntityEmailAddresses() + contact_details.email_addresses.primary = random_email() + return contact_details + + +def build_profile(): + profile = Profile() + profile.urls = ['https://www.superheroexample.com'] + profile.mccs = ['0742'] + return profile + + +def create_valid_reserve_rule_request(): + holding_duration = HoldingDuration() + holding_duration.weeks = 8 + + rolling_rule = RollingReserveRule() + rolling_rule.percentage = 12.5 + rolling_rule.holding_duration = holding_duration + + reserve_rule_request = ReserveRuleRequest() + reserve_rule_request.type = 'rolling' + reserve_rule_request.rolling = rolling_rule + reserve_rule_request.valid_from = (datetime.now(timezone.utc) + timedelta(days=30)).isoformat() + + return reserve_rule_request + + +def validate_reserve_rule_id_response(response): + assert response is not None + assert_response(response, 'id') + assert response.id is not None + assert response.id != '' + + +def validate_reserve_rules_response(response): + assert response is not None + assert_response(response, 'data') + assert response.data is not None + assert len(response.data) > 0 + assert response.data[0].id is not None + assert hasattr(response.data[0], 'type') + + +def validate_reserve_rule_response(response, original_request): + assert response is not None + assert_response(response, 'id', 'type', 'rolling') + assert response.id is not None + assert response.type == original_request.type + assert response.rolling is not None + assert response.rolling.percentage == original_request.rolling.percentage + assert response.rolling.holding_duration is not None + assert response.rolling.holding_duration.weeks == original_request.rolling.holding_duration.weeks + assert hasattr(response, 'valid_from') + From a666bbc4c95f7ffb2595ad0324ec5eee4f89e3db Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 16 Apr 2026 17:49:08 +0200 Subject: [PATCH 02/16] AccountsClient updaye (entity files) + unit and integration tests --- checkout_sdk/accounts/accounts.py | 25 ++++++++++++++++++- checkout_sdk/accounts/accounts_client.py | 16 ++++++++++-- tests/accounts/accounts_client_test.py | 13 +++++++++- tests/accounts/accounts_integration_test.py | 27 ++++++++++++++++++++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 3e609d88..0c6b075d 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -400,4 +400,27 @@ class RollingReserveRule: class HoldingDuration: - weeks: int # Single integer, not list (matches C# structure) + weeks: int + + +class FilePurpose(str, Enum): + ADDITIONAL_DOCUMENT = 'additional_document' + ARTICLES_OF_ASSOCIATION = 'articles_of_association' + BANK_VERIFICATION = 'bank_verification' + CERTIFIED_AUTHORISED_SIGNATORY = 'certified_authorised_signatory' + COMPANY_OWNERSHIP = 'company_ownership' + IDENTIFICATION = 'identification' + IDENTITY_VERIFICATION = 'identity_verification' + DISPUTE_EVIDENCE = 'dispute_evidence' + COMPANY_VERIFICATION = 'company_verification' + FINANCIAL_VERIFICATION = 'financial_verification' + TAX_VERIFICATION = 'tax_verification' + PROOF_OF_LEGALITY = 'proof_of_legality' + PROOF_OF_PRINCIPAL_ADDRESS = 'proof_of_principal_address' + SHAREHOLDER_STRUCTURE = 'shareholder_structure' + PROOF_OF_RESIDENTIAL_ADDRESS = 'proof_of_residential_address' + PROOF_OF_REGISTRATION = 'proof_of_registration' + + +class EntityFileRequest: + purpose: FilePurpose diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 75ae62b6..75a360ae 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -3,7 +3,8 @@ from warnings import warn from checkout_sdk.accounts.accounts import OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ + EntityFileRequest from checkout_sdk.api_client import ApiClient from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration @@ -144,4 +145,15 @@ def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, u self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), self._sdk_authorization(), update_request, - headers=headers) \ No newline at end of file + headers=headers) + + 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), + self._sdk_authorization(), + entity_file_request) + + def retrieve_entity_file(self, entity_id: str, file_id: str): + return self.__files_client.get( + self.build_path(self.__ENTITIES_PATH, entity_id, self.__FILES_PATH, file_id), + self._sdk_authorization()) \ No newline at end of file diff --git a/tests/accounts/accounts_client_test.py b/tests/accounts/accounts_client_test.py index 356d7ee1..c9085a0b 100644 --- a/tests/accounts/accounts_client_test.py +++ b/tests/accounts/accounts_client_test.py @@ -1,7 +1,8 @@ import pytest from checkout_sdk.accounts.accounts import OnboardEntityRequest, AccountsPaymentInstrument, UpdateScheduleRequest, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ + EntityFileRequest, FilePurpose from checkout_sdk.accounts.accounts_client import AccountsClient from checkout_sdk.common.enums import Currency from checkout_sdk.files.files import FileRequest @@ -85,3 +86,13 @@ def test_should_update_reserve_rule(self, mocker, client: AccountsClient): mocker.patch('checkout_sdk.api_client.ApiClient.put', return_value='response') assert client.update_reserve_rule('entity_id', 'reserve_rule_id', 'etag_value', ReserveRuleRequest()) == 'response' + def test_should_upload_entity_file(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + request = EntityFileRequest() + request.purpose = FilePurpose.IDENTIFICATION + assert client.upload_entity_file('entity_id', request) == 'response' + + def test_should_retrieve_entity_file(self, mocker, client: AccountsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.retrieve_entity_file('entity_id', 'file_id') == 'response' + diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index 8276c6b1..690d73fa 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -9,7 +9,7 @@ from checkout_sdk.accounts.accounts import OnboardEntityRequest, ContactDetails, Profile, Individual, \ DateOfBirth, Identification, EntityEmailAddresses, Company, EntityRepresentative, PaymentInstrumentRequest, \ InstrumentDocument, InstrumentDetailsFasterPayments, ReserveRuleRequest, RollingReserveRule, \ - HoldingDuration + HoldingDuration, EntityFileRequest, FilePurpose from checkout_sdk.checkout_api import CheckoutApi from checkout_sdk.common.enums import Currency, Country, InstrumentType from checkout_sdk.files.files import FileRequest @@ -227,6 +227,31 @@ def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api) assert response.id == create_response.id +def test_should_upload_entity_file_and_retrieve(accounts_checkout_api): + # Arrange + entity_id = create_test_entity(accounts_checkout_api) + + # Test upload entity file + request = EntityFileRequest() + request.purpose = FilePurpose.IDENTIFICATION + + # Act - Upload file + upload_response = accounts_checkout_api.accounts.upload_entity_file(entity_id, request) + + # Assert - Upload response + assert_response(upload_response, 'id', '_links') + assert upload_response.id is not None + assert upload_response.id != '' + + # Act - Retrieve file + file_id = upload_response.id + retrieve_response = accounts_checkout_api.accounts.retrieve_entity_file(entity_id, file_id) + + # Assert - Retrieve response + assert_response(retrieve_response, 'id') + assert retrieve_response.id == file_id + + # Common methods def upload_file(api): request = FileRequest() From 62e654a9996fad9e74ab7e2bbce1fea00c70d2dd Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 17 Apr 2026 10:14:14 +0200 Subject: [PATCH 03/16] Forward client update (secrets) + unit and integration tests --- checkout_sdk/forward/forward.py | 6 ++ checkout_sdk/forward/forward_client.py | 29 ++++++++- tests/forward/forward_client_test.py | 20 +++++- tests/forward/forward_integration_test.py | 74 ++++++++++++++++++++--- 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/checkout_sdk/forward/forward.py b/checkout_sdk/forward/forward.py index d6bb831f..9021e320 100644 --- a/checkout_sdk/forward/forward.py +++ b/checkout_sdk/forward/forward.py @@ -85,3 +85,9 @@ class ForwardRequest: reference: str = None processing_channel_id: str = None network_token: NetworkToken = None + + +class SecretRequest: + name: str + value: str + entity_id: str = None diff --git a/checkout_sdk/forward/forward_client.py b/checkout_sdk/forward/forward_client.py index 1b4fb8e9..612a5ec0 100644 --- a/checkout_sdk/forward/forward_client.py +++ b/checkout_sdk/forward/forward_client.py @@ -2,11 +2,12 @@ from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration from checkout_sdk.client import Client -from checkout_sdk.forward.forward import ForwardRequest +from checkout_sdk.forward.forward import ForwardRequest, SecretRequest class ForwardClient(Client): __FORWARD_PATH = 'forward' + __SECRETS_PATH = 'secrets' def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): super().__init__(api_client=api_client, @@ -18,3 +19,29 @@ def forward_request(self, request: ForwardRequest): def get(self, request_id: str): return self._api_client.get(self.build_path(self.__FORWARD_PATH, request_id), self._sdk_authorization()) + + def create_secret(self, request: SecretRequest): + return self._api_client.post( + self.build_path(self.__FORWARD_PATH, self.__SECRETS_PATH), + self._sdk_authorization(), + request + ) + + def list_secrets(self): + return self._api_client.get( + self.build_path(self.__FORWARD_PATH, self.__SECRETS_PATH), + self._sdk_authorization() + ) + + def update_secret(self, name: str, request: SecretRequest): + return self._api_client.patch( + self.build_path(self.__FORWARD_PATH, self.__SECRETS_PATH, name), + self._sdk_authorization(), + request + ) + + def delete_secret(self, name: str): + return self._api_client.delete( + self.build_path(self.__FORWARD_PATH, self.__SECRETS_PATH, name), + self._sdk_authorization() + ) diff --git a/tests/forward/forward_client_test.py b/tests/forward/forward_client_test.py index 868ef3e4..b2673440 100644 --- a/tests/forward/forward_client_test.py +++ b/tests/forward/forward_client_test.py @@ -1,6 +1,6 @@ import pytest -from checkout_sdk.forward.forward import ForwardRequest +from checkout_sdk.forward.forward import ForwardRequest, SecretRequest from checkout_sdk.forward.forward_client import ForwardClient @@ -9,7 +9,7 @@ def client(mock_sdk_configuration, mock_api_client): return ForwardClient(api_client=mock_api_client, configuration=mock_sdk_configuration) -class TestForexClient: +class TestForwardClient: def test_should_forward_request(self, mocker, client: ForwardClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') @@ -18,3 +18,19 @@ def test_should_forward_request(self, mocker, client: ForwardClient): def test_should_get_forward_request(self, mocker, client: ForwardClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.get('forward_id') == 'response' + + def test_should_create_secret(self, mocker, client: ForwardClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_secret(SecretRequest()) == 'response' + + def test_should_list_secrets(self, mocker, client: ForwardClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.list_secrets() == 'response' + + def test_should_update_secret(self, mocker, client: ForwardClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.update_secret('secret_name', SecretRequest()) == 'response' + + def test_should_delete_secret(self, mocker, client: ForwardClient): + mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') + assert client.delete_secret('secret_name') == 'response' diff --git a/tests/forward/forward_integration_test.py b/tests/forward/forward_integration_test.py index 4349b0ba..af7b9844 100644 --- a/tests/forward/forward_integration_test.py +++ b/tests/forward/forward_integration_test.py @@ -1,35 +1,36 @@ from __future__ import absolute_import import pytest +import uuid from checkout_sdk.forward.forward import ForwardRequest, IdSource, NetworkToken, MethodType, DestinationRequest, \ - Headers, DlocalSignature, DlocalParameters + Headers, DlocalSignature, DlocalParameters, SecretRequest from tests.checkout_test_utils import assert_response @pytest.mark.skip(reason='This test requires a valid id or Token source') def test_should_forward_and_get_request(default_api): - id_source = IdSource + id_source = IdSource() id_source.id = 'src_v5rgkf3gdtpuzjqesyxmyodnya' - network_token = NetworkToken + network_token = NetworkToken() network_token.enabled = True network_token.request_cryptogram = False - headers = Headers + headers = Headers() headers.encrypted = '' headers.raw = { 'Idempotency-Key': 'xe4fad12367dfgrds', 'Content-Type': 'application/json', } - dlocal_parameters = DlocalParameters + dlocal_parameters = DlocalParameters() dlocal_parameters.secret_key = '9f439fe1a9f96e67b047d3c1a28c33a2e' - signature = DlocalSignature + signature = DlocalSignature() signature.dlocal_parameters = dlocal_parameters - destination_request = DestinationRequest + destination_request = DestinationRequest() destination_request.url = 'https://example.com/forward' destination_request.method = MethodType.POST destination_request.headers = headers @@ -41,7 +42,7 @@ def test_should_forward_and_get_request(default_api): '"merchant_initiated": true}') destination_request.signature = signature - forward_request = ForwardRequest + forward_request = ForwardRequest() forward_request.source = id_source forward_request.reference = 'ORD-5023-4E89' forward_request.processing_channel_id = 'pc_azsiyswl7bwe2ynjzujy7lcjca' @@ -53,3 +54,60 @@ def test_should_forward_and_get_request(default_api): get_response = default_api.forward.get_forward_request(response['request_id']) assert_response(get_response, 'request_id', 'destination_request', 'destination_response') + + +def build_create_secret_request(name: str = None, value: str = "secret_value", entity_id: str = None) -> SecretRequest: + request = SecretRequest() + request.name = name or f"secret_{str(uuid.uuid4()).replace('-', '')[:16]}" + request.value = value + request.entity_id = entity_id + return request + + +def build_update_secret_request(value: str = "updated_value", entity_id: str = None) -> SecretRequest: + request = SecretRequest() + request.value = value + request.entity_id = entity_id + return request + + +def assert_secret_response(response, expected_name: str = None): + assert_response(response, 'name', 'created_at', 'updated_at', 'version') + if expected_name: + assert response['name'] == expected_name + + +@pytest.mark.skip(reason='This test requires forward secrets scopes and valid credentials') +def test_should_create_list_update_delete_secret(default_api): + # Create secret + create_request = build_create_secret_request(value="initial_value") + secret_name = create_request.name + + create_response = default_api.forward.create_secret(create_request) + assert_secret_response(create_response, secret_name) + + # List secrets - should contain our secret + list_response = default_api.forward.list_secrets() + assert_response(list_response, 'data') + assert any(secret['name'] == secret_name for secret in list_response['data']) + + # Update secret + update_request = build_update_secret_request(value="new_updated_value") + update_response = default_api.forward.update_secret(secret_name, update_request) + assert_secret_response(update_response, secret_name) + + # Delete secret + delete_response = default_api.forward.delete_secret(secret_name) + # Delete should return empty response (204 No Content) + assert delete_response is not None + + +@pytest.mark.skip(reason='This test requires forward secrets scopes and valid credentials') +def test_should_create_secret_with_entity_id(default_api): + create_request = build_create_secret_request(entity_id="ent_test123") + + create_response = default_api.forward.create_secret(create_request) + assert_secret_response(create_response, create_request.name) + + # Cleanup + default_api.forward.delete_secret(create_request.name) From 9725ad269b78f63ed1dd44160c96dc2fe2ebbbb5 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 17 Apr 2026 13:35:53 +0200 Subject: [PATCH 04/16] - Custom headers for python - AgenticCommerceClient + unit and integration tests - ComplianceRequestsClient + unit and integration tests --- .../agenticcommerce/agentic_commerce.py | 92 +++++++++ .../agentic_commerce_client.py | 25 +++ checkout_sdk/api_client.py | 44 ++++- checkout_sdk/checkout_api.py | 4 + checkout_sdk/compliancerequests/__init__.py | 0 .../compliancerequests/compliance_requests.py | 19 ++ .../compliance_requests_client.py | 24 +++ .../agentic_commerce_client_test.py | 57 ++++++ .../agentic_commerce_integration_test.py | 180 ++++++++++++++++++ .../compliance_requests_client_test.py | 63 ++++++ .../compliance_requests_integration_test.py | 166 ++++++++++++++++ 11 files changed, 668 insertions(+), 6 deletions(-) create mode 100644 checkout_sdk/agenticcommerce/agentic_commerce.py create mode 100644 checkout_sdk/agenticcommerce/agentic_commerce_client.py create mode 100644 checkout_sdk/compliancerequests/__init__.py create mode 100644 checkout_sdk/compliancerequests/compliance_requests.py create mode 100644 checkout_sdk/compliancerequests/compliance_requests_client.py create mode 100644 tests/agenticcommerce/agentic_commerce_client_test.py create mode 100644 tests/agenticcommerce/agentic_commerce_integration_test.py create mode 100644 tests/compliancerequests/compliance_requests_client_test.py create mode 100644 tests/compliancerequests/compliance_requests_integration_test.py diff --git a/checkout_sdk/agenticcommerce/agentic_commerce.py b/checkout_sdk/agenticcommerce/agentic_commerce.py new file mode 100644 index 00000000..65dd0bbc --- /dev/null +++ b/checkout_sdk/agenticcommerce/agentic_commerce.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import + +from enum import Enum +from typing import List, Dict +from datetime import datetime + +from checkout_sdk.common.enums import Country, Currency + + +class DelegatedPaymentMethodType(str, Enum): + CARD = 'card' + + +class DelegatedCardNumberType(str, Enum): + FPAN = 'fpan' + NETWORK_TOKEN = 'network_token' + + +class DelegatedPaymentAllowanceReason(str, Enum): + ONE_TIME = 'one_time' + + +class DelegatedCardFundingType(str, Enum): + CREDIT = 'credit' + DEBIT = 'debit' + PREPAID = 'prepaid' + + +class DelegatedPaymentMethodCard: + type: DelegatedPaymentMethodType + card_number_type: DelegatedCardNumberType + number: str + exp_month: str = None + exp_year: str = None + name: str = None + cvc: str = None + cryptogram: str = None + eci_value: str = None + checks_performed: List[str] = None + iin: str = None + display_card_funding_type: DelegatedCardFundingType = None + display_wallet_type: str = None + display_brand: str = None + display_last4: str = None + metadata: Dict[str, str] = None + + def __init__(self): + self.type = DelegatedPaymentMethodType.CARD + + +class DelegatedPaymentAllowance: + reason: DelegatedPaymentAllowanceReason + max_amount: int + currency: Currency + merchant_id: str + checkout_session_id: str + expires_at: datetime + + +class DelegatedPaymentBillingAddress: + name: str + line_one: str + line_two: str = None + city: str + state: str = None + postal_code: str + country: Country + + +class DelegatedPaymentRiskSignal: + type: str + score: int + action: str + + +class DelegatedPaymentRequest: + payment_method: DelegatedPaymentMethodCard + allowance: DelegatedPaymentAllowance + billing_address: DelegatedPaymentBillingAddress = None + risk_signals: List[DelegatedPaymentRiskSignal] + metadata: Dict[str, str] + + +class DelegatedPaymentHeaders: + signature: str + timestamp: str + api_version: str + + def get_header_mappings(self) -> Dict[str, str]: + return { + 'api_version': 'API-Version' + } \ No newline at end of file diff --git a/checkout_sdk/agenticcommerce/agentic_commerce_client.py b/checkout_sdk/agenticcommerce/agentic_commerce_client.py new file mode 100644 index 00000000..93e78861 --- /dev/null +++ b/checkout_sdk/agenticcommerce/agentic_commerce_client.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import + +from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest, DelegatedPaymentHeaders +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 + + +class AgenticCommerceClient(Client): + __AGENTIC_COMMERCE_PATH = 'agentic_commerce' + __DELEGATE_PAYMENT_PATH = 'delegate_payment' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY) + + def create_delegated_payment_token(self, request: DelegatedPaymentRequest, headers: DelegatedPaymentHeaders): + return self._api_client.post( + self.build_path(self.__AGENTIC_COMMERCE_PATH, self.__DELEGATE_PAYMENT_PATH), + self._sdk_authorization(), + request, + headers = headers + ) \ No newline at end of file diff --git a/checkout_sdk/api_client.py b/checkout_sdk/api_client.py index 02c2c10b..db98d13c 100644 --- a/checkout_sdk/api_client.py +++ b/checkout_sdk/api_client.py @@ -32,7 +32,6 @@ class ApiClient: def __init__(self, configuration: CheckoutConfiguration, base_uri: str): self._http_client = configuration.http_client self._base_uri = base_uri - def get(self, path, authorization: SdkAuthorization, @@ -43,9 +42,10 @@ def post(self, path, authorization: SdkAuthorization, request=None, - idempotency_key: str = None): + idempotency_key: str = None, + headers=None): return self.invoke(method='POST', path=path, authorization=authorization, body=request, - idempotency_key=idempotency_key) + idempotency_key=idempotency_key, headers=headers) def put(self, path, @@ -57,8 +57,9 @@ def put(self, def patch(self, path, authorization: SdkAuthorization, - request=None): - return self.invoke(method='PATCH', path=path, authorization=authorization, body=request) + request=None, + headers=None): + return self.invoke(method='PATCH', path=path, authorization=authorization, body=request, headers=headers) def delete(self, path, @@ -94,7 +95,8 @@ def invoke(self, request_headers['Cko-Idempotency-Key'] = idempotency_key if headers is not None: - request_headers.update(headers) + custom_headers = self._process_custom_headers(headers) + request_headers.update(custom_headers) base_uri = self._base_uri + path @@ -137,3 +139,33 @@ def invoke(self, return ResponseWrapper(http_metadata, contents) else: return ResponseWrapper(http_metadata) + + def _process_custom_headers(self, custom_headers): + headers = {} + if custom_headers is None: + return None + + # Get custom mappings if the class defines them + if hasattr(custom_headers, 'get_header_mappings'): + custom_mappings = custom_headers.get_header_mappings() + + # Iterate through all attributes + for attr_name in dir(custom_headers): + # Skip private attributes and methods + if attr_name.startswith('_') or callable(getattr(custom_headers, attr_name)): + continue + + value = getattr(custom_headers, attr_name) + if value is not None and value != '': + # Use custom mapping if available, otherwise convert using default logic + header_name = custom_mappings.get(attr_name, self._convert_property_to_header(attr_name)) + headers[header_name] = str(value) + else: + headers = custom_headers + + return headers + + def _convert_property_to_header(self, property_name): + # Convert snake_case to Title-Case (e.g., 'api_version' -> 'Api-Version') + return '-'.join(word.capitalize() for word in property_name.split('_')) + diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 2d17b900..11b010a0 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -4,6 +4,7 @@ from checkout_sdk.api_client import ApiClient from checkout_sdk.balances.balances_client import BalancesClient from checkout_sdk.checkout_configuration import CheckoutConfiguration +from checkout_sdk.compliancerequests.compliance_requests_client import ComplianceRequestsClient from checkout_sdk.customers.customers_client import CustomersClient from checkout_sdk.disputes.disputes_client import DisputesClient from checkout_sdk.financial.financial_client import FinancialClient @@ -25,6 +26,7 @@ from checkout_sdk.metadata.metadata_client import CardMetadataClient from checkout_sdk.forward.forward_client import ForwardClient from checkout_sdk.payments.setups.setups_client import PaymentSetupsClient +from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient def _base_api_client(configuration: CheckoutConfiguration) -> ApiClient: @@ -56,6 +58,7 @@ def __init__(self, configuration: CheckoutConfiguration): super().__init__(base_api_client, configuration) self.tokens = TokensClient(api_client=base_api_client, configuration=configuration) self.customers = CustomersClient(api_client=base_api_client, configuration=configuration) + self.compliance_requests = ComplianceRequestsClient(api_client=base_api_client, configuration=configuration) self.instruments = InstrumentsClient(api_client=base_api_client, configuration=configuration) self.payments = PaymentsClient(api_client=base_api_client, configuration=configuration) self.sessions = SessionsClient(api_client=base_api_client, configuration=configuration) @@ -78,3 +81,4 @@ def __init__(self, configuration: CheckoutConfiguration): self.payment_sessions = PaymentSessionsClient(api_client=base_api_client, configuration=configuration) self.forward = ForwardClient(api_client=base_api_client, configuration=configuration) self.setups = PaymentSetupsClient(api_client=base_api_client, configuration=configuration) + self.agentic_commerce = AgenticCommerceClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/compliancerequests/__init__.py b/checkout_sdk/compliancerequests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/compliancerequests/compliance_requests.py b/checkout_sdk/compliancerequests/compliance_requests.py new file mode 100644 index 00000000..efb141e7 --- /dev/null +++ b/checkout_sdk/compliancerequests/compliance_requests.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import + +from typing import List + + +class ComplianceRespondedField: + name: str + value: str + not_available: bool + + +class ComplianceRespondedFields: + sender: List[ComplianceRespondedField] + recipient: List[ComplianceRespondedField] + + +class ComplianceRequestRespondRequest: + fields: ComplianceRespondedFields + comments: str \ No newline at end of file diff --git a/checkout_sdk/compliancerequests/compliance_requests_client.py b/checkout_sdk/compliancerequests/compliance_requests_client.py new file mode 100644 index 00000000..23ac9330 --- /dev/null +++ b/checkout_sdk/compliancerequests/compliance_requests_client.py @@ -0,0 +1,24 @@ +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.compliancerequests.compliance_requests import ComplianceRequestRespondRequest + + +class ComplianceRequestsClient(Client): + __COMPLIANCE_REQUESTS_PATH = 'compliance-requests' + + 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_compliance_request(self, payment_id: str): + return self._api_client.get(self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), + self._sdk_authorization()) + + def respond_to_compliance_request(self, payment_id: str, request: ComplianceRequestRespondRequest): + return self._api_client.post(self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), + self._sdk_authorization(), request) \ No newline at end of file diff --git a/tests/agenticcommerce/agentic_commerce_client_test.py b/tests/agenticcommerce/agentic_commerce_client_test.py new file mode 100644 index 00000000..63618ede --- /dev/null +++ b/tests/agenticcommerce/agentic_commerce_client_test.py @@ -0,0 +1,57 @@ +import pytest + +from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient +from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest, DelegatedPaymentHeaders + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return AgenticCommerceClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestAgenticCommerceClient: + + def test_create_delegated_payment_token(self, mocker, client: AgenticCommerceClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + request = DelegatedPaymentRequest() + headers = DelegatedPaymentHeaders() + + response = client.create_delegated_payment_token(request, headers) + + assert response == 'response' + + def test_create_delegated_payment_token_with_none_request(self, mocker, client: AgenticCommerceClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + headers = DelegatedPaymentHeaders() + + response = client.create_delegated_payment_token(None, headers) + + assert response == 'response' + # Verify None was passed as the request parameter + mock_post.assert_called_once() + args = mock_post.call_args[0] + assert args[2] is None # request parameter position + + def test_create_delegated_payment_token_with_none_headers(self, mocker, client: AgenticCommerceClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + request = DelegatedPaymentRequest() + + response = client.create_delegated_payment_token(request, None) + + assert response == 'response' + # Verify None was passed as the headers parameter + mock_post.assert_called_once() + kwargs = mock_post.call_args.kwargs + assert kwargs['headers'] is None # headers parameter is passed as keyword argument + + def test_create_delegated_payment_token_calls_correct_endpoint(self, mocker, client: AgenticCommerceClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + request = DelegatedPaymentRequest() + headers = DelegatedPaymentHeaders() + + client.create_delegated_payment_token(request, headers) + + # Verify the correct endpoint path was called + mock_post.assert_called_once() + args = mock_post.call_args[0] + assert 'agentic_commerce/delegate_payment' in args[0] # Path argument \ No newline at end of file diff --git a/tests/agenticcommerce/agentic_commerce_integration_test.py b/tests/agenticcommerce/agentic_commerce_integration_test.py new file mode 100644 index 00000000..79762e39 --- /dev/null +++ b/tests/agenticcommerce/agentic_commerce_integration_test.py @@ -0,0 +1,180 @@ +from __future__ import absolute_import + +import pytest + +from datetime import datetime, timezone, timedelta +from checkout_sdk.agenticcommerce.agentic_commerce import ( + DelegatedPaymentRequest, DelegatedPaymentHeaders, DelegatedPaymentMethodCard, + DelegatedPaymentAllowance, DelegatedPaymentBillingAddress, DelegatedPaymentRiskSignal, + DelegatedCardNumberType, DelegatedPaymentAllowanceReason +) +from checkout_sdk.common.enums import Currency, Country +from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest, DelegatedPaymentHeaders +from checkout_sdk.exception import CheckoutApiException +from tests.checkout_test_utils import assert_response + + +@pytest.mark.skip(reason="Requires a valid HMAC signing key and merchant enabled for agentic commerce") +def test_should_create_delegated_payment_token(default_api): + request = build_valid_delegated_payment_request() + headers = build_valid_delegated_payment_headers() + + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) + + assert_delegated_payment_token_response(response) + assert_response(response, + 'id', + 'created', + 'metadata') + + +@pytest.mark.skip(reason="Requires a valid HMAC signing key and merchant enabled for agentic commerce") +def test_should_create_delegated_payment_token_with_billing_address(default_api): + request = build_valid_delegated_payment_request_with_billing_address() + headers = build_valid_delegated_payment_headers() + + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) + + assert_delegated_payment_token_response(response) + assert_response(response, + 'id', + 'created', + 'metadata') + + +@pytest.mark.skip(reason="Requires a valid HMAC signing key and merchant enabled for agentic commerce") +def test_should_create_delegated_payment_token_with_network_token(default_api): + request = build_valid_delegated_payment_request_with_network_token() + headers = build_valid_delegated_payment_headers() + + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) + + assert_delegated_payment_token_response(response) + assert_response(response, + 'id', + 'created', + 'metadata') + + +def test_should_fail_create_delegated_payment_token_with_invalid_request(default_api): + # Build request with missing required fields + from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest + invalid_request = DelegatedPaymentRequest() + headers = build_valid_delegated_payment_headers() + + with pytest.raises(CheckoutApiException) as exc_info: + default_api.agentic_commerce.create_delegated_payment_token(invalid_request, headers) + + # Should get a validation error from the API + assert exc_info.value.http_metadata.status_code in [400, 422] + + +def test_should_fail_create_delegated_payment_token_with_invalid_signature(default_api): + request = build_valid_delegated_payment_request() + + # Build headers with invalid signature + from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentHeaders + invalid_headers = DelegatedPaymentHeaders() + invalid_headers.signature = "invalid-signature" + invalid_headers.timestamp = "2026-03-11T10:30:00Z" + + with pytest.raises(CheckoutApiException) as exc_info: + default_api.agentic_commerce.create_delegated_payment_token(request, invalid_headers) + + # Should get an authentication error + assert exc_info.value.http_metadata.status_code in [401, 403] + + + # Common methods +def build_valid_delegated_payment_request(): + # Payment Method (Card) + payment_method = DelegatedPaymentMethodCard() + payment_method.card_number_type = DelegatedCardNumberType.FPAN + payment_method.number = "4242424242424242" + payment_method.exp_month = "11" + payment_method.exp_year = "2026" + payment_method.metadata = {"issuing_bank": "test"} + + # Allowance + allowance = DelegatedPaymentAllowance() + allowance.reason = DelegatedPaymentAllowanceReason.ONE_TIME + allowance.max_amount = 10000 + allowance.currency = Currency.USD + allowance.merchant_id = "cli_vkuhvk4vjn2edkps7dfsq6emqm" + allowance.checkout_session_id = "1PQrsT" + # Set expires_at to 1 hour from now (datetime object) + allowance.expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + + # Risk Signals + risk_signal = DelegatedPaymentRiskSignal() + risk_signal.type = "card_testing" + risk_signal.score = 10 + risk_signal.action = "blocked" + + # Build the request + request = DelegatedPaymentRequest() + request.payment_method = payment_method + request.allowance = allowance + request.risk_signals = [risk_signal] + request.metadata = {"campaign": "q4"} + + return request + + +def build_valid_delegated_payment_request_with_billing_address(): + request = build_valid_delegated_payment_request() + + billing_address = DelegatedPaymentBillingAddress() + billing_address.name = "John Doe" + billing_address.line_one = "123 Test Street" + billing_address.city = "London" + billing_address.postal_code = "SW1A 1AA" + billing_address.country = Country.GB + + request.billing_address = billing_address + + return request + + +def build_valid_delegated_payment_request_with_network_token(): + request = build_valid_delegated_payment_request() + + # Change to network token type + request.payment_method.card_number_type = DelegatedCardNumberType.NETWORK_TOKEN + request.payment_method.number = "4111111111111111" # Network token placeholder + + return request + + +def build_valid_delegated_payment_headers(): + headers = DelegatedPaymentHeaders() + headers.signature = "eyJtZX..." # Base64 HMAC signature placeholder + headers.timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + headers.api_version = "2026-01-01" + return headers + + +def build_delegated_payment_response_mock(): + return { + 'id': 'vt_abc123def456ghi789', + 'created': '2026-03-11T10:30:00Z', + 'metadata': {'psp': 'checkout.com'} + } + + +def assert_delegated_payment_response(response): + assert_response(response, + 'id', + 'created', + 'metadata') + + +def assert_delegated_payment_token_response(response): + assert response is not None + assert hasattr(response, 'id') + assert response['id'] is not None + assert response['id'].startswith('vt_') + assert hasattr(response, 'created') + assert response['created'] is not None + assert hasattr(response, 'metadata') + assert response['metadata'] is not None \ No newline at end of file diff --git a/tests/compliancerequests/compliance_requests_client_test.py b/tests/compliancerequests/compliance_requests_client_test.py new file mode 100644 index 00000000..eb2f2a20 --- /dev/null +++ b/tests/compliancerequests/compliance_requests_client_test.py @@ -0,0 +1,63 @@ +import pytest + +from checkout_sdk.compliancerequests.compliance_requests_client import ComplianceRequestsClient +from checkout_sdk.compliancerequests.compliance_requests import ComplianceRequestRespondRequest + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return ComplianceRequestsClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestComplianceRequestsClient: + + def test_get_compliance_request(self, mocker, client: ComplianceRequestsClient): + mock_get = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" + + response = client.get_compliance_request(payment_id) + + assert response == 'response' + # Verify the correct endpoint path was called + mock_get.assert_called_once() + args = mock_get.call_args[0] + assert f'compliance-requests/{payment_id}' in args[0] # Path argument + + def test_get_compliance_request_with_none_payment_id(self, mocker, client: ComplianceRequestsClient): + mock_get = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + + with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): + client.get_compliance_request(None) + + def test_respond_to_compliance_request(self, mocker, client: ComplianceRequestsClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" + request = ComplianceRequestRespondRequest() + + response = client.respond_to_compliance_request(payment_id, request) + + assert response == 'response' + # Verify the correct endpoint path was called + mock_post.assert_called_once() + args = mock_post.call_args[0] + assert f'compliance-requests/{payment_id}' in args[0] # Path argument + assert args[2] == request # Request parameter + + def test_respond_to_compliance_request_with_none_payment_id(self, mocker, client: ComplianceRequestsClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + request = ComplianceRequestRespondRequest() + + with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): + client.respond_to_compliance_request(None, request) + + def test_respond_to_compliance_request_with_none_request(self, mocker, client: ComplianceRequestsClient): + mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" + + response = client.respond_to_compliance_request(payment_id, None) + + assert response == 'response' + # Verify None was passed as the request parameter + mock_post.assert_called_once() + args = mock_post.call_args[0] + assert args[2] is None # Request parameter \ No newline at end of file diff --git a/tests/compliancerequests/compliance_requests_integration_test.py b/tests/compliancerequests/compliance_requests_integration_test.py new file mode 100644 index 00000000..91ae4bbe --- /dev/null +++ b/tests/compliancerequests/compliance_requests_integration_test.py @@ -0,0 +1,166 @@ +from __future__ import absolute_import + +import pytest + +from checkout_sdk.exception import CheckoutApiException +from checkout_sdk.compliancerequests.compliance_requests import ( + ComplianceRequestRespondRequest, + ComplianceRespondedFields, + ComplianceRespondedField +) +from tests.checkout_test_utils import assert_response + + +@pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") +def test_should_get_compliance_request(default_api): + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID + + response = default_api.compliance_requests.get_compliance_request(payment_id) + + assert_compliance_request_response(response) + assert_response(response, + 'payment_id', + 'status', + 'amount', + 'currency') + assert response['payment_id'] == payment_id + + +@pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") +def test_should_respond_to_compliance_request(default_api): + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID + request = build_valid_respond_request() + + response = default_api.compliance_requests.respond_to_compliance_request(payment_id, request) + + assert_compliance_respond_response(response) + # Typically returns empty response (204 No Content) + + +@pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") +def test_should_respond_to_compliance_request_with_not_available_fields(default_api): + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID + request = build_valid_respond_request_with_not_available_field() + + response = default_api.compliance_requests.respond_to_compliance_request(payment_id, request) + + assert_compliance_respond_response(response) + + +def test_should_fail_get_compliance_request_with_invalid_payment_id(default_api): + invalid_payment_id = "pay_invalid_payment_id" + + with pytest.raises(CheckoutApiException) as exc_info: + default_api.compliance_requests.get_compliance_request(invalid_payment_id) + + # Should get a not found error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [401, 404, 422] + + +def test_should_fail_respond_to_compliance_request_with_invalid_payment_id(default_api): + invalid_payment_id = "pay_invalid_payment_id" + request = build_valid_respond_request() + + with pytest.raises(CheckoutApiException) as exc_info: + default_api.compliance_requests.respond_to_compliance_request(invalid_payment_id, request) + + # Should get a not found or validation error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [401, 404, 422] + + +def test_should_fail_respond_to_compliance_request_with_empty_request(default_api): + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" + from checkout_sdk.compliancerequests.compliance_requests import ComplianceRequestRespondRequest + empty_request = ComplianceRequestRespondRequest() + + with pytest.raises(CheckoutApiException) as exc_info: + default_api.compliance_requests.respond_to_compliance_request(payment_id, empty_request) + + # Should get a validation error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [400, 401, 422] + +# Common methods +def build_valid_respond_request(): + """Build a valid ComplianceRequestRespondRequest following the C# test structure.""" + + # Create sender field + sender_field = ComplianceRespondedField() + sender_field.name = "date_of_birth" + sender_field.value = "2000-01-01" + sender_field.not_available = False + + # Create recipient field + recipient_field = ComplianceRespondedField() + recipient_field.name = "full_name" + recipient_field.value = "John Doe" + recipient_field.not_available = False + + # Create responded fields + responded_fields = ComplianceRespondedFields() + responded_fields.sender = [sender_field] + responded_fields.recipient = [recipient_field] + + # Build the request + request = ComplianceRequestRespondRequest() + request.fields = responded_fields + request.comments = "Providing the requested compliance information" + + return request + + +def build_valid_respond_request_with_not_available_field(): + # Create sender field with not_available = True + sender_field = ComplianceRespondedField() + sender_field.name = "social_security_number" + sender_field.value = None + sender_field.not_available = True + + # Create responded fields + responded_fields = ComplianceRespondedFields() + responded_fields.sender = [sender_field] + responded_fields.recipient = [] + + # Build the request + request = ComplianceRequestRespondRequest() + request.fields = responded_fields + request.comments = "Some fields are not available for compliance reasons" + + return request + + +def build_get_compliance_request_response_mock(): + return { + 'payment_id': 'pay_fun26akvvjjerahhctaq2uzhu4', + 'status': 'pending', + 'amount': '38.23', + 'currency': 'HKD', + 'created_at': '2026-03-11T10:30:00Z', + 'request_id': 'req_abc123def456', + 'fields_required': { + 'sender': ['date_of_birth', 'full_name'], + 'recipient': ['full_name', 'address'] + } + } + + +def build_respond_to_compliance_request_response_mock(): + return { + 'http_metadata': { + 'status_code': 204 + } + } + + +def assert_compliance_request_response(response): + assert_response(response, + 'payment_id', + 'status', + 'amount', + 'currency') + + +def assert_compliance_respond_response(response): + assert response is not None + # For respond requests, typically returns empty response with 204 status code + if hasattr(response, 'http_metadata'): + assert hasattr(response, 'http_metadata') \ No newline at end of file From 65c3d3ba65fe0fd1e2f552144314018a22dd25d0 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 17 Apr 2026 13:59:40 +0200 Subject: [PATCH 05/16] EtagHeader using custom headers, custom headers code simplification --- checkout_sdk/accounts/accounts.py | 10 ++++++++ checkout_sdk/accounts/accounts_client.py | 5 ++-- checkout_sdk/api_client.py | 32 ++++++++++++------------ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 0c6b075d..b6b4df22 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -1,4 +1,5 @@ from enum import Enum +from typing import Dict from checkout_sdk.common.common import Phone, Address from checkout_sdk.common.common import ResidentialStatusType, AccountHolderIdentification @@ -424,3 +425,12 @@ class FilePurpose(str, Enum): class EntityFileRequest: purpose: FilePurpose + + +class EtagHeader: + etag: str + + def get_header_mappings(self) -> Dict[str, str]: + return { + 'etag': 'If-Match' + } \ No newline at end of file diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 75a360ae..33e42214 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -2,7 +2,7 @@ from warnings import warn -from checkout_sdk.accounts.accounts import OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, \ +from checkout_sdk.accounts.accounts import EtagHeader, OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, \ PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ EntityFileRequest from checkout_sdk.api_client import ApiClient @@ -139,7 +139,8 @@ def get_reserve_rule_details(self, entity_id: str, reserve_rule_id: str): def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, update_request: ReserveRuleRequest): headers = None if(etag is not None): - headers = {'If-Match': etag} + headers = EtagHeader() + headers.etag = etag return self._api_client.put( self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), diff --git a/checkout_sdk/api_client.py b/checkout_sdk/api_client.py index db98d13c..9917dc96 100644 --- a/checkout_sdk/api_client.py +++ b/checkout_sdk/api_client.py @@ -140,28 +140,28 @@ def invoke(self, else: return ResponseWrapper(http_metadata) - def _process_custom_headers(self, custom_headers): - headers = {} + def _process_custom_headers(self, custom_headers): + # Trivial case if custom_headers is None: return None - # Get custom mappings if the class defines them + # Get custom mappings if the class defines them, otherwise use empty dict + headers = {} + custom_mappings = {} if hasattr(custom_headers, 'get_header_mappings'): custom_mappings = custom_headers.get_header_mappings() - # Iterate through all attributes - for attr_name in dir(custom_headers): - # Skip private attributes and methods - if attr_name.startswith('_') or callable(getattr(custom_headers, attr_name)): - continue - - value = getattr(custom_headers, attr_name) - if value is not None and value != '': - # Use custom mapping if available, otherwise convert using default logic - header_name = custom_mappings.get(attr_name, self._convert_property_to_header(attr_name)) - headers[header_name] = str(value) - else: - headers = custom_headers + # Iterate through all attributes + for attr_name in dir(custom_headers): + # Skip private attributes and methods + if attr_name.startswith('_') or callable(getattr(custom_headers, attr_name)): + continue + + value = getattr(custom_headers, attr_name) + if value is not None and value != '': + # Use custom mapping if available, otherwise convert using default logic + header_name = custom_mappings.get(attr_name, self._convert_property_to_header(attr_name)) + headers[header_name] = str(value) return headers From 3392db47a0442dad3ee7cb44dc7a77e5aea9529b Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 20 Apr 2026 10:50:15 +0200 Subject: [PATCH 06/16] StandaloneAccountUpdaterClient + unit and integration tests --- checkout_sdk/checkout_api.py | 3 + .../standaloneaccountupdater/__init__.py | 0 .../standalone_account_updater.py | 20 +++++ .../standalone_account_updater_client.py | 21 +++++ tests/conftest.py | 3 +- .../standalone_account_updater_client_test.py | 16 ++++ ...dalone_account_updater_integration_test.py | 83 +++++++++++++++++++ 7 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 checkout_sdk/standaloneaccountupdater/__init__.py create mode 100644 checkout_sdk/standaloneaccountupdater/standalone_account_updater.py create mode 100644 checkout_sdk/standaloneaccountupdater/standalone_account_updater_client.py create mode 100644 tests/standaloneaccountupdater/standalone_account_updater_client_test.py create mode 100644 tests/standaloneaccountupdater/standalone_account_updater_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 11b010a0..9ef27f45 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -27,6 +27,7 @@ from checkout_sdk.forward.forward_client import ForwardClient from checkout_sdk.payments.setups.setups_client import PaymentSetupsClient from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient +from checkout_sdk.standaloneaccountupdater.standalone_account_updater_client import StandaloneAccountUpdaterClient def _base_api_client(configuration: CheckoutConfiguration) -> ApiClient: @@ -82,3 +83,5 @@ def __init__(self, configuration: CheckoutConfiguration): self.forward = ForwardClient(api_client=base_api_client, configuration=configuration) self.setups = PaymentSetupsClient(api_client=base_api_client, configuration=configuration) self.agentic_commerce = AgenticCommerceClient(api_client=base_api_client, configuration=configuration) + self.standalone_account_updater = StandaloneAccountUpdaterClient(api_client=base_api_client, + configuration=configuration) diff --git a/checkout_sdk/standaloneaccountupdater/__init__.py b/checkout_sdk/standaloneaccountupdater/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/standaloneaccountupdater/standalone_account_updater.py b/checkout_sdk/standaloneaccountupdater/standalone_account_updater.py new file mode 100644 index 00000000..379fba59 --- /dev/null +++ b/checkout_sdk/standaloneaccountupdater/standalone_account_updater.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import + + +class CardDetailsRequest: + number: str + expiry_month: int + expiry_year: int + + +class InstrumentReference: + id: str + + +class SourceOptions: + card: CardDetailsRequest + instrument: InstrumentReference + + +class GetUpdatedCardCredentialsRequest: + source_options: SourceOptions diff --git a/checkout_sdk/standaloneaccountupdater/standalone_account_updater_client.py b/checkout_sdk/standaloneaccountupdater/standalone_account_updater_client.py new file mode 100644 index 00000000..74845675 --- /dev/null +++ b/checkout_sdk/standaloneaccountupdater/standalone_account_updater_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.standaloneaccountupdater.standalone_account_updater import GetUpdatedCardCredentialsRequest + + +class StandaloneAccountUpdaterClient(Client): + __ACCOUNT_UPDATER_PATH = 'account-updater/cards' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.OAUTH) + + def get_updated_card_credentials(self, request: GetUpdatedCardCredentialsRequest): + return self._api_client.post(self.__ACCOUNT_UPDATER_PATH, + self._sdk_authorization(), + request) diff --git a/tests/conftest.py b/tests/conftest.py index f851c878..4405a7b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,7 +48,8 @@ def oauth_api(): .scopes([OAuthScopes.GATEWAY, OAuthScopes.VAULT, OAuthScopes.PAYOUTS_BANK_DETAILS, 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_CARD_METADATA, OAuthScopes.FINANCIAL_ACTIONS, + OAuthScopes.VAULT_REAL_TIME_ACCOUNT_UPDATER]) \ .build() diff --git a/tests/standaloneaccountupdater/standalone_account_updater_client_test.py b/tests/standaloneaccountupdater/standalone_account_updater_client_test.py new file mode 100644 index 00000000..5901efa7 --- /dev/null +++ b/tests/standaloneaccountupdater/standalone_account_updater_client_test.py @@ -0,0 +1,16 @@ +import pytest + +from checkout_sdk.standaloneaccountupdater.standalone_account_updater import GetUpdatedCardCredentialsRequest +from checkout_sdk.standaloneaccountupdater.standalone_account_updater_client import StandaloneAccountUpdaterClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return StandaloneAccountUpdaterClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestStandaloneAccountUpdaterClient: + + def test_should_get_updated_card_credentials(self, mocker, client: StandaloneAccountUpdaterClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.get_updated_card_credentials(GetUpdatedCardCredentialsRequest()) == 'response' diff --git a/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py b/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py new file mode 100644 index 00000000..9ff4753b --- /dev/null +++ b/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py @@ -0,0 +1,83 @@ +import pytest + +from checkout_sdk.exception import CheckoutApiException +from checkout_sdk.standaloneaccountupdater.standalone_account_updater import ( + GetUpdatedCardCredentialsRequest, SourceOptions, CardDetailsRequest, InstrumentReference +) +from tests.checkout_test_utils import assert_response + + +# tests + +@pytest.mark.skip(reason='Requires valid account updater credentials and live card data') +def test_should_get_updated_card_credentials_with_card(oauth_api): + response = oauth_api.standalone_account_updater.get_updated_card_credentials( + card_credentials_request(2030) + ) + assert_updated_card_credentials_response(response) + + +@pytest.mark.skip(reason='Requires valid account updater credentials and live instrument data') +def test_should_get_updated_card_credentials_with_instrument(oauth_api): + response = oauth_api.standalone_account_updater.get_updated_card_credentials( + instrument_credentials_request('ins_v5rgkf3gdtpuzjqesyxmyodnya') + ) + assert_updated_card_credentials_response(response) + + +def test_should_throw_on_invalid_card_request(oauth_api): + with pytest.raises(CheckoutApiException): + oauth_api.standalone_account_updater.get_updated_card_credentials(invalid_card_credentials_request()) + + +def test_should_throw_422_on_standard_test_card(oauth_api): + try: + oauth_api.standalone_account_updater.get_updated_card_credentials(card_credentials_request(2026)) + pytest.fail() + except CheckoutApiException as err: + assert err.args[0] == 'The API response status code (422) does not indicate success.' + +# common functions + +def card_credentials_request(expiry_year: int) -> GetUpdatedCardCredentialsRequest: + card = CardDetailsRequest() + card.number = '4242424242424242' + card.expiry_month = 12 + card.expiry_year = expiry_year + + source = SourceOptions() + source.card = card + + request = GetUpdatedCardCredentialsRequest() + request.source_options = source + return request + + +def instrument_credentials_request(instrument_id: str) -> GetUpdatedCardCredentialsRequest: + instrument = InstrumentReference() + instrument.id = instrument_id + + source = SourceOptions() + source.instrument = instrument + + request = GetUpdatedCardCredentialsRequest() + request.source_options = source + return request + + +def invalid_card_credentials_request() -> GetUpdatedCardCredentialsRequest: + card = CardDetailsRequest() + card.number = 'invalid_card_number' + card.expiry_month = 13 + card.expiry_year = 2020 + + source = SourceOptions() + source.card = card + + request = GetUpdatedCardCredentialsRequest() + request.source_options = source + return request + + +def assert_updated_card_credentials_response(response): + assert_response(response, 'http_metadata', 'account_update_status') \ No newline at end of file From 3abcd5cb193e53c524b5b5d5802fd9b3993e6e58 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 20 Apr 2026 18:06:43 +0200 Subject: [PATCH 07/16] Identity modules + unit and integration tests --- checkout_sdk/checkout_api.py | 11 + .../identities/amlscreening/__init__.py | 0 .../identities/amlscreening/amlscreening.py | 11 + .../amlscreening/amlscreening_client.py | 25 +++ .../identities/applicants/__init__.py | 0 .../identities/applicants/applicants.py | 12 ++ .../applicants/applicants_client.py | 36 ++++ .../identities/faceauthentication/__init__.py | 0 .../faceauthentication/faceauthentication.py | 16 ++ .../faceauthentication_client.py | 51 +++++ .../iddocumentverification/__init__.py | 0 .../iddocumentverification.py | 16 ++ .../iddocumentverification_client.py | 63 ++++++ .../identityverification/__init__.py | 0 .../identityverification.py | 28 +++ .../identityverification_client.py | 66 ++++++ tests/identities/amlscreening/__init__.py | 0 .../amlscreening/amlscreening_client_test.py | 20 ++ .../amlscreening_integration_test.py | 57 ++++++ tests/identities/applicants/__init__.py | 0 .../applicants/applicants_client_test.py | 30 +++ .../applicants/applicants_integration_test.py | 90 +++++++++ .../identities/faceauthentication/__init__.py | 0 .../faceauthentication_client_test.py | 42 ++++ .../faceauthentication_integration_test.py | 109 ++++++++++ .../iddocumentverification/__init__.py | 0 .../iddocumentverification_client_test.py | 45 +++++ ...iddocumentverification_integration_test.py | 130 ++++++++++++ .../identityverification/__init__.py | 0 .../identityverification_client_test.py | 49 +++++ .../identityverification_integration_test.py | 188 ++++++++++++++++++ 31 files changed, 1095 insertions(+) create mode 100644 checkout_sdk/identities/amlscreening/__init__.py create mode 100644 checkout_sdk/identities/amlscreening/amlscreening.py create mode 100644 checkout_sdk/identities/amlscreening/amlscreening_client.py create mode 100644 checkout_sdk/identities/applicants/__init__.py create mode 100644 checkout_sdk/identities/applicants/applicants.py create mode 100644 checkout_sdk/identities/applicants/applicants_client.py create mode 100644 checkout_sdk/identities/faceauthentication/__init__.py create mode 100644 checkout_sdk/identities/faceauthentication/faceauthentication.py create mode 100644 checkout_sdk/identities/faceauthentication/faceauthentication_client.py create mode 100644 checkout_sdk/identities/iddocumentverification/__init__.py create mode 100644 checkout_sdk/identities/iddocumentverification/iddocumentverification.py create mode 100644 checkout_sdk/identities/iddocumentverification/iddocumentverification_client.py create mode 100644 checkout_sdk/identities/identityverification/__init__.py create mode 100644 checkout_sdk/identities/identityverification/identityverification.py create mode 100644 checkout_sdk/identities/identityverification/identityverification_client.py create mode 100644 tests/identities/amlscreening/__init__.py create mode 100644 tests/identities/amlscreening/amlscreening_client_test.py create mode 100644 tests/identities/amlscreening/amlscreening_integration_test.py create mode 100644 tests/identities/applicants/__init__.py create mode 100644 tests/identities/applicants/applicants_client_test.py create mode 100644 tests/identities/applicants/applicants_integration_test.py create mode 100644 tests/identities/faceauthentication/__init__.py create mode 100644 tests/identities/faceauthentication/faceauthentication_client_test.py create mode 100644 tests/identities/faceauthentication/faceauthentication_integration_test.py create mode 100644 tests/identities/iddocumentverification/__init__.py create mode 100644 tests/identities/iddocumentverification/iddocumentverification_client_test.py create mode 100644 tests/identities/iddocumentverification/iddocumentverification_integration_test.py create mode 100644 tests/identities/identityverification/__init__.py create mode 100644 tests/identities/identityverification/identityverification_client_test.py create mode 100644 tests/identities/identityverification/identityverification_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 9ef27f45..271fc92c 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -28,6 +28,11 @@ from checkout_sdk.payments.setups.setups_client import PaymentSetupsClient from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient from checkout_sdk.standaloneaccountupdater.standalone_account_updater_client import StandaloneAccountUpdaterClient +from checkout_sdk.identities.amlscreening.amlscreening_client import AmlScreeningClient +from checkout_sdk.identities.faceauthentication.faceauthentication_client import FaceAuthenticationClient +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 def _base_api_client(configuration: CheckoutConfiguration) -> ApiClient: @@ -85,3 +90,9 @@ def __init__(self, configuration: CheckoutConfiguration): self.agentic_commerce = AgenticCommerceClient(api_client=base_api_client, configuration=configuration) self.standalone_account_updater = StandaloneAccountUpdaterClient(api_client=base_api_client, configuration=configuration) + self.aml_screening = AmlScreeningClient(api_client=base_api_client, configuration=configuration) + self.face_authentication = FaceAuthenticationClient(api_client=base_api_client, configuration=configuration) + self.id_document_verification = IdDocumentVerificationClient(api_client=base_api_client, + configuration=configuration) + self.applicants = ApplicantsClient(api_client=base_api_client, configuration=configuration) + self.identity_verification = IdentityVerificationClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/identities/amlscreening/__init__.py b/checkout_sdk/identities/amlscreening/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/identities/amlscreening/amlscreening.py b/checkout_sdk/identities/amlscreening/amlscreening.py new file mode 100644 index 00000000..c80830d3 --- /dev/null +++ b/checkout_sdk/identities/amlscreening/amlscreening.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import + + +class SearchParameters: + configuration_identifier: str + + +class AmlScreeningRequest: + applicant_id: str + search_parameters: SearchParameters + monitored: bool diff --git a/checkout_sdk/identities/amlscreening/amlscreening_client.py b/checkout_sdk/identities/amlscreening/amlscreening_client.py new file mode 100644 index 00000000..6a4c06db --- /dev/null +++ b/checkout_sdk/identities/amlscreening/amlscreening_client.py @@ -0,0 +1,25 @@ +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.identities.amlscreening.amlscreening import AmlScreeningRequest + + +class AmlScreeningClient(Client): + __AML_PATH = 'aml-verifications' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_aml_screening(self, request: AmlScreeningRequest): + return self._api_client.post(self.__AML_PATH, + self._sdk_authorization(), + request) + + def get_aml_screening(self, aml_verification_id: str): + return self._api_client.get(self.build_path(self.__AML_PATH, aml_verification_id), + self._sdk_authorization()) diff --git a/checkout_sdk/identities/applicants/__init__.py b/checkout_sdk/identities/applicants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/identities/applicants/applicants.py b/checkout_sdk/identities/applicants/applicants.py new file mode 100644 index 00000000..8730a866 --- /dev/null +++ b/checkout_sdk/identities/applicants/applicants.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + + +class CreateApplicantRequest: + external_applicant_id: str + email: str + external_applicant_name: str + + +class UpdateApplicantRequest: + email: str + external_applicant_name: str diff --git a/checkout_sdk/identities/applicants/applicants_client.py b/checkout_sdk/identities/applicants/applicants_client.py new file mode 100644 index 00000000..087964bf --- /dev/null +++ b/checkout_sdk/identities/applicants/applicants_client.py @@ -0,0 +1,36 @@ +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.identities.applicants.applicants import CreateApplicantRequest, UpdateApplicantRequest + + +class ApplicantsClient(Client): + __APPLICANTS_PATH = 'applicants' + __ANONYMIZE_PATH = 'anonymize' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_applicant(self, request: CreateApplicantRequest): + return self._api_client.post(self.__APPLICANTS_PATH, + self._sdk_authorization(), + request) + + def get_applicant(self, applicant_id: str): + return self._api_client.get(self.build_path(self.__APPLICANTS_PATH, applicant_id), + self._sdk_authorization()) + + def update_applicant(self, applicant_id: str, request: UpdateApplicantRequest): + return self._api_client.patch(self.build_path(self.__APPLICANTS_PATH, applicant_id), + self._sdk_authorization(), + request) + + def anonymize_applicant(self, applicant_id: str): + return self._api_client.post( + self.build_path(self.__APPLICANTS_PATH, applicant_id, self.__ANONYMIZE_PATH), + self._sdk_authorization()) diff --git a/checkout_sdk/identities/faceauthentication/__init__.py b/checkout_sdk/identities/faceauthentication/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/identities/faceauthentication/faceauthentication.py b/checkout_sdk/identities/faceauthentication/faceauthentication.py new file mode 100644 index 00000000..8252414b --- /dev/null +++ b/checkout_sdk/identities/faceauthentication/faceauthentication.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import + + +class ClientInformation: + pre_selected_residence_country: str + pre_selected_language: str + + +class FaceAuthenticationRequest: + applicant_id: str + user_journey_id: str + + +class FaceAuthenticationAttemptRequest: + redirect_url: str + client_information: ClientInformation diff --git a/checkout_sdk/identities/faceauthentication/faceauthentication_client.py b/checkout_sdk/identities/faceauthentication/faceauthentication_client.py new file mode 100644 index 00000000..37cb96cb --- /dev/null +++ b/checkout_sdk/identities/faceauthentication/faceauthentication_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.identities.faceauthentication.faceauthentication import ( + FaceAuthenticationRequest, FaceAuthenticationAttemptRequest +) + + +class FaceAuthenticationClient(Client): + __FACE_AUTHENTICATIONS_PATH = 'face-authentications' + __ANONYMIZE_PATH = 'anonymize' + __ATTEMPTS_PATH = 'attempts' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_face_authentication(self, request: FaceAuthenticationRequest): + return self._api_client.post(self.__FACE_AUTHENTICATIONS_PATH, + self._sdk_authorization(), + request) + + def get_face_authentication(self, face_authentication_id: str): + return self._api_client.get(self.build_path(self.__FACE_AUTHENTICATIONS_PATH, face_authentication_id), + self._sdk_authorization()) + + def anonymize_face_authentication(self, face_authentication_id: str): + return self._api_client.post( + self.build_path(self.__FACE_AUTHENTICATIONS_PATH, face_authentication_id, self.__ANONYMIZE_PATH), + self._sdk_authorization()) + + def create_face_authentication_attempt(self, face_authentication_id: str, request: FaceAuthenticationAttemptRequest): + return self._api_client.post( + self.build_path(self.__FACE_AUTHENTICATIONS_PATH, face_authentication_id, self.__ATTEMPTS_PATH), + self._sdk_authorization(), + request) + + def get_face_authentication_attempts(self, face_authentication_id: str): + return self._api_client.get( + self.build_path(self.__FACE_AUTHENTICATIONS_PATH, face_authentication_id, self.__ATTEMPTS_PATH), + self._sdk_authorization()) + + def get_face_authentication_attempt(self, face_authentication_id: str, attempt_id: str): + return self._api_client.get( + self.build_path(self.__FACE_AUTHENTICATIONS_PATH, face_authentication_id, self.__ATTEMPTS_PATH, + attempt_id), + self._sdk_authorization()) diff --git a/checkout_sdk/identities/iddocumentverification/__init__.py b/checkout_sdk/identities/iddocumentverification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/identities/iddocumentverification/iddocumentverification.py b/checkout_sdk/identities/iddocumentverification/iddocumentverification.py new file mode 100644 index 00000000..3cebf3cd --- /dev/null +++ b/checkout_sdk/identities/iddocumentverification/iddocumentverification.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import + + +class DeclaredData: + name: str + + +class IdDocumentVerificationRequest: + applicant_id: str + user_journey_id: str + declared_data: DeclaredData + + +class IdDocumentVerificationAttemptRequest: + document_front: str + document_back: str diff --git a/checkout_sdk/identities/iddocumentverification/iddocumentverification_client.py b/checkout_sdk/identities/iddocumentverification/iddocumentverification_client.py new file mode 100644 index 00000000..e873caec --- /dev/null +++ b/checkout_sdk/identities/iddocumentverification/iddocumentverification_client.py @@ -0,0 +1,63 @@ +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.identities.iddocumentverification.iddocumentverification import ( + IdDocumentVerificationRequest, IdDocumentVerificationAttemptRequest +) + + +class IdDocumentVerificationClient(Client): + __ID_DOCUMENT_VERIFICATIONS_PATH = 'id-document-verifications' + __ANONYMIZE_PATH = 'anonymize' + __ATTEMPTS_PATH = 'attempts' + __PDF_REPORT_PATH = 'pdf-report' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_id_document_verification(self, request: IdDocumentVerificationRequest): + return self._api_client.post(self.__ID_DOCUMENT_VERIFICATIONS_PATH, + self._sdk_authorization(), + request) + + def get_id_document_verification(self, id_document_verification_id: str): + return self._api_client.get( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id), + self._sdk_authorization()) + + def anonymize_id_document_verification(self, id_document_verification_id: str): + return self._api_client.post( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id, + self.__ANONYMIZE_PATH), + self._sdk_authorization()) + + def create_id_document_verification_attempt(self, id_document_verification_id: str, + request: IdDocumentVerificationAttemptRequest): + return self._api_client.post( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id, + self.__ATTEMPTS_PATH), + self._sdk_authorization(), + request) + + def get_id_document_verification_attempts(self, id_document_verification_id: str): + return self._api_client.get( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id, + self.__ATTEMPTS_PATH), + self._sdk_authorization()) + + def get_id_document_verification_attempt(self, id_document_verification_id: str, attempt_id: str): + return self._api_client.get( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id, + self.__ATTEMPTS_PATH, attempt_id), + self._sdk_authorization()) + + def get_id_document_verification_report(self, id_document_verification_id: str): + return self._api_client.get( + self.build_path(self.__ID_DOCUMENT_VERIFICATIONS_PATH, id_document_verification_id, + self.__PDF_REPORT_PATH), + self._sdk_authorization()) diff --git a/checkout_sdk/identities/identityverification/__init__.py b/checkout_sdk/identities/identityverification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/identities/identityverification/identityverification.py b/checkout_sdk/identities/identityverification/identityverification.py new file mode 100644 index 00000000..cb62341e --- /dev/null +++ b/checkout_sdk/identities/identityverification/identityverification.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import + + +class DeclaredData: + name: str + + +class ClientInformation: + pre_selected_residence_country: str + pre_selected_language: str + + +class IdentityVerificationRequest: + applicant_id: str + declared_data: DeclaredData + user_journey_id: str + + +class IdentityVerificationAndAttemptRequest: + declared_data: DeclaredData + redirect_url: str + user_journey_id: str + applicant_id: str + + +class IdentityVerificationAttemptRequest: + redirect_url: str + client_information: ClientInformation diff --git a/checkout_sdk/identities/identityverification/identityverification_client.py b/checkout_sdk/identities/identityverification/identityverification_client.py new file mode 100644 index 00000000..24ac2d5e --- /dev/null +++ b/checkout_sdk/identities/identityverification/identityverification_client.py @@ -0,0 +1,66 @@ +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.identities.identityverification.identityverification import ( + IdentityVerificationRequest, + IdentityVerificationAndAttemptRequest, + IdentityVerificationAttemptRequest, +) + + +class IdentityVerificationClient(Client): + __CREATE_AND_OPEN_PATH = 'create-and-open-idv' + __IDENTITY_VERIFICATIONS_PATH = 'identity-verifications' + __ANONYMIZE_PATH = 'anonymize' + __ATTEMPTS_PATH = 'attempts' + __PDF_REPORT_PATH = 'pdf-report' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_identity_verification_and_attempt(self, request: IdentityVerificationAndAttemptRequest): + return self._api_client.post(self.__CREATE_AND_OPEN_PATH, + self._sdk_authorization(), + request) + + def create_identity_verification(self, request: IdentityVerificationRequest): + return self._api_client.post(self.__IDENTITY_VERIFICATIONS_PATH, + self._sdk_authorization(), + request) + + def get_identity_verification(self, identity_verification_id: str): + return self._api_client.get(self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id), + self._sdk_authorization()) + + def anonymize_identity_verification(self, identity_verification_id: str): + return self._api_client.post( + self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id, self.__ANONYMIZE_PATH), + self._sdk_authorization()) + + def create_identity_verification_attempt(self, identity_verification_id: str, + request: IdentityVerificationAttemptRequest): + return self._api_client.post( + self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id, self.__ATTEMPTS_PATH), + self._sdk_authorization(), + request) + + def get_identity_verification_attempts(self, identity_verification_id: str): + return self._api_client.get( + self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id, self.__ATTEMPTS_PATH), + self._sdk_authorization()) + + def get_identity_verification_attempt(self, identity_verification_id: str, attempt_id: str): + return self._api_client.get( + self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id, self.__ATTEMPTS_PATH, + attempt_id), + self._sdk_authorization()) + + def get_identity_verification_report(self, identity_verification_id: str): + return self._api_client.get( + self.build_path(self.__IDENTITY_VERIFICATIONS_PATH, identity_verification_id, self.__PDF_REPORT_PATH), + self._sdk_authorization()) diff --git a/tests/identities/amlscreening/__init__.py b/tests/identities/amlscreening/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/identities/amlscreening/amlscreening_client_test.py b/tests/identities/amlscreening/amlscreening_client_test.py new file mode 100644 index 00000000..c6f0a4a2 --- /dev/null +++ b/tests/identities/amlscreening/amlscreening_client_test.py @@ -0,0 +1,20 @@ +import pytest + +from checkout_sdk.identities.amlscreening.amlscreening import AmlScreeningRequest +from checkout_sdk.identities.amlscreening.amlscreening_client import AmlScreeningClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return AmlScreeningClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +class TestAmlScreeningClient: + + def test_should_create_aml_screening(self, mocker, client: AmlScreeningClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_aml_screening(AmlScreeningRequest()) == 'response' + + def test_should_get_aml_screening(self, mocker, client: AmlScreeningClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_aml_screening('scr_7hr7swleu6guzjqesyxmyodnya') == 'response' diff --git a/tests/identities/amlscreening/amlscreening_integration_test.py b/tests/identities/amlscreening/amlscreening_integration_test.py new file mode 100644 index 00000000..2ba1e1af --- /dev/null +++ b/tests/identities/amlscreening/amlscreening_integration_test.py @@ -0,0 +1,57 @@ +import os + +import pytest + +from checkout_sdk.identities.amlscreening.amlscreening import AmlScreeningRequest, SearchParameters +from tests.checkout_test_utils import assert_response + +# tests + +@pytest.mark.skip(reason='Requires valid applicant ID and AML configuration') +def test_should_create_aml_screening(default_api): + response = default_api.aml_screening.create_aml_screening(aml_screening_request()) + assert_aml_screening_response(response) + + +@pytest.mark.skip(reason='Requires valid applicant ID and AML configuration') +def test_should_get_aml_screening(default_api): + created = default_api.aml_screening.create_aml_screening(aml_screening_request()) + retrieved = default_api.aml_screening.get_aml_screening(created.id) + assert_aml_screening_response(retrieved) + + +@pytest.mark.skip(reason='Requires valid applicant ID and AML configuration') +def test_should_create_and_track_aml_screening_workflow(default_api): + created = default_api.aml_screening.create_aml_screening(aml_screening_request()) + assert_aml_screening_response(created) + + updated = default_api.aml_screening.get_aml_screening(created.id) + assert_aml_screening_response(updated) + assert updated.id == created.id + assert updated.applicant_id == created.applicant_id + + +@pytest.mark.skip(reason='Requires valid applicant ID and AML configuration') +def test_should_validate_monitoring_configuration(default_api): + request = aml_screening_request() + request.monitored = False + + response = default_api.aml_screening.create_aml_screening(request) + assert_aml_screening_response(response) + assert response.monitored is False + +# common functions + +def aml_screening_request() -> AmlScreeningRequest: + search_parameters = SearchParameters() + search_parameters.configuration_identifier = os.environ.get('CHECKOUT_TEST_AML_CONFIG_ID', 'config_test_id') + + request = AmlScreeningRequest() + request.applicant_id = os.environ.get('CHECKOUT_TEST_APPLICANT_ID', 'aplt_test_applicant_id') + request.search_parameters = search_parameters + request.monitored = True + return request + + +def assert_aml_screening_response(response): + assert_response(response, 'http_metadata', 'id', 'applicant_id', 'status', 'search_parameters') diff --git a/tests/identities/applicants/__init__.py b/tests/identities/applicants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/identities/applicants/applicants_client_test.py b/tests/identities/applicants/applicants_client_test.py new file mode 100644 index 00000000..4ad6a2fa --- /dev/null +++ b/tests/identities/applicants/applicants_client_test.py @@ -0,0 +1,30 @@ +import pytest + +from checkout_sdk.identities.applicants.applicants import CreateApplicantRequest, UpdateApplicantRequest +from checkout_sdk.identities.applicants.applicants_client import ApplicantsClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return ApplicantsClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestApplicantsClient: + + def test_should_create_applicant(self, mocker, client: ApplicantsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_applicant(CreateApplicantRequest()) == 'response' + + def test_should_get_applicant(self, mocker, client: ApplicantsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_applicant('aplt_7hr7swleu6guzjqesyxmyodnya') == 'response' + + def test_should_update_applicant(self, mocker, client: ApplicantsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.patch', return_value='response') + assert client.update_applicant('aplt_7hr7swleu6guzjqesyxmyodnya', UpdateApplicantRequest()) == 'response' + + def test_should_anonymize_applicant(self, mocker, client: ApplicantsClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.anonymize_applicant('aplt_7hr7swleu6guzjqesyxmyodnya') == 'response' diff --git a/tests/identities/applicants/applicants_integration_test.py b/tests/identities/applicants/applicants_integration_test.py new file mode 100644 index 00000000..6ebc3ecf --- /dev/null +++ b/tests/identities/applicants/applicants_integration_test.py @@ -0,0 +1,90 @@ +import pytest + +from checkout_sdk.exception import CheckoutApiException +from checkout_sdk.identities.applicants.applicants import CreateApplicantRequest, UpdateApplicantRequest +from tests.checkout_test_utils import assert_response, random_email + + +# tests + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_applicant(default_api): + response = default_api.applicants.create_applicant(create_applicant_request()) + assert_applicant_response(response) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_applicant(default_api): + created = default_api.applicants.create_applicant(create_applicant_request()) + retrieved = default_api.applicants.get_applicant(created.id) + assert_applicant_response(retrieved) + assert retrieved.id == created.id + assert retrieved.email == created.email + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_update_applicant(default_api): + created = default_api.applicants.create_applicant(create_applicant_request()) + request = update_applicant_request() + updated = default_api.applicants.update_applicant(created.id, request) + assert_applicant_response(updated) + assert updated.id == created.id + assert updated.email == request.email + assert updated.external_applicant_name == request.external_applicant_name + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_anonymize_applicant(default_api): + created = default_api.applicants.create_applicant(create_applicant_request()) + response = default_api.applicants.anonymize_applicant(created.id) + assert_response(response, 'http_metadata', 'id') + assert response.id == created.id + with pytest.raises(CheckoutApiException): + default_api.applicants.get_applicant(created.id) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_update_and_retrieve_applicant_workflow(default_api): + created = default_api.applicants.create_applicant(create_applicant_request()) + assert_applicant_response(created) + + request = update_applicant_request() + updated = default_api.applicants.update_applicant(created.id, request) + assert_applicant_response(updated) + assert updated.id == created.id + + retrieved = default_api.applicants.get_applicant(created.id) + assert_applicant_response(retrieved) + assert retrieved.id == created.id + assert retrieved.email == request.email + assert retrieved.external_applicant_name == request.external_applicant_name + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_validate_optional_fields(default_api): + request = CreateApplicantRequest() + request.email = random_email() + response = default_api.applicants.create_applicant(request) + assert_applicant_response(response) + assert response.email == request.email + + +# common methods + +def create_applicant_request() -> CreateApplicantRequest: + request = CreateApplicantRequest() + request.external_applicant_id = 'ext_test_applicant' + request.email = random_email() + request.external_applicant_name = 'Test Applicant Name' + return request + + +def update_applicant_request() -> UpdateApplicantRequest: + request = UpdateApplicantRequest() + request.email = random_email() + request.external_applicant_name = 'Updated Test Applicant Name' + return request + + +def assert_applicant_response(response): + assert_response(response, 'http_metadata', 'id', 'email') diff --git a/tests/identities/faceauthentication/__init__.py b/tests/identities/faceauthentication/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/identities/faceauthentication/faceauthentication_client_test.py b/tests/identities/faceauthentication/faceauthentication_client_test.py new file mode 100644 index 00000000..5e41eade --- /dev/null +++ b/tests/identities/faceauthentication/faceauthentication_client_test.py @@ -0,0 +1,42 @@ +import pytest + +from checkout_sdk.identities.faceauthentication.faceauthentication import ( + FaceAuthenticationRequest, FaceAuthenticationAttemptRequest +) +from checkout_sdk.identities.faceauthentication.faceauthentication_client import FaceAuthenticationClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return FaceAuthenticationClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestFaceAuthenticationClient: + + def test_should_create_face_authentication(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_face_authentication(FaceAuthenticationRequest()) == 'response' + + def test_should_get_face_authentication(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_face_authentication('fav_mtta050yudd54y5iqb5ijh8jtvz') == 'response' + + def test_should_anonymize_face_authentication(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.anonymize_face_authentication('fav_mtta050yudd54y5iqb5ijh8jtvz') == 'response' + + def test_should_create_face_authentication_attempt(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_face_authentication_attempt('fav_mtta050yudd54y5iqb5ijh8jtvz', + FaceAuthenticationAttemptRequest()) == 'response' + + def test_should_get_face_authentication_attempts(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_face_authentication_attempts('fav_mtta050yudd54y5iqb5ijh8jtvz') == 'response' + + def test_should_get_face_authentication_attempt(self, mocker, client: FaceAuthenticationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_face_authentication_attempt('fav_mtta050yudd54y5iqb5ijh8jtvz', + 'fatp_nk1wbmmczqumwt95k3v39mhbh2w') == 'response' diff --git a/tests/identities/faceauthentication/faceauthentication_integration_test.py b/tests/identities/faceauthentication/faceauthentication_integration_test.py new file mode 100644 index 00000000..fa067727 --- /dev/null +++ b/tests/identities/faceauthentication/faceauthentication_integration_test.py @@ -0,0 +1,109 @@ +import pytest + +from checkout_sdk.identities.faceauthentication.faceauthentication import ( + FaceAuthenticationRequest, FaceAuthenticationAttemptRequest, ClientInformation +) +from tests.checkout_test_utils import assert_response, new_uuid + + +# tests + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_face_authentication(default_api): + response = default_api.face_authentication.create_face_authentication(face_authentication_request()) + assert_face_authentication_response(response) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_face_authentication(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + retrieved = default_api.face_authentication.get_face_authentication(created.id) + assert_face_authentication_response(retrieved) + assert retrieved.id == created.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_anonymize_face_authentication(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + response = default_api.face_authentication.anonymize_face_authentication(created.id) + assert_response(response, 'http_metadata', 'id') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_face_authentication_attempt(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + attempt = default_api.face_authentication.create_face_authentication_attempt(created.id, + face_authentication_attempt_request()) + assert_face_authentication_attempt_response(attempt) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_face_authentication_attempts(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + created_attempt = default_api.face_authentication.create_face_authentication_attempt( + created.id, face_authentication_attempt_request()) + attempts = default_api.face_authentication.get_face_authentication_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + assert any(a.id == created_attempt.id for a in attempts.data) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_face_authentication_attempt(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + created_attempt = default_api.face_authentication.create_face_authentication_attempt( + created.id, face_authentication_attempt_request()) + retrieved = default_api.face_authentication.get_face_authentication_attempt(created.id, created_attempt.id) + assert_face_authentication_attempt_response(retrieved) + assert retrieved.id == created_attempt.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_perform_face_authentication_workflow(default_api): + created = default_api.face_authentication.create_face_authentication(face_authentication_request()) + assert_face_authentication_response(created) + + retrieved = default_api.face_authentication.get_face_authentication(created.id) + assert_face_authentication_response(retrieved) + assert retrieved.id == created.id + + attempt_request = face_authentication_attempt_request() + created_attempt = default_api.face_authentication.create_face_authentication_attempt(created.id, attempt_request) + assert_face_authentication_attempt_response(created_attempt) + + attempts = default_api.face_authentication.get_face_authentication_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + + retrieved_attempt = default_api.face_authentication.get_face_authentication_attempt(created.id, created_attempt.id) + assert_face_authentication_attempt_response(retrieved_attempt) + assert retrieved_attempt.id == created_attempt.id + + anonymized = default_api.face_authentication.anonymize_face_authentication(created.id) + assert_response(anonymized, 'http_metadata', 'id') + + +# common methods + +def face_authentication_request() -> FaceAuthenticationRequest: + request = FaceAuthenticationRequest() + request.applicant_id = new_uuid() + request.user_journey_id = new_uuid() + return request + + +def face_authentication_attempt_request() -> FaceAuthenticationAttemptRequest: + client_information = ClientInformation() + client_information.pre_selected_residence_country = 'US' + client_information.pre_selected_language = 'en-US' + + request = FaceAuthenticationAttemptRequest() + request.redirect_url = 'https://example.com/redirect' + request.client_information = client_information + return request + + +def assert_face_authentication_response(response): + assert_response(response, 'http_metadata', 'id', 'applicant_id', 'status') + + +def assert_face_authentication_attempt_response(response): + assert_response(response, 'http_metadata', 'id', 'status') diff --git a/tests/identities/iddocumentverification/__init__.py b/tests/identities/iddocumentverification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/identities/iddocumentverification/iddocumentverification_client_test.py b/tests/identities/iddocumentverification/iddocumentverification_client_test.py new file mode 100644 index 00000000..e9f33207 --- /dev/null +++ b/tests/identities/iddocumentverification/iddocumentverification_client_test.py @@ -0,0 +1,45 @@ +import pytest + +from checkout_sdk.identities.iddocumentverification.iddocumentverification import ( + IdDocumentVerificationRequest, IdDocumentVerificationAttemptRequest +) +from checkout_sdk.identities.iddocumentverification.iddocumentverification_client import IdDocumentVerificationClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return IdDocumentVerificationClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestIdDocumentVerificationClient: + + def test_should_create_id_document_verification(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_id_document_verification(IdDocumentVerificationRequest()) == 'response' + + def test_should_get_id_document_verification(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_id_document_verification('iddoc_12345') == 'response' + + def test_should_anonymize_id_document_verification(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.anonymize_id_document_verification('iddoc_12345') == 'response' + + def test_should_create_id_document_verification_attempt(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_id_document_verification_attempt('iddoc_12345', + IdDocumentVerificationAttemptRequest()) == 'response' + + def test_should_get_id_document_verification_attempts(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_id_document_verification_attempts('iddoc_12345') == 'response' + + def test_should_get_id_document_verification_attempt(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_id_document_verification_attempt('iddoc_12345', 'attempt_67890') == 'response' + + def test_should_get_id_document_verification_report(self, mocker, client: IdDocumentVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_id_document_verification_report('iddoc_12345') == 'response' diff --git a/tests/identities/iddocumentverification/iddocumentverification_integration_test.py b/tests/identities/iddocumentverification/iddocumentverification_integration_test.py new file mode 100644 index 00000000..309b3724 --- /dev/null +++ b/tests/identities/iddocumentverification/iddocumentverification_integration_test.py @@ -0,0 +1,130 @@ +import pytest + +from checkout_sdk.identities.iddocumentverification.iddocumentverification import ( + IdDocumentVerificationRequest, IdDocumentVerificationAttemptRequest, DeclaredData +) +from tests.checkout_test_utils import assert_response, new_uuid + + +# tests + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_id_document_verification(default_api): + response = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + assert_id_document_verification_response(response) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_id_document_verification(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + retrieved = default_api.id_document_verification.get_id_document_verification(created.id) + assert_id_document_verification_response(retrieved) + assert retrieved.id == created.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_anonymize_id_document_verification(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + response = default_api.id_document_verification.anonymize_id_document_verification(created.id) + assert_response(response, 'http_metadata', 'id') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_id_document_verification_attempt(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + attempt = default_api.id_document_verification.create_id_document_verification_attempt( + created.id, id_document_verification_attempt_request()) + assert_id_document_verification_attempt_response(attempt) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_id_document_verification_attempts(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + created_attempt = default_api.id_document_verification.create_id_document_verification_attempt( + created.id, id_document_verification_attempt_request()) + attempts = default_api.id_document_verification.get_id_document_verification_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + assert any(a.id == created_attempt.id for a in attempts.data) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_id_document_verification_attempt(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + created_attempt = default_api.id_document_verification.create_id_document_verification_attempt( + created.id, id_document_verification_attempt_request()) + retrieved = default_api.id_document_verification.get_id_document_verification_attempt( + created.id, created_attempt.id) + assert_id_document_verification_attempt_response(retrieved) + assert retrieved.id == created_attempt.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_id_document_verification_report(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + report = default_api.id_document_verification.get_id_document_verification_report(created.id) + assert_response(report, 'http_metadata', 'signed_url') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_perform_id_document_verification_workflow(default_api): + created = default_api.id_document_verification.create_id_document_verification( + id_document_verification_request()) + assert_id_document_verification_response(created) + + retrieved = default_api.id_document_verification.get_id_document_verification(created.id) + assert_id_document_verification_response(retrieved) + assert retrieved.id == created.id + + attempt_request = id_document_verification_attempt_request() + created_attempt = default_api.id_document_verification.create_id_document_verification_attempt( + created.id, attempt_request) + assert_id_document_verification_attempt_response(created_attempt) + + attempts = default_api.id_document_verification.get_id_document_verification_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + + retrieved_attempt = default_api.id_document_verification.get_id_document_verification_attempt( + created.id, created_attempt.id) + assert_id_document_verification_attempt_response(retrieved_attempt) + assert retrieved_attempt.id == created_attempt.id + + report = default_api.id_document_verification.get_id_document_verification_report(created.id) + assert_response(report, 'http_metadata', 'signed_url') + + anonymized = default_api.id_document_verification.anonymize_id_document_verification(created.id) + assert_response(anonymized, 'http_metadata', 'id') + + +# common methods + +def id_document_verification_request() -> IdDocumentVerificationRequest: + declared_data = DeclaredData() + declared_data.name = 'John Doe' + + request = IdDocumentVerificationRequest() + request.applicant_id = new_uuid() + request.user_journey_id = new_uuid() + request.declared_data = declared_data + return request + + +def id_document_verification_attempt_request() -> IdDocumentVerificationAttemptRequest: + request = IdDocumentVerificationAttemptRequest() + request.document_front = 'base64-encoded-front-image-data' + request.document_back = 'base64-encoded-back-image-data' + return request + + +def assert_id_document_verification_response(response): + assert_response(response, 'http_metadata', 'id', 'applicant_id', 'status') + + +def assert_id_document_verification_attempt_response(response): + assert_response(response, 'http_metadata', 'id', 'status') diff --git a/tests/identities/identityverification/__init__.py b/tests/identities/identityverification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/identities/identityverification/identityverification_client_test.py b/tests/identities/identityverification/identityverification_client_test.py new file mode 100644 index 00000000..8fd8e815 --- /dev/null +++ b/tests/identities/identityverification/identityverification_client_test.py @@ -0,0 +1,49 @@ +import pytest + +from checkout_sdk.identities.identityverification.identityverification import ( + IdentityVerificationRequest, IdentityVerificationAndAttemptRequest, IdentityVerificationAttemptRequest +) +from checkout_sdk.identities.identityverification.identityverification_client import IdentityVerificationClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return IdentityVerificationClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestIdentityVerificationClient: + + def test_should_create_identity_verification_and_attempt(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_identity_verification_and_attempt(IdentityVerificationAndAttemptRequest()) == 'response' + + def test_should_create_identity_verification(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_identity_verification(IdentityVerificationRequest()) == 'response' + + def test_should_get_identity_verification(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_identity_verification('idv_12345') == 'response' + + def test_should_anonymize_identity_verification(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.anonymize_identity_verification('idv_12345') == 'response' + + def test_should_create_identity_verification_attempt(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_identity_verification_attempt('idv_12345', + IdentityVerificationAttemptRequest()) == 'response' + + def test_should_get_identity_verification_attempts(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_identity_verification_attempts('idv_12345') == 'response' + + def test_should_get_identity_verification_attempt(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_identity_verification_attempt('idv_12345', 'attempt_67890') == 'response' + + def test_should_get_identity_verification_report(self, mocker, client: IdentityVerificationClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_identity_verification_report('idv_12345') == 'response' diff --git a/tests/identities/identityverification/identityverification_integration_test.py b/tests/identities/identityverification/identityverification_integration_test.py new file mode 100644 index 00000000..a69a4fbd --- /dev/null +++ b/tests/identities/identityverification/identityverification_integration_test.py @@ -0,0 +1,188 @@ +import pytest + +from checkout_sdk.identities.identityverification.identityverification import ( + IdentityVerificationRequest, IdentityVerificationAndAttemptRequest, + IdentityVerificationAttemptRequest, DeclaredData, ClientInformation +) +from tests.checkout_test_utils import assert_response, new_uuid + + +# tests + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_identity_verification_and_attempt(default_api): + response = default_api.identity_verification.create_identity_verification_and_attempt( + identity_verification_and_attempt_request()) + assert_identity_verification_and_attempt_response(response) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_identity_verification(default_api): + response = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + assert_identity_verification_response(response) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_identity_verification(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + retrieved = default_api.identity_verification.get_identity_verification(created.id) + assert_identity_verification_response(retrieved) + assert retrieved.id == created.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_anonymize_identity_verification(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + response = default_api.identity_verification.anonymize_identity_verification(created.id) + assert_response(response, 'http_metadata', 'id') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_create_identity_verification_attempt(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + attempt = default_api.identity_verification.create_identity_verification_attempt( + created.id, identity_verification_attempt_request()) + assert_identity_verification_attempt_response(attempt) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_identity_verification_attempts(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + created_attempt = default_api.identity_verification.create_identity_verification_attempt( + created.id, identity_verification_attempt_request()) + attempts = default_api.identity_verification.get_identity_verification_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + assert any(a.id == created_attempt.id for a in attempts.data) + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_identity_verification_attempt(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + created_attempt = default_api.identity_verification.create_identity_verification_attempt( + created.id, identity_verification_attempt_request()) + retrieved = default_api.identity_verification.get_identity_verification_attempt( + created.id, created_attempt.id) + assert_identity_verification_attempt_response(retrieved) + assert retrieved.id == created_attempt.id + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_get_identity_verification_report(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + report = default_api.identity_verification.get_identity_verification_report(created.id) + assert_response(report, 'http_metadata', 'signed_url') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_perform_complete_identity_verification_workflow(default_api): + created_with_attempt = default_api.identity_verification.create_identity_verification_and_attempt( + identity_verification_and_attempt_request()) + assert_identity_verification_and_attempt_response(created_with_attempt) + + retrieved = default_api.identity_verification.get_identity_verification(created_with_attempt.id) + assert_identity_verification_response(retrieved) + assert retrieved.id == created_with_attempt.id + + attempt = default_api.identity_verification.create_identity_verification_attempt( + created_with_attempt.id, identity_verification_attempt_request()) + assert_identity_verification_attempt_response(attempt) + + attempts = default_api.identity_verification.get_identity_verification_attempts(created_with_attempt.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + assert any(a.id == attempt.id for a in attempts.data) + + retrieved_attempt = default_api.identity_verification.get_identity_verification_attempt( + created_with_attempt.id, attempt.id) + assert_identity_verification_attempt_response(retrieved_attempt) + assert retrieved_attempt.id == attempt.id + + report = default_api.identity_verification.get_identity_verification_report(created_with_attempt.id) + assert_response(report, 'http_metadata', 'signed_url') + + anonymized = default_api.identity_verification.anonymize_identity_verification(created_with_attempt.id) + assert_response(anonymized, 'http_metadata', 'id') + + +@pytest.mark.skip(reason='Requires valid test environment setup') +def test_should_perform_separate_create_and_attempt_workflow(default_api): + created = default_api.identity_verification.create_identity_verification( + identity_verification_request()) + assert_identity_verification_response(created) + + retrieved = default_api.identity_verification.get_identity_verification(created.id) + assert_identity_verification_response(retrieved) + assert retrieved.id == created.id + + attempt = default_api.identity_verification.create_identity_verification_attempt( + created.id, identity_verification_attempt_request()) + assert_identity_verification_attempt_response(attempt) + + attempts = default_api.identity_verification.get_identity_verification_attempts(created.id) + assert_response(attempts, 'http_metadata', 'total_count', 'data') + assert any(a.id == attempt.id for a in attempts.data) + + retrieved_attempt = default_api.identity_verification.get_identity_verification_attempt( + created.id, attempt.id) + assert_identity_verification_attempt_response(retrieved_attempt) + assert retrieved_attempt.id == attempt.id + + report = default_api.identity_verification.get_identity_verification_report(created.id) + assert_response(report, 'http_metadata', 'signed_url') + + anonymized = default_api.identity_verification.anonymize_identity_verification(created.id) + assert_response(anonymized, 'http_metadata', 'id') + + +# common methods + +def identity_verification_and_attempt_request() -> IdentityVerificationAndAttemptRequest: + declared_data = DeclaredData() + declared_data.name = 'John Doe' + + request = IdentityVerificationAndAttemptRequest() + request.applicant_id = new_uuid() + request.user_journey_id = new_uuid() + request.redirect_url = 'https://example.com/redirect' + request.declared_data = declared_data + return request + + +def identity_verification_request() -> IdentityVerificationRequest: + declared_data = DeclaredData() + declared_data.name = 'John Doe' + + request = IdentityVerificationRequest() + request.applicant_id = new_uuid() + request.user_journey_id = new_uuid() + request.declared_data = declared_data + return request + + +def identity_verification_attempt_request() -> IdentityVerificationAttemptRequest: + client_information = ClientInformation() + client_information.pre_selected_residence_country = 'US' + client_information.pre_selected_language = 'en-US' + + request = IdentityVerificationAttemptRequest() + request.redirect_url = 'https://example.com/redirect' + request.client_information = client_information + return request + + +def assert_identity_verification_and_attempt_response(response): + assert_response(response, 'http_metadata', 'id', 'applicant_id', 'status', 'redirect_url') + + +def assert_identity_verification_response(response): + assert_response(response, 'http_metadata', 'id', 'applicant_id', 'status') + + +def assert_identity_verification_attempt_response(response): + assert_response(response, 'http_metadata', 'id', 'status') From 0d97825fe38a638546c57182c97775869075f426 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 21 Apr 2026 10:56:34 +0200 Subject: [PATCH 08/16] Applepay client + unit and integration tests --- checkout_sdk/checkout_api.py | 2 + checkout_sdk/payments/applepay/__init__.py | 0 checkout_sdk/payments/applepay/applepay.py | 18 ++++ .../payments/applepay/applepay_client.py | 30 +++++++ tests/payments/applepay/__init__.py | 0 .../payments/applepay/applepay_client_test.py | 27 ++++++ .../applepay/applepay_integration_test.py | 82 +++++++++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 checkout_sdk/payments/applepay/__init__.py create mode 100644 checkout_sdk/payments/applepay/applepay.py create mode 100644 checkout_sdk/payments/applepay/applepay_client.py create mode 100644 tests/payments/applepay/__init__.py create mode 100644 tests/payments/applepay/applepay_client_test.py create mode 100644 tests/payments/applepay/applepay_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 271fc92c..8edef3fe 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -27,6 +27,7 @@ from checkout_sdk.forward.forward_client import ForwardClient from checkout_sdk.payments.setups.setups_client import PaymentSetupsClient from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient +from checkout_sdk.payments.applepay.applepay_client import ApplePayClient from checkout_sdk.standaloneaccountupdater.standalone_account_updater_client import StandaloneAccountUpdaterClient from checkout_sdk.identities.amlscreening.amlscreening_client import AmlScreeningClient from checkout_sdk.identities.faceauthentication.faceauthentication_client import FaceAuthenticationClient @@ -88,6 +89,7 @@ def __init__(self, configuration: CheckoutConfiguration): self.forward = ForwardClient(api_client=base_api_client, configuration=configuration) self.setups = PaymentSetupsClient(api_client=base_api_client, configuration=configuration) self.agentic_commerce = AgenticCommerceClient(api_client=base_api_client, configuration=configuration) + self.apple_pay = ApplePayClient(api_client=base_api_client, configuration=configuration) self.standalone_account_updater = StandaloneAccountUpdaterClient(api_client=base_api_client, configuration=configuration) self.aml_screening = AmlScreeningClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/payments/applepay/__init__.py b/checkout_sdk/payments/applepay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/payments/applepay/applepay.py b/checkout_sdk/payments/applepay/applepay.py new file mode 100644 index 00000000..16688b8b --- /dev/null +++ b/checkout_sdk/payments/applepay/applepay.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class ProtocolVersions(str, Enum): + EC_V1 = 'ec_v1' + RSA_V1 = 'rsa_v1' + + +class UploadCertificateRequest: + content: str + + +class EnrollDomainRequest: + domain: str + + +class GenerateSigningRequestRequest: + protocol_version: ProtocolVersions diff --git a/checkout_sdk/payments/applepay/applepay_client.py b/checkout_sdk/payments/applepay/applepay_client.py new file mode 100644 index 00000000..ae404dc6 --- /dev/null +++ b/checkout_sdk/payments/applepay/applepay_client.py @@ -0,0 +1,30 @@ +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.payments.applepay.applepay import UploadCertificateRequest, EnrollDomainRequest, \ + GenerateSigningRequestRequest + + +class ApplePayClient(Client): + __CERTIFICATES_PATH = 'applepay/certificates' + __ENROLLMENTS_PATH = 'applepay/enrollments' + __SIGNING_REQUESTS_PATH = 'applepay/signing-requests' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def upload_payment_processing_certificate(self, request: UploadCertificateRequest): + return self._api_client.post(self.__CERTIFICATES_PATH, self._sdk_authorization(), request) + + def enroll_domain(self, request: EnrollDomainRequest): + return self._api_client.post(self.__ENROLLMENTS_PATH, + self._sdk_authorization(AuthorizationType.OAUTH), + request) + + def generate_certificate_signing_request(self, request: GenerateSigningRequestRequest): + return self._api_client.post(self.__SIGNING_REQUESTS_PATH, self._sdk_authorization(), request) diff --git a/tests/payments/applepay/__init__.py b/tests/payments/applepay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/payments/applepay/applepay_client_test.py b/tests/payments/applepay/applepay_client_test.py new file mode 100644 index 00000000..1e3cbf8e --- /dev/null +++ b/tests/payments/applepay/applepay_client_test.py @@ -0,0 +1,27 @@ +import pytest + +from checkout_sdk.payments.applepay.applepay import UploadCertificateRequest, EnrollDomainRequest, \ + GenerateSigningRequestRequest +from checkout_sdk.payments.applepay.applepay_client import ApplePayClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return ApplePayClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestApplePayClient: + + def test_should_upload_payment_processing_certificate(self, mocker, client: ApplePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.upload_payment_processing_certificate(UploadCertificateRequest()) == 'response' + + def test_should_enroll_domain(self, mocker, client: ApplePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.enroll_domain(EnrollDomainRequest()) == 'response' + + def test_should_generate_certificate_signing_request(self, mocker, client: ApplePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.generate_certificate_signing_request(GenerateSigningRequestRequest()) == 'response' diff --git a/tests/payments/applepay/applepay_integration_test.py b/tests/payments/applepay/applepay_integration_test.py new file mode 100644 index 00000000..8ea15c74 --- /dev/null +++ b/tests/payments/applepay/applepay_integration_test.py @@ -0,0 +1,82 @@ +from __future__ import absolute_import + +import pytest + +from checkout_sdk.payments.applepay.applepay import UploadCertificateRequest, EnrollDomainRequest, \ + GenerateSigningRequestRequest, ProtocolVersions +from tests.checkout_test_utils import assert_response + + +# tests + +@pytest.mark.skip(reason='Requires valid payment processing certificate') +def test_should_upload_payment_processing_certificate(default_api): + request = create_upload_certificate_request() + response = default_api.apple_pay.upload_payment_processing_certificate(request) + assert_upload_certificate_response(response) + + +@pytest.mark.skip(reason='Requires OAuth credentials and valid domain verification') +def test_should_enroll_domain(default_api): + request = create_enroll_domain_request() + response = default_api.apple_pay.enroll_domain(request) + assert response is not None + + +def test_should_generate_certificate_signing_request(default_api): + request = create_generate_signing_request(ProtocolVersions.EC_V1) + response = default_api.apple_pay.generate_certificate_signing_request(request) + assert_generate_signing_request_response(response) + + +def test_should_generate_certificate_signing_request_with_rsa_protocol(default_api): + request = create_generate_signing_request(ProtocolVersions.RSA_V1) + response = default_api.apple_pay.generate_certificate_signing_request(request) + assert_generate_signing_request_response(response) + + +# common methods + +def create_upload_certificate_request() -> UploadCertificateRequest: + request = UploadCertificateRequest() + request.content = ('MIIEfTCCBCOgAwIBAgIID/asezaWNycwCgYIKoZIzj0EAwIwgYAxNDAyBgNVBAMMK0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9w' + 'ZXIgUmVsYXRpb25zIENBIC0gRzIxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRMwEQYDVQQK' + 'DApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzAeFw0yMTA2MTExMzQ0MjVaFw0yMzA3MTExMzQ0MjRaMIGuMS0wKwYKCZIm' + 'iZPyLGQBAQwdbWVyY2hhbnQuY29tLmNoZWNrb3V0LnNhbmRib3gxQzBBBgNVBAMMOkFwcGxlIFBheSBQYXltZW50IFBy' + 'b2Nlc3Npbmc6bWVyY2hhbnQuY29tLmNoZWNrb3V0LnNhbmRib3gxEzARBgNVBAsMCkUzMlhCUUs0UTUxFjAUBgNVBAoM' + 'DUNoZWNrb3V0IEx0ZC4xCzAJBgNVBAYTAkdCMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsvyUM9D1cssldH+VPpt' + 'En4VAw/Q6ovJuHVlyBSRaPGLHFce04lCiT/xnXOWRkUxyCzQWKhfG2zo19u4s+evx7aOCAlUwggJRMAwGA1UdEwEB/w' + 'QCMAAwHwYDVR0jBBgwFoAUhLaEzDqGYnIWWZToGqO9SN863wswRwYIKwYBBQUHAQEEOzA5MDcGCCsGAQUFBzABhitod' + 'HRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDA0LWFwcGxld3dkcmNhMjAxMIIBHQYDVR0gBIIBFDCCARAwggEMBgkqhkiG' + '92NkBQEwgf4wgcMGCCsGAQUFBwICMIG2DIGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydCBh' + 'c3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW' + 'ducyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLj' + 'A2BggrBgEFBQcCAQYqaHR0cDovL3d3dy5hcHBsZS5jb20vY2VydGlmaWNhdGVhdXRob3JpdHkvMDYGA1UdHwQvMC0wK6' + 'ApoCeGJWh0dHA6Ly9jcmwuYXBwbGUuY29tL2FwcGxld3dkcmNhMi5jcmwwHQYDVR0OBBYEFE2G+vfc0O4zhDEFl3Xpr' + '4AJsegTMA4GA1UdDwEB/wQEAwIHIDBPBgoZhkiG92NkBgAEBEIMQDdGRjg0REI5MDE5NkVGN0I5RTc4NDZEMjg4NzZCNk' + 'JGRDU2RjM4MDlCNzUyNjAzRDM4QzcxNUJFMTY2M0JENEMwCgYIKoZIzj0EAwIDSAAwRQIgTjywMwOrLX3TwDUrPn7yDG' + 'L/dhc+VNudv0uGBOWRyXACIQClFQFvgx+hfTwVdHt8klrswpgtZtbYjs74p9GYuc8Puw==') + return request + + +def create_enroll_domain_request() -> EnrollDomainRequest: + request = EnrollDomainRequest() + request.domain = 'checkout-test-domain.com' + return request + + +def create_generate_signing_request(protocol_version: ProtocolVersions) -> GenerateSigningRequestRequest: + request = GenerateSigningRequestRequest() + request.protocol_version = protocol_version + return request + + +def assert_upload_certificate_response(response): + assert_response(response, 'http_metadata', 'id', 'public_key_hash', 'valid_from', 'valid_until') + assert response.valid_until > response.valid_from + + +def assert_generate_signing_request_response(response): + assert_response(response, 'http_metadata', 'content') + assert 'BEGIN CERTIFICATE REQUEST' in response.content + assert 'END CERTIFICATE REQUEST' in response.content From 5896a6e6279e9da1538f1bf58deb5d83985a5b6a Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 21 Apr 2026 12:43:35 +0200 Subject: [PATCH 09/16] GooglePayClient + unit and integration tests --- checkout_sdk/checkout_api.py | 2 + checkout_sdk/payments/googlepay/__init__.py | 0 checkout_sdk/payments/googlepay/googlepay.py | 8 +++ .../payments/googlepay/googlepay_client.py | 38 ++++++++++++ tests/payments/googlepay/__init__.py | 0 .../googlepay/googlepay_client_test.py | 30 +++++++++ .../googlepay/googlepay_integration_test.py | 62 +++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 checkout_sdk/payments/googlepay/__init__.py create mode 100644 checkout_sdk/payments/googlepay/googlepay.py create mode 100644 checkout_sdk/payments/googlepay/googlepay_client.py create mode 100644 tests/payments/googlepay/__init__.py create mode 100644 tests/payments/googlepay/googlepay_client_test.py create mode 100644 tests/payments/googlepay/googlepay_integration_test.py diff --git a/checkout_sdk/checkout_api.py b/checkout_sdk/checkout_api.py index 8edef3fe..6fbc5552 100644 --- a/checkout_sdk/checkout_api.py +++ b/checkout_sdk/checkout_api.py @@ -28,6 +28,7 @@ from checkout_sdk.payments.setups.setups_client import PaymentSetupsClient from checkout_sdk.agenticcommerce.agentic_commerce_client import AgenticCommerceClient from checkout_sdk.payments.applepay.applepay_client import ApplePayClient +from checkout_sdk.payments.googlepay.googlepay_client import GooglePayClient from checkout_sdk.standaloneaccountupdater.standalone_account_updater_client import StandaloneAccountUpdaterClient from checkout_sdk.identities.amlscreening.amlscreening_client import AmlScreeningClient from checkout_sdk.identities.faceauthentication.faceauthentication_client import FaceAuthenticationClient @@ -90,6 +91,7 @@ def __init__(self, configuration: CheckoutConfiguration): self.setups = PaymentSetupsClient(api_client=base_api_client, configuration=configuration) self.agentic_commerce = AgenticCommerceClient(api_client=base_api_client, configuration=configuration) self.apple_pay = ApplePayClient(api_client=base_api_client, configuration=configuration) + self.google_pay = GooglePayClient(api_client=base_api_client, configuration=configuration) self.standalone_account_updater = StandaloneAccountUpdaterClient(api_client=base_api_client, configuration=configuration) self.aml_screening = AmlScreeningClient(api_client=base_api_client, configuration=configuration) diff --git a/checkout_sdk/payments/googlepay/__init__.py b/checkout_sdk/payments/googlepay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/payments/googlepay/googlepay.py b/checkout_sdk/payments/googlepay/googlepay.py new file mode 100644 index 00000000..857911de --- /dev/null +++ b/checkout_sdk/payments/googlepay/googlepay.py @@ -0,0 +1,8 @@ +class GooglePayEnrollmentRequest: + entity_id: str + email_address: str + accept_terms_of_service: bool + + +class GooglePayRegisterDomainRequest: + web_domain: str diff --git a/checkout_sdk/payments/googlepay/googlepay_client.py b/checkout_sdk/payments/googlepay/googlepay_client.py new file mode 100644 index 00000000..765d83ea --- /dev/null +++ b/checkout_sdk/payments/googlepay/googlepay_client.py @@ -0,0 +1,38 @@ +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.payments.googlepay.googlepay import GooglePayEnrollmentRequest, GooglePayRegisterDomainRequest + + +class GooglePayClient(Client): + __GOOGLEPAY_ENROLLMENTS_PATH = 'googlepay/enrollments' + __DOMAIN_PATH = 'domain' + __DOMAINS_PATH = 'domains' + __STATE_PATH = 'state' + + def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): + super().__init__(api_client=api_client, + configuration=configuration, + authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + + def create_enrollment(self, request: GooglePayEnrollmentRequest): + return self._api_client.post(self.__GOOGLEPAY_ENROLLMENTS_PATH, self._sdk_authorization(), request) + + def register_domain(self, entity_id: str, request: GooglePayRegisterDomainRequest): + return self._api_client.post( + self.build_path(self.__GOOGLEPAY_ENROLLMENTS_PATH, entity_id, self.__DOMAIN_PATH), + self._sdk_authorization(), + request) + + def get_registered_domains(self, entity_id: str): + return self._api_client.get( + self.build_path(self.__GOOGLEPAY_ENROLLMENTS_PATH, entity_id, self.__DOMAINS_PATH), + self._sdk_authorization()) + + def get_enrollment_state(self, entity_id: str): + return self._api_client.get( + self.build_path(self.__GOOGLEPAY_ENROLLMENTS_PATH, entity_id, self.__STATE_PATH), + self._sdk_authorization()) diff --git a/tests/payments/googlepay/__init__.py b/tests/payments/googlepay/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/payments/googlepay/googlepay_client_test.py b/tests/payments/googlepay/googlepay_client_test.py new file mode 100644 index 00000000..08dc8b49 --- /dev/null +++ b/tests/payments/googlepay/googlepay_client_test.py @@ -0,0 +1,30 @@ +import pytest + +from checkout_sdk.payments.googlepay.googlepay import GooglePayEnrollmentRequest, GooglePayRegisterDomainRequest +from checkout_sdk.payments.googlepay.googlepay_client import GooglePayClient + + +@pytest.fixture(scope='class') +def client(mock_sdk_configuration, mock_api_client): + return GooglePayClient(api_client=mock_api_client, configuration=mock_sdk_configuration) + + +# tests + +class TestGooglePayClient: + + def test_should_create_enrollment(self, mocker, client: GooglePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.create_enrollment(GooglePayEnrollmentRequest()) == 'response' + + def test_should_register_domain(self, mocker, client: GooglePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + assert client.register_domain('ent_uzm3uxtssvmuxnyrfdffcyjxeu', GooglePayRegisterDomainRequest()) == 'response' + + def test_should_get_registered_domains(self, mocker, client: GooglePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_registered_domains('ent_uzm3uxtssvmuxnyrfdffcyjxeu') == 'response' + + def test_should_get_enrollment_state(self, mocker, client: GooglePayClient): + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + assert client.get_enrollment_state('ent_uzm3uxtssvmuxnyrfdffcyjxeu') == 'response' diff --git a/tests/payments/googlepay/googlepay_integration_test.py b/tests/payments/googlepay/googlepay_integration_test.py new file mode 100644 index 00000000..da26198b --- /dev/null +++ b/tests/payments/googlepay/googlepay_integration_test.py @@ -0,0 +1,62 @@ +from __future__ import absolute_import + +import pytest + +from checkout_sdk.payments.googlepay.googlepay import GooglePayEnrollmentRequest, GooglePayRegisterDomainRequest +from tests.checkout_test_utils import assert_response + + +# tests + +@pytest.mark.skip(reason='Requires a valid entity with Google Pay enrollment permissions') +def test_should_create_enrollment(default_api): + request = create_enrollment_request() + response = default_api.google_pay.create_enrollment(request) + assert_enrollment_response(response) + + +@pytest.mark.skip(reason='Requires an actively enrolled Google Pay entity') +def test_should_register_domain(default_api): + request = create_register_domain_request() + response = default_api.google_pay.register_domain('ent_uzm3uxtssvmuxnyrfdffcyjxeu', request) + assert response is not None + + +@pytest.mark.skip(reason='Requires an actively enrolled Google Pay entity') +def test_should_get_registered_domains(default_api): + response = default_api.google_pay.get_registered_domains('ent_uzm3uxtssvmuxnyrfdffcyjxeu') + assert_registered_domains_response(response) + + +@pytest.mark.skip(reason='Requires an actively enrolled Google Pay entity') +def test_should_get_enrollment_state(default_api): + response = default_api.google_pay.get_enrollment_state('ent_uzm3uxtssvmuxnyrfdffcyjxeu') + assert_enrollment_state_response(response) + + +# common methods + +def create_enrollment_request() -> GooglePayEnrollmentRequest: + request = GooglePayEnrollmentRequest() + request.entity_id = 'ent_uzm3uxtssvmuxnyrfdffcyjxeu' + request.email_address = 'test@example.com' + request.accept_terms_of_service = True + return request + + +def create_register_domain_request() -> GooglePayRegisterDomainRequest: + request = GooglePayRegisterDomainRequest() + request.web_domain = 'checkout-test-domain.com' + return request + + +def assert_enrollment_response(response): + assert_response(response, 'http_metadata', 'tos_accepted_time', 'state') + + +def assert_registered_domains_response(response): + assert_response(response, 'http_metadata', 'domains') + + +def assert_enrollment_state_response(response): + assert_response(response, 'http_metadata', 'state') From 08c3b460da570199cb334670640fa35d0a4dd697 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 21 Apr 2026 13:43:58 +0200 Subject: [PATCH 10/16] Flake8 fixes and more --- checkout_sdk/accounts/accounts.py | 18 ++--- checkout_sdk/accounts/accounts_client.py | 12 +-- .../agenticcommerce/agentic_commerce.py | 4 +- .../agentic_commerce_client.py | 4 +- checkout_sdk/api_client.py | 51 ++++++------ .../compliancerequests/compliance_requests.py | 4 +- .../compliance_requests_client.py | 10 ++- tests/accounts/accounts_client_test.py | 1 - tests/accounts/accounts_integration_test.py | 55 ++++--------- .../agentic_commerce_client_test.py | 25 +++--- .../agentic_commerce_integration_test.py | 81 ++++++++----------- .../compliance_requests_client_test.py | 34 ++++---- .../compliance_requests_integration_test.py | 71 +++++++--------- tests/forward/forward_integration_test.py | 18 ++--- .../amlscreening_integration_test.py | 2 + .../iddocumentverification_client_test.py | 4 +- ...dalone_account_updater_integration_test.py | 3 +- 17 files changed, 170 insertions(+), 227 deletions(-) diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index b6b4df22..503bbd94 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -389,19 +389,19 @@ class ReserveRuleType(str, Enum): ROLLING = 'rolling' -class ReserveRuleRequest: - type: ReserveRuleType - rolling: RollingReserveRule - valid_from: str +class HoldingDuration: + weeks: int class RollingReserveRule: percentage: float holding_duration: HoldingDuration - -class HoldingDuration: - weeks: int + +class ReserveRuleRequest: + type: ReserveRuleType + rolling: RollingReserveRule + valid_from: str class FilePurpose(str, Enum): @@ -429,8 +429,8 @@ class EntityFileRequest: class EtagHeader: etag: str - + def get_header_mappings(self) -> Dict[str, str]: return { 'etag': 'If-Match' - } \ No newline at end of file + } diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index 33e42214..fe3e20b4 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -2,9 +2,11 @@ from warnings import warn -from checkout_sdk.accounts.accounts import EtagHeader, OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, \ - PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, ReserveRuleRequest, \ - EntityFileRequest +from checkout_sdk.accounts.accounts import ( + EtagHeader, OnboardEntityRequest, UpdateScheduleRequest, AccountsPaymentInstrument, + PaymentInstrumentRequest, PaymentInstrumentsQuery, UpdatePaymentInstrumentRequest, + ReserveRuleRequest, EntityFileRequest +) from checkout_sdk.api_client import ApiClient from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration @@ -138,7 +140,7 @@ def get_reserve_rule_details(self, entity_id: str, reserve_rule_id: str): def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, update_request: ReserveRuleRequest): headers = None - if(etag is not None): + if (etag is not None): headers = EtagHeader() headers.etag = etag @@ -157,4 +159,4 @@ def upload_entity_file(self, entity_id: str, entity_file_request: EntityFileRequ def retrieve_entity_file(self, entity_id: str, file_id: str): return self.__files_client.get( self.build_path(self.__ENTITIES_PATH, entity_id, self.__FILES_PATH, file_id), - self._sdk_authorization()) \ No newline at end of file + self._sdk_authorization()) diff --git a/checkout_sdk/agenticcommerce/agentic_commerce.py b/checkout_sdk/agenticcommerce/agentic_commerce.py index 65dd0bbc..575dc656 100644 --- a/checkout_sdk/agenticcommerce/agentic_commerce.py +++ b/checkout_sdk/agenticcommerce/agentic_commerce.py @@ -85,8 +85,8 @@ class DelegatedPaymentHeaders: signature: str timestamp: str api_version: str - + def get_header_mappings(self) -> Dict[str, str]: return { 'api_version': 'API-Version' - } \ No newline at end of file + } diff --git a/checkout_sdk/agenticcommerce/agentic_commerce_client.py b/checkout_sdk/agenticcommerce/agentic_commerce_client.py index 93e78861..d69dac1f 100644 --- a/checkout_sdk/agenticcommerce/agentic_commerce_client.py +++ b/checkout_sdk/agenticcommerce/agentic_commerce_client.py @@ -21,5 +21,5 @@ def create_delegated_payment_token(self, request: DelegatedPaymentRequest, heade self.build_path(self.__AGENTIC_COMMERCE_PATH, self.__DELEGATE_PAYMENT_PATH), self._sdk_authorization(), request, - headers = headers - ) \ No newline at end of file + headers=headers + ) diff --git a/checkout_sdk/api_client.py b/checkout_sdk/api_client.py index 9917dc96..8d5acbed 100644 --- a/checkout_sdk/api_client.py +++ b/checkout_sdk/api_client.py @@ -32,6 +32,7 @@ class ApiClient: def __init__(self, configuration: CheckoutConfiguration, base_uri: str): self._http_client = configuration.http_client self._base_uri = base_uri + def get(self, path, authorization: SdkAuthorization, @@ -101,17 +102,8 @@ def invoke(self, base_uri = self._base_uri + path try: - json_body = None - params_dict = None - files = None - - if body is not None: - json_body = json.dumps(body, cls=JsonSerializer) - elif params is not None: - params_dict = json.loads(json.dumps(params, cls=JsonSerializer)) - elif file_request is not None: - request_headers.pop('Content-Type') - files, json_body = get_file_request(file_request, multipart_file) + json_body, params_dict, files = self._prepare_request_payload( + request_headers, body, params, file_request, multipart_file) self._logger.info(method + ' ' + path) @@ -137,35 +129,40 @@ def invoke(self, else: contents = response.text return ResponseWrapper(http_metadata, contents) - else: - return ResponseWrapper(http_metadata) - - def _process_custom_headers(self, custom_headers): - # Trivial case + return ResponseWrapper(http_metadata) + + def _prepare_request_payload(self, request_headers, body, params, file_request, multipart_file): + json_body = None + params_dict = None + files = None + if body is not None: + json_body = json.dumps(body, cls=JsonSerializer) + elif params is not None: + params_dict = json.loads(json.dumps(params, cls=JsonSerializer)) + elif file_request is not None: + request_headers.pop('Content-Type') + files, json_body = get_file_request(file_request, multipart_file) + return json_body, params_dict, files + + def _process_custom_headers(self, custom_headers): if custom_headers is None: return None - - # Get custom mappings if the class defines them, otherwise use empty dict + headers = {} custom_mappings = {} if hasattr(custom_headers, 'get_header_mappings'): custom_mappings = custom_headers.get_header_mappings() - - # Iterate through all attributes + for attr_name in dir(custom_headers): - # Skip private attributes and methods if attr_name.startswith('_') or callable(getattr(custom_headers, attr_name)): continue - + value = getattr(custom_headers, attr_name) if value is not None and value != '': - # Use custom mapping if available, otherwise convert using default logic header_name = custom_mappings.get(attr_name, self._convert_property_to_header(attr_name)) headers[header_name] = str(value) - + return headers - + def _convert_property_to_header(self, property_name): - # Convert snake_case to Title-Case (e.g., 'api_version' -> 'Api-Version') return '-'.join(word.capitalize() for word in property_name.split('_')) - diff --git a/checkout_sdk/compliancerequests/compliance_requests.py b/checkout_sdk/compliancerequests/compliance_requests.py index efb141e7..85711f6e 100644 --- a/checkout_sdk/compliancerequests/compliance_requests.py +++ b/checkout_sdk/compliancerequests/compliance_requests.py @@ -9,11 +9,11 @@ class ComplianceRespondedField: not_available: bool -class ComplianceRespondedFields: +class ComplianceRespondedFields: sender: List[ComplianceRespondedField] recipient: List[ComplianceRespondedField] class ComplianceRequestRespondRequest: fields: ComplianceRespondedFields - comments: str \ No newline at end of file + comments: str diff --git a/checkout_sdk/compliancerequests/compliance_requests_client.py b/checkout_sdk/compliancerequests/compliance_requests_client.py index 23ac9330..f54d7c5e 100644 --- a/checkout_sdk/compliancerequests/compliance_requests_client.py +++ b/checkout_sdk/compliancerequests/compliance_requests_client.py @@ -16,9 +16,11 @@ def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) def get_compliance_request(self, payment_id: str): - return self._api_client.get(self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), - self._sdk_authorization()) + return self._api_client.get( + self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), + self._sdk_authorization()) def respond_to_compliance_request(self, payment_id: str, request: ComplianceRequestRespondRequest): - return self._api_client.post(self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), - self._sdk_authorization(), request) \ No newline at end of file + return self._api_client.post( + self.build_path(self.__COMPLIANCE_REQUESTS_PATH, payment_id), + self._sdk_authorization(), request) diff --git a/tests/accounts/accounts_client_test.py b/tests/accounts/accounts_client_test.py index c9085a0b..85a0ab65 100644 --- a/tests/accounts/accounts_client_test.py +++ b/tests/accounts/accounts_client_test.py @@ -95,4 +95,3 @@ def test_should_upload_entity_file(self, mocker, client: AccountsClient): def test_should_retrieve_entity_file(self, mocker, client: AccountsClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') assert client.retrieve_entity_file('entity_id', 'file_id') == 'response' - diff --git a/tests/accounts/accounts_integration_test.py b/tests/accounts/accounts_integration_test.py index 690d73fa..fb846ed9 100644 --- a/tests/accounts/accounts_integration_test.py +++ b/tests/accounts/accounts_integration_test.py @@ -10,7 +10,6 @@ DateOfBirth, Identification, EntityEmailAddresses, Company, EntityRepresentative, PaymentInstrumentRequest, \ InstrumentDocument, InstrumentDetailsFasterPayments, ReserveRuleRequest, RollingReserveRule, \ HoldingDuration, EntityFileRequest, FilePurpose -from checkout_sdk.checkout_api import CheckoutApi from checkout_sdk.common.enums import Currency, Country, InstrumentType from checkout_sdk.files.files import FileRequest from checkout_sdk.oauth_scopes import OAuthScopes @@ -144,58 +143,47 @@ def test_should_create_and_retrieve_payment_instrument(accounts_checkout_api): assert_response(query_response, 'data') + def test_should_get_sub_entity_members(accounts_checkout_api): entity_id = create_test_entity(accounts_checkout_api) - - # Get members (may be empty for new entity) + members_response = accounts_checkout_api.accounts.get_sub_entity_members(entity_id) - - # Response should have structure even if empty + assert members_response is not None def test_create_reserve_rule_should_return_valid_response(accounts_checkout_api): - # Arrange entity_id = create_test_entity(accounts_checkout_api) reserve_rule_request = create_valid_reserve_rule_request() - # Act response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) - # Assert validate_reserve_rule_id_response(response) def test_get_reserve_rules_should_return_valid_response(accounts_checkout_api): - # Arrange entity_id = create_test_entity(accounts_checkout_api) reserve_rule_request = create_valid_reserve_rule_request() create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) validate_reserve_rule_id_response(create_response) - # Act response = accounts_checkout_api.accounts.get_reserve_rules(entity_id) - # Assert validate_reserve_rules_response(response) def test_get_reserve_rule_details_should_return_valid_response(accounts_checkout_api): - # Arrange entity_id = create_test_entity(accounts_checkout_api) reserve_rule_request = create_valid_reserve_rule_request() create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, reserve_rule_request) validate_reserve_rule_id_response(create_response) - # Act response = accounts_checkout_api.accounts.get_reserve_rule_details(entity_id, create_response.id) - # Assert validate_reserve_rule_response(response, reserve_rule_request) def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api): - # Arrange entity_id = create_test_entity(accounts_checkout_api) original_request = create_valid_reserve_rule_request() create_response = accounts_checkout_api.accounts.create_reserve_rule(entity_id, original_request) @@ -204,8 +192,7 @@ def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api) update_request = create_valid_reserve_rule_request() update_request.rolling.percentage = 15.0 update_request.rolling.holding_duration.weeks = 16 - - # Get ETag from the creation response headers + etag = None if hasattr(create_response, 'http_metadata') and hasattr(create_response.http_metadata, 'headers'): headers = create_response.http_metadata.headers @@ -214,40 +201,32 @@ def test_update_reserve_rule_should_return_valid_response(accounts_checkout_api) elif 'ETag' in headers: etag = headers['ETag'] - # Act (will set the If-Match header when using the etag) response = accounts_checkout_api.accounts.update_reserve_rule( - entity_id, - create_response.id, + entity_id, + create_response.id, etag, update_request ) - # Assert validate_reserve_rule_id_response(response) assert response.id == create_response.id def test_should_upload_entity_file_and_retrieve(accounts_checkout_api): - # Arrange entity_id = create_test_entity(accounts_checkout_api) - - # Test upload entity file + request = EntityFileRequest() request.purpose = FilePurpose.IDENTIFICATION - - # Act - Upload file + upload_response = accounts_checkout_api.accounts.upload_entity_file(entity_id, request) - - # Assert - Upload response + assert_response(upload_response, 'id', '_links') assert upload_response.id is not None assert upload_response.id != '' - - # Act - Retrieve file + file_id = upload_response.id retrieve_response = accounts_checkout_api.accounts.retrieve_entity_file(entity_id, file_id) - - # Assert - Retrieve response + assert_response(retrieve_response, 'id') assert retrieve_response.id == file_id @@ -261,6 +240,7 @@ def upload_file(api): assert_response(response, 'id', '_links') return response + def create_test_entity(api): entity_request = OnboardEntityRequest() entity_request.reference = new_uuid()[:15] @@ -277,10 +257,10 @@ def create_test_entity(api): representative.last_name = 'Doe' representative.address = address() entity_request.company.representatives = [representative] - + entity_response = api.accounts.create_entity(entity_request) assert_response(entity_response, 'id') - + return entity_response.id @@ -302,16 +282,16 @@ def build_profile(): def create_valid_reserve_rule_request(): holding_duration = HoldingDuration() holding_duration.weeks = 8 - + rolling_rule = RollingReserveRule() rolling_rule.percentage = 12.5 rolling_rule.holding_duration = holding_duration - + reserve_rule_request = ReserveRuleRequest() reserve_rule_request.type = 'rolling' reserve_rule_request.rolling = rolling_rule reserve_rule_request.valid_from = (datetime.now(timezone.utc) + timedelta(days=30)).isoformat() - + return reserve_rule_request @@ -341,4 +321,3 @@ def validate_reserve_rule_response(response, original_request): assert response.rolling.holding_duration is not None assert response.rolling.holding_duration.weeks == original_request.rolling.holding_duration.weeks assert hasattr(response, 'valid_from') - diff --git a/tests/agenticcommerce/agentic_commerce_client_test.py b/tests/agenticcommerce/agentic_commerce_client_test.py index 63618ede..d7da2083 100644 --- a/tests/agenticcommerce/agentic_commerce_client_test.py +++ b/tests/agenticcommerce/agentic_commerce_client_test.py @@ -15,43 +15,40 @@ def test_create_delegated_payment_token(self, mocker, client: AgenticCommerceCli mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') request = DelegatedPaymentRequest() headers = DelegatedPaymentHeaders() - + response = client.create_delegated_payment_token(request, headers) - + assert response == 'response' def test_create_delegated_payment_token_with_none_request(self, mocker, client: AgenticCommerceClient): mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') headers = DelegatedPaymentHeaders() - + response = client.create_delegated_payment_token(None, headers) - + assert response == 'response' - # Verify None was passed as the request parameter mock_post.assert_called_once() args = mock_post.call_args[0] - assert args[2] is None # request parameter position + assert args[2] is None def test_create_delegated_payment_token_with_none_headers(self, mocker, client: AgenticCommerceClient): mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') request = DelegatedPaymentRequest() - + response = client.create_delegated_payment_token(request, None) - + assert response == 'response' - # Verify None was passed as the headers parameter mock_post.assert_called_once() kwargs = mock_post.call_args.kwargs - assert kwargs['headers'] is None # headers parameter is passed as keyword argument + assert kwargs['headers'] is None def test_create_delegated_payment_token_calls_correct_endpoint(self, mocker, client: AgenticCommerceClient): mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') request = DelegatedPaymentRequest() headers = DelegatedPaymentHeaders() - + client.create_delegated_payment_token(request, headers) - - # Verify the correct endpoint path was called + mock_post.assert_called_once() args = mock_post.call_args[0] - assert 'agentic_commerce/delegate_payment' in args[0] # Path argument \ No newline at end of file + assert 'agentic_commerce/delegate_payment' in args[0] diff --git a/tests/agenticcommerce/agentic_commerce_integration_test.py b/tests/agenticcommerce/agentic_commerce_integration_test.py index 79762e39..1f4c2d3b 100644 --- a/tests/agenticcommerce/agentic_commerce_integration_test.py +++ b/tests/agenticcommerce/agentic_commerce_integration_test.py @@ -9,7 +9,6 @@ DelegatedCardNumberType, DelegatedPaymentAllowanceReason ) from checkout_sdk.common.enums import Currency, Country -from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest, DelegatedPaymentHeaders from checkout_sdk.exception import CheckoutApiException from tests.checkout_test_utils import assert_response @@ -18,137 +17,127 @@ def test_should_create_delegated_payment_token(default_api): request = build_valid_delegated_payment_request() headers = build_valid_delegated_payment_headers() - + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) - + assert_delegated_payment_token_response(response) - assert_response(response, - 'id', - 'created', - 'metadata') + assert_response(response, + 'id', + 'created', + 'metadata') @pytest.mark.skip(reason="Requires a valid HMAC signing key and merchant enabled for agentic commerce") def test_should_create_delegated_payment_token_with_billing_address(default_api): request = build_valid_delegated_payment_request_with_billing_address() headers = build_valid_delegated_payment_headers() - + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) - + assert_delegated_payment_token_response(response) assert_response(response, - 'id', - 'created', - 'metadata') + 'id', + 'created', + 'metadata') @pytest.mark.skip(reason="Requires a valid HMAC signing key and merchant enabled for agentic commerce") def test_should_create_delegated_payment_token_with_network_token(default_api): request = build_valid_delegated_payment_request_with_network_token() headers = build_valid_delegated_payment_headers() - + response = default_api.agentic_commerce.create_delegated_payment_token(request, headers) - + assert_delegated_payment_token_response(response) assert_response(response, - 'id', - 'created', - 'metadata') + 'id', + 'created', + 'metadata') def test_should_fail_create_delegated_payment_token_with_invalid_request(default_api): - # Build request with missing required fields from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentRequest invalid_request = DelegatedPaymentRequest() headers = build_valid_delegated_payment_headers() - + with pytest.raises(CheckoutApiException) as exc_info: default_api.agentic_commerce.create_delegated_payment_token(invalid_request, headers) - - # Should get a validation error from the API + assert exc_info.value.http_metadata.status_code in [400, 422] def test_should_fail_create_delegated_payment_token_with_invalid_signature(default_api): request = build_valid_delegated_payment_request() - - # Build headers with invalid signature + from checkout_sdk.agenticcommerce.agentic_commerce import DelegatedPaymentHeaders invalid_headers = DelegatedPaymentHeaders() invalid_headers.signature = "invalid-signature" invalid_headers.timestamp = "2026-03-11T10:30:00Z" - + with pytest.raises(CheckoutApiException) as exc_info: default_api.agentic_commerce.create_delegated_payment_token(request, invalid_headers) - - # Should get an authentication error + assert exc_info.value.http_metadata.status_code in [401, 403] - # Common methods +# Common methods def build_valid_delegated_payment_request(): - # Payment Method (Card) payment_method = DelegatedPaymentMethodCard() payment_method.card_number_type = DelegatedCardNumberType.FPAN payment_method.number = "4242424242424242" payment_method.exp_month = "11" payment_method.exp_year = "2026" payment_method.metadata = {"issuing_bank": "test"} - - # Allowance + allowance = DelegatedPaymentAllowance() allowance.reason = DelegatedPaymentAllowanceReason.ONE_TIME allowance.max_amount = 10000 allowance.currency = Currency.USD allowance.merchant_id = "cli_vkuhvk4vjn2edkps7dfsq6emqm" allowance.checkout_session_id = "1PQrsT" - # Set expires_at to 1 hour from now (datetime object) allowance.expires_at = datetime.now(timezone.utc) + timedelta(hours=1) - - # Risk Signals + risk_signal = DelegatedPaymentRiskSignal() risk_signal.type = "card_testing" risk_signal.score = 10 risk_signal.action = "blocked" - - # Build the request + request = DelegatedPaymentRequest() request.payment_method = payment_method request.allowance = allowance request.risk_signals = [risk_signal] request.metadata = {"campaign": "q4"} - + return request def build_valid_delegated_payment_request_with_billing_address(): request = build_valid_delegated_payment_request() - + billing_address = DelegatedPaymentBillingAddress() billing_address.name = "John Doe" billing_address.line_one = "123 Test Street" billing_address.city = "London" billing_address.postal_code = "SW1A 1AA" billing_address.country = Country.GB - + request.billing_address = billing_address - + return request def build_valid_delegated_payment_request_with_network_token(): request = build_valid_delegated_payment_request() - - # Change to network token type + request.payment_method.card_number_type = DelegatedCardNumberType.NETWORK_TOKEN - request.payment_method.number = "4111111111111111" # Network token placeholder - + request.payment_method.number = "4111111111111111" + return request def build_valid_delegated_payment_headers(): headers = DelegatedPaymentHeaders() - headers.signature = "eyJtZX..." # Base64 HMAC signature placeholder + headers.signature = "eyJtZX..." headers.timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") headers.api_version = "2026-01-01" return headers @@ -165,7 +154,7 @@ def build_delegated_payment_response_mock(): def assert_delegated_payment_response(response): assert_response(response, 'id', - 'created', + 'created', 'metadata') @@ -177,4 +166,4 @@ def assert_delegated_payment_token_response(response): assert hasattr(response, 'created') assert response['created'] is not None assert hasattr(response, 'metadata') - assert response['metadata'] is not None \ No newline at end of file + assert response['metadata'] is not None diff --git a/tests/compliancerequests/compliance_requests_client_test.py b/tests/compliancerequests/compliance_requests_client_test.py index eb2f2a20..a720bb20 100644 --- a/tests/compliancerequests/compliance_requests_client_test.py +++ b/tests/compliancerequests/compliance_requests_client_test.py @@ -14,18 +14,17 @@ class TestComplianceRequestsClient: def test_get_compliance_request(self, mocker, client: ComplianceRequestsClient): mock_get = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" - + response = client.get_compliance_request(payment_id) - + assert response == 'response' - # Verify the correct endpoint path was called mock_get.assert_called_once() args = mock_get.call_args[0] - assert f'compliance-requests/{payment_id}' in args[0] # Path argument + assert f'compliance-requests/{payment_id}' in args[0] def test_get_compliance_request_with_none_payment_id(self, mocker, client: ComplianceRequestsClient): - mock_get = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') - + mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') + with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): client.get_compliance_request(None) @@ -33,31 +32,26 @@ def test_respond_to_compliance_request(self, mocker, client: ComplianceRequestsC mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" request = ComplianceRequestRespondRequest() - + response = client.respond_to_compliance_request(payment_id, request) - + assert response == 'response' - # Verify the correct endpoint path was called mock_post.assert_called_once() args = mock_post.call_args[0] - assert f'compliance-requests/{payment_id}' in args[0] # Path argument - assert args[2] == request # Request parameter + assert f'compliance-requests/{payment_id}' in args[0] + assert args[2] == request def test_respond_to_compliance_request_with_none_payment_id(self, mocker, client: ComplianceRequestsClient): - mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') request = ComplianceRequestRespondRequest() - + with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): client.respond_to_compliance_request(None, request) def test_respond_to_compliance_request_with_none_request(self, mocker, client: ComplianceRequestsClient): - mock_post = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" - + response = client.respond_to_compliance_request(payment_id, None) - + assert response == 'response' - # Verify None was passed as the request parameter - mock_post.assert_called_once() - args = mock_post.call_args[0] - assert args[2] is None # Request parameter \ No newline at end of file diff --git a/tests/compliancerequests/compliance_requests_integration_test.py b/tests/compliancerequests/compliance_requests_integration_test.py index 91ae4bbe..1862051e 100644 --- a/tests/compliancerequests/compliance_requests_integration_test.py +++ b/tests/compliancerequests/compliance_requests_integration_test.py @@ -13,58 +13,55 @@ @pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") def test_should_get_compliance_request(default_api): - payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID - + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" + response = default_api.compliance_requests.get_compliance_request(payment_id) - + assert_compliance_request_response(response) assert_response(response, - 'payment_id', - 'status', - 'amount', - 'currency') + 'payment_id', + 'status', + 'amount', + 'currency') assert response['payment_id'] == payment_id @pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") def test_should_respond_to_compliance_request(default_api): - payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" request = build_valid_respond_request() - + response = default_api.compliance_requests.respond_to_compliance_request(payment_id, request) - + assert_compliance_respond_response(response) - # Typically returns empty response (204 No Content) @pytest.mark.skip(reason="Requires a payment ID associated with an active compliance request") def test_should_respond_to_compliance_request_with_not_available_fields(default_api): - payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" # Replace with actual payment ID + payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" request = build_valid_respond_request_with_not_available_field() - + response = default_api.compliance_requests.respond_to_compliance_request(payment_id, request) - + assert_compliance_respond_response(response) def test_should_fail_get_compliance_request_with_invalid_payment_id(default_api): invalid_payment_id = "pay_invalid_payment_id" - + with pytest.raises(CheckoutApiException) as exc_info: default_api.compliance_requests.get_compliance_request(invalid_payment_id) - - # Should get a not found error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [401, 404, 422] def test_should_fail_respond_to_compliance_request_with_invalid_payment_id(default_api): invalid_payment_id = "pay_invalid_payment_id" request = build_valid_respond_request() - + with pytest.raises(CheckoutApiException) as exc_info: default_api.compliance_requests.respond_to_compliance_request(invalid_payment_id, request) - - # Should get a not found or validation error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [401, 404, 422] @@ -72,59 +69,50 @@ def test_should_fail_respond_to_compliance_request_with_empty_request(default_ap payment_id = "pay_fun26akvvjjerahhctaq2uzhu4" from checkout_sdk.compliancerequests.compliance_requests import ComplianceRequestRespondRequest empty_request = ComplianceRequestRespondRequest() - + with pytest.raises(CheckoutApiException) as exc_info: default_api.compliance_requests.respond_to_compliance_request(payment_id, empty_request) - - # Should get a validation error, or unauthorized if compliance endpoint requires special permissions + assert exc_info.value.http_metadata.status_code in [400, 401, 422] + # Common methods def build_valid_respond_request(): - """Build a valid ComplianceRequestRespondRequest following the C# test structure.""" - - # Create sender field sender_field = ComplianceRespondedField() sender_field.name = "date_of_birth" sender_field.value = "2000-01-01" sender_field.not_available = False - - # Create recipient field + recipient_field = ComplianceRespondedField() recipient_field.name = "full_name" recipient_field.value = "John Doe" recipient_field.not_available = False - - # Create responded fields + responded_fields = ComplianceRespondedFields() responded_fields.sender = [sender_field] responded_fields.recipient = [recipient_field] - - # Build the request + request = ComplianceRequestRespondRequest() request.fields = responded_fields request.comments = "Providing the requested compliance information" - + return request -def build_valid_respond_request_with_not_available_field(): - # Create sender field with not_available = True +def build_valid_respond_request_with_not_available_field(): sender_field = ComplianceRespondedField() sender_field.name = "social_security_number" sender_field.value = None sender_field.not_available = True - - # Create responded fields + responded_fields = ComplianceRespondedFields() responded_fields.sender = [sender_field] responded_fields.recipient = [] - - # Build the request + request = ComplianceRequestRespondRequest() request.fields = responded_fields request.comments = "Some fields are not available for compliance reasons" - + return request @@ -161,6 +149,5 @@ def assert_compliance_request_response(response): def assert_compliance_respond_response(response): assert response is not None - # For respond requests, typically returns empty response with 204 status code if hasattr(response, 'http_metadata'): - assert hasattr(response, 'http_metadata') \ No newline at end of file + assert hasattr(response, 'http_metadata') diff --git a/tests/forward/forward_integration_test.py b/tests/forward/forward_integration_test.py index af7b9844..269ee8a5 100644 --- a/tests/forward/forward_integration_test.py +++ b/tests/forward/forward_integration_test.py @@ -79,35 +79,29 @@ def assert_secret_response(response, expected_name: str = None): @pytest.mark.skip(reason='This test requires forward secrets scopes and valid credentials') def test_should_create_list_update_delete_secret(default_api): - # Create secret create_request = build_create_secret_request(value="initial_value") secret_name = create_request.name - + create_response = default_api.forward.create_secret(create_request) assert_secret_response(create_response, secret_name) - - # List secrets - should contain our secret + list_response = default_api.forward.list_secrets() assert_response(list_response, 'data') assert any(secret['name'] == secret_name for secret in list_response['data']) - - # Update secret + update_request = build_update_secret_request(value="new_updated_value") update_response = default_api.forward.update_secret(secret_name, update_request) assert_secret_response(update_response, secret_name) - - # Delete secret + delete_response = default_api.forward.delete_secret(secret_name) - # Delete should return empty response (204 No Content) assert delete_response is not None @pytest.mark.skip(reason='This test requires forward secrets scopes and valid credentials') def test_should_create_secret_with_entity_id(default_api): create_request = build_create_secret_request(entity_id="ent_test123") - + create_response = default_api.forward.create_secret(create_request) assert_secret_response(create_response, create_request.name) - - # Cleanup + default_api.forward.delete_secret(create_request.name) diff --git a/tests/identities/amlscreening/amlscreening_integration_test.py b/tests/identities/amlscreening/amlscreening_integration_test.py index 2ba1e1af..e5622ef8 100644 --- a/tests/identities/amlscreening/amlscreening_integration_test.py +++ b/tests/identities/amlscreening/amlscreening_integration_test.py @@ -7,6 +7,7 @@ # tests + @pytest.mark.skip(reason='Requires valid applicant ID and AML configuration') def test_should_create_aml_screening(default_api): response = default_api.aml_screening.create_aml_screening(aml_screening_request()) @@ -42,6 +43,7 @@ def test_should_validate_monitoring_configuration(default_api): # common functions + def aml_screening_request() -> AmlScreeningRequest: search_parameters = SearchParameters() search_parameters.configuration_identifier = os.environ.get('CHECKOUT_TEST_AML_CONFIG_ID', 'config_test_id') diff --git a/tests/identities/iddocumentverification/iddocumentverification_client_test.py b/tests/identities/iddocumentverification/iddocumentverification_client_test.py index e9f33207..9aa60fe2 100644 --- a/tests/identities/iddocumentverification/iddocumentverification_client_test.py +++ b/tests/identities/iddocumentverification/iddocumentverification_client_test.py @@ -29,8 +29,8 @@ def test_should_anonymize_id_document_verification(self, mocker, client: IdDocum def test_should_create_id_document_verification_attempt(self, mocker, client: IdDocumentVerificationClient): mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') - assert client.create_id_document_verification_attempt('iddoc_12345', - IdDocumentVerificationAttemptRequest()) == 'response' + assert client.create_id_document_verification_attempt( + 'iddoc_12345', IdDocumentVerificationAttemptRequest()) == 'response' def test_should_get_id_document_verification_attempts(self, mocker, client: IdDocumentVerificationClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') diff --git a/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py b/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py index 9ff4753b..3bbdae82 100644 --- a/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py +++ b/tests/standaloneaccountupdater/standalone_account_updater_integration_test.py @@ -39,6 +39,7 @@ def test_should_throw_422_on_standard_test_card(oauth_api): # common functions + def card_credentials_request(expiry_year: int) -> GetUpdatedCardCredentialsRequest: card = CardDetailsRequest() card.number = '4242424242424242' @@ -80,4 +81,4 @@ def invalid_card_credentials_request() -> GetUpdatedCardCredentialsRequest: def assert_updated_card_credentials_response(response): - assert_response(response, 'http_metadata', 'account_update_status') \ No newline at end of file + assert_response(response, 'http_metadata', 'account_update_status') From f3b871a4acc8a0411eb484c9d85610a3a4456753 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 21 Apr 2026 13:48:37 +0200 Subject: [PATCH 11/16] Accounts client line truncate --- checkout_sdk/accounts/accounts_client.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/checkout_sdk/accounts/accounts_client.py b/checkout_sdk/accounts/accounts_client.py index fe3e20b4..5348fa3b 100644 --- a/checkout_sdk/accounts/accounts_client.py +++ b/checkout_sdk/accounts/accounts_client.py @@ -134,9 +134,9 @@ def get_reserve_rules(self, entity_id: str): self._sdk_authorization()) def get_reserve_rule_details(self, entity_id: str, reserve_rule_id: str): - return self._api_client.get( - self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), - self._sdk_authorization()) + path = self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, + entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id) + return self._api_client.get(path, self._sdk_authorization()) def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, update_request: ReserveRuleRequest): headers = None @@ -144,11 +144,9 @@ def update_reserve_rule(self, entity_id: str, reserve_rule_id: str, etag: str, u headers = EtagHeader() headers.etag = etag - return self._api_client.put( - self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id), - self._sdk_authorization(), - update_request, - headers=headers) + path = self.build_path(self.__ACCOUNTS_PATH, self.__ENTITIES_PATH, + entity_id, self.__RESERVE_RULES_PATH, reserve_rule_id) + return self._api_client.put(path, self._sdk_authorization(), update_request, headers=headers) def upload_entity_file(self, entity_id: str, entity_file_request: EntityFileRequest): return self.__files_client.post( From 647d52f12ce6fb09035fc5d7b4b37c55c9c51968 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Tue, 21 Apr 2026 15:36:16 +0200 Subject: [PATCH 12/16] compliance request client mock test fix --- tests/compliancerequests/compliance_requests_client_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/compliancerequests/compliance_requests_client_test.py b/tests/compliancerequests/compliance_requests_client_test.py index a720bb20..b00d2ca7 100644 --- a/tests/compliancerequests/compliance_requests_client_test.py +++ b/tests/compliancerequests/compliance_requests_client_test.py @@ -25,7 +25,7 @@ def test_get_compliance_request(self, mocker, client: ComplianceRequestsClient): def test_get_compliance_request_with_none_payment_id(self, mocker, client: ComplianceRequestsClient): mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') - with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): + with pytest.raises(TypeError, match="sequence item 1: expected str"): client.get_compliance_request(None) def test_respond_to_compliance_request(self, mocker, client: ComplianceRequestsClient): @@ -45,7 +45,7 @@ def test_respond_to_compliance_request_with_none_payment_id(self, mocker, client mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') request = ComplianceRequestRespondRequest() - with pytest.raises(TypeError, match="sequence item 1: expected str instance, NoneType found"): + with pytest.raises(TypeError, match="sequence item 1: expected str"): client.respond_to_compliance_request(None, request) def test_respond_to_compliance_request_with_none_request(self, mocker, client: ComplianceRequestsClient): From 7c8cbb6ea936174d91cb155ce32d03481b14199a Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 22 Apr 2026 12:08:41 +0200 Subject: [PATCH 13/16] Applied suggestions to fix --- checkout_sdk/api_client.py | 2 +- checkout_sdk/forward/forward.py | 4 ++++ checkout_sdk/forward/forward_client.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/checkout_sdk/api_client.py b/checkout_sdk/api_client.py index 8d5acbed..7da47125 100644 --- a/checkout_sdk/api_client.py +++ b/checkout_sdk/api_client.py @@ -146,7 +146,7 @@ def _prepare_request_payload(self, request_headers, body, params, file_request, def _process_custom_headers(self, custom_headers): if custom_headers is None: - return None + return {} headers = {} custom_mappings = {} diff --git a/checkout_sdk/forward/forward.py b/checkout_sdk/forward/forward.py index 9021e320..3519515c 100644 --- a/checkout_sdk/forward/forward.py +++ b/checkout_sdk/forward/forward.py @@ -91,3 +91,7 @@ class SecretRequest: name: str value: str entity_id: str = None + +class UpdateSecretRequest: + value: str + entity_id: str = None \ No newline at end of file diff --git a/checkout_sdk/forward/forward_client.py b/checkout_sdk/forward/forward_client.py index 612a5ec0..f22fabde 100644 --- a/checkout_sdk/forward/forward_client.py +++ b/checkout_sdk/forward/forward_client.py @@ -2,7 +2,7 @@ from checkout_sdk.authorization_type import AuthorizationType from checkout_sdk.checkout_configuration import CheckoutConfiguration from checkout_sdk.client import Client -from checkout_sdk.forward.forward import ForwardRequest, SecretRequest +from checkout_sdk.forward.forward import ForwardRequest, SecretRequest, UpdateSecretRequest class ForwardClient(Client): @@ -33,7 +33,7 @@ def list_secrets(self): self._sdk_authorization() ) - def update_secret(self, name: str, request: SecretRequest): + def update_secret(self, name: str, request: UpdateSecretRequest): return self._api_client.patch( self.build_path(self.__FORWARD_PATH, self.__SECRETS_PATH, name), self._sdk_authorization(), From 688fb3230cae3b7a3112b1ce3ca668da4c29acf6 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 22 Apr 2026 12:12:35 +0200 Subject: [PATCH 14/16] Flake 8: Cosmetic spaces --- checkout_sdk/forward/forward.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/checkout_sdk/forward/forward.py b/checkout_sdk/forward/forward.py index 3519515c..aba285ea 100644 --- a/checkout_sdk/forward/forward.py +++ b/checkout_sdk/forward/forward.py @@ -92,6 +92,7 @@ class SecretRequest: value: str entity_id: str = None + class UpdateSecretRequest: value: str - entity_id: str = None \ No newline at end of file + entity_id: str = None From c83825c67f7df372268ad86462bf4b9f9146e1b7 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 22 Apr 2026 15:28:51 +0200 Subject: [PATCH 15/16] Changed oauth scopes and auth of some methods and some other minor changes --- checkout_sdk/agenticcommerce/__init__.py | 0 checkout_sdk/oauth_scopes.py | 1 + checkout_sdk/payments/applepay/applepay_client.py | 6 ++++-- checkout_sdk/payments/googlepay/googlepay_client.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 checkout_sdk/agenticcommerce/__init__.py diff --git a/checkout_sdk/agenticcommerce/__init__.py b/checkout_sdk/agenticcommerce/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/checkout_sdk/oauth_scopes.py b/checkout_sdk/oauth_scopes.py index d2d91ed8..b1583534 100644 --- a/checkout_sdk/oauth_scopes.py +++ b/checkout_sdk/oauth_scopes.py @@ -12,6 +12,7 @@ class OAuthScopes(str, Enum): 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' diff --git a/checkout_sdk/payments/applepay/applepay_client.py b/checkout_sdk/payments/applepay/applepay_client.py index ae404dc6..a2122348 100644 --- a/checkout_sdk/payments/applepay/applepay_client.py +++ b/checkout_sdk/payments/applepay/applepay_client.py @@ -19,7 +19,8 @@ def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) def upload_payment_processing_certificate(self, request: UploadCertificateRequest): - return self._api_client.post(self.__CERTIFICATES_PATH, self._sdk_authorization(), request) + return self._api_client.post(self.__CERTIFICATES_PATH, + self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request) def enroll_domain(self, request: EnrollDomainRequest): return self._api_client.post(self.__ENROLLMENTS_PATH, @@ -27,4 +28,5 @@ def enroll_domain(self, request: EnrollDomainRequest): request) def generate_certificate_signing_request(self, request: GenerateSigningRequestRequest): - return self._api_client.post(self.__SIGNING_REQUESTS_PATH, self._sdk_authorization(), request) + return self._api_client.post(self.__SIGNING_REQUESTS_PATH, + self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request) diff --git a/checkout_sdk/payments/googlepay/googlepay_client.py b/checkout_sdk/payments/googlepay/googlepay_client.py index 765d83ea..d62ae90d 100644 --- a/checkout_sdk/payments/googlepay/googlepay_client.py +++ b/checkout_sdk/payments/googlepay/googlepay_client.py @@ -16,7 +16,7 @@ class GooglePayClient(Client): def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): super().__init__(api_client=api_client, configuration=configuration, - authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) + authorization_type=AuthorizationType.OAUTH) def create_enrollment(self, request: GooglePayEnrollmentRequest): return self._api_client.post(self.__GOOGLEPAY_ENROLLMENTS_PATH, self._sdk_authorization(), request) From fab505cf713dc3d35e6fb4a0493d4d442cbadbc5 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 22 Apr 2026 15:39:20 +0200 Subject: [PATCH 16/16] Flake8: cosmetics spaces aligned --- checkout_sdk/payments/applepay/applepay_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checkout_sdk/payments/applepay/applepay_client.py b/checkout_sdk/payments/applepay/applepay_client.py index a2122348..c6b5e304 100644 --- a/checkout_sdk/payments/applepay/applepay_client.py +++ b/checkout_sdk/payments/applepay/applepay_client.py @@ -20,7 +20,7 @@ def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): def upload_payment_processing_certificate(self, request: UploadCertificateRequest): return self._api_client.post(self.__CERTIFICATES_PATH, - self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request) + self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request) def enroll_domain(self, request: EnrollDomainRequest): return self._api_client.post(self.__ENROLLMENTS_PATH, @@ -29,4 +29,4 @@ def enroll_domain(self, request: EnrollDomainRequest): def generate_certificate_signing_request(self, request: GenerateSigningRequestRequest): return self._api_client.post(self.__SIGNING_REQUESTS_PATH, - self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request) + self._sdk_authorization(AuthorizationType.PUBLIC_KEY), request)