diff --git a/checkout_sdk/accounts/accounts.py b/checkout_sdk/accounts/accounts.py index 777e1eb..1dd6dba 100644 --- a/checkout_sdk/accounts/accounts.py +++ b/checkout_sdk/accounts/accounts.py @@ -285,6 +285,17 @@ class InstrumentDetailsCardToken(InstrumentDetails): token: str +class InstrumentAccountType(str, Enum): + SAVINGS = 'savings' + CHECKING = 'checking' + + +class InstrumentDetailsAch(InstrumentDetails): + account_number: str + routing_number: str + account_type: InstrumentAccountType + + class BankDetails: name: str branch: str diff --git a/checkout_sdk/issuing/cardholders.py b/checkout_sdk/issuing/cardholders.py index 9ccb685..dc83338 100644 --- a/checkout_sdk/issuing/cardholders.py +++ b/checkout_sdk/issuing/cardholders.py @@ -1,19 +1,12 @@ from enum import Enum from checkout_sdk.common.common import Phone, Address -from checkout_sdk.common.enums import DocumentType class CardholderType(str, Enum): INDIVIDUAL = 'individual' -class CardholderDocument: - type: DocumentType - front_document_id: str - back_document_id: str - - class CardholderRequest: type: CardholderType reference: str @@ -26,4 +19,3 @@ class CardholderRequest: date_of_birth: str billing_address: Address residency_address: Address - document: CardholderDocument diff --git a/checkout_sdk/issuing/cards.py b/checkout_sdk/issuing/cards.py index a6f3c52..2aa8f3b 100644 --- a/checkout_sdk/issuing/cards.py +++ b/checkout_sdk/issuing/cards.py @@ -62,6 +62,7 @@ class CardRequest: activate_card: bool metadata: CardMetadata revocation_date: str + activation_date: str # ISO-8601 (IssuingActivationDate) def __init__(self, type_p: CardType): self.type = type_p @@ -89,6 +90,8 @@ class UpdateCardRequest: metadata: CardMetadata expiry_month: int expiry_year: int + activation_date: str # ISO-8601 (IssuingActivationDate) + revocation_date: str # yyyy-mm-dd (IssuingRevocationDate) class RenewCardRequest: @@ -105,10 +108,6 @@ class VirtualCardRenewRequest(RenewCardRequest): pass -class ScheduleCardRevocationRequest: - revocation_date: str - - class SecurityPair: question: str answer: str diff --git a/checkout_sdk/issuing/disputes.py b/checkout_sdk/issuing/disputes.py index 76d7394..fa7a68b 100644 --- a/checkout_sdk/issuing/disputes.py +++ b/checkout_sdk/issuing/disputes.py @@ -1,3 +1,6 @@ +from enum import Enum + + class DisputeEvidence: name: str content: str @@ -9,12 +12,32 @@ class DisputeReasonChange: justification: str +class IssuingDisputeFraudType(str, Enum): + CARD_LOST = 'card_lost' + CARD_STOLEN = 'card_stolen' + CARD_NEVER_RECEIVED = 'card_never_received' + FRAUDULENT_ACCOUNT = 'fraudulent_account' + COUNTERFEIT_CARD = 'counterfeit_card' + ACCOUNT_TAKEOVER = 'account_takeover' + CARD_NOT_PRESENT_FRAUD = 'card_not_present_fraud' + MERCHANT_MISREPRESENTATION = 'merchant_misrepresentation' + CARDHOLDER_MANIPULATION = 'cardholder_manipulation' + INCORRECT_PROCESSING = 'incorrect_processing' + OTHER = 'other' + + +class IssuingDisputeFraudDetails: + fraud_type: IssuingDisputeFraudType # required + description: str + + class CreateDisputeRequest: transaction_id: str reason: str evidence: list # DisputeEvidence amount: int presentment_message_id: str + fraud_details: IssuingDisputeFraudDetails justification: str @@ -23,3 +46,22 @@ class EscalateDisputeRequest: additional_evidence: list # DisputeEvidence amount: int reason_change: DisputeReasonChange + fraud_details: IssuingDisputeFraudDetails + + +class AmendDisputeRequest: + reason: str + amount: int + evidence: list # DisputeEvidence + fraud_details: IssuingDisputeFraudDetails + reason_change_justification: str # max 13000 + action_response: str # max 1000 + + +class SubmitDisputeRequest: + # Deprecated: the submit endpoint is deprecated in the API swagger. Use CreateDisputeRequest to + # create and submit in one step, or AmendDisputeRequest when the dispute status is + # 'action_required'. + reason: str + evidence: list # DisputeEvidence + amount: int diff --git a/checkout_sdk/issuing/issuing_client.py b/checkout_sdk/issuing/issuing_client.py index af50605..92c46c1 100644 --- a/checkout_sdk/issuing/issuing_client.py +++ b/checkout_sdk/issuing/issuing_client.py @@ -6,11 +6,11 @@ from checkout_sdk.client import Client from checkout_sdk.issuing.cardholders import CardholderRequest from checkout_sdk.issuing.cards import CardRequest, ThreeDsEnrollmentRequest, UpdateThreeDsEnrollmentRequest, \ - CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, RenewCardRequest, \ - ScheduleCardRevocationRequest + CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, RenewCardRequest from checkout_sdk.issuing.controls import CardControlRequest, CardControlsQuery, UpdateCardControlRequest, \ CreateControlGroupRequest, ControlGroupQueryTarget, ControlProfileRequest -from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest +from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest, AmendDisputeRequest, \ + SubmitDisputeRequest from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest, \ CardRefundAuthorizationRequest, SimulateOobAuthenticationRequest from checkout_sdk.issuing.transactions import TransactionsQueryFilter @@ -25,7 +25,6 @@ class IssuingClient(Client): __CREDENTIALS = 'credentials' __RENEW = 'renew' __REVOKE = 'revoke' - __SCHEDULE_REVOCATION = 'schedule-revocation' __SUSPEND = 'suspend' __CONTROLS = 'controls' __CONTROL_GROUPS = 'control-groups' @@ -37,6 +36,8 @@ class IssuingClient(Client): __DISPUTES = 'disputes' __CANCEL = 'cancel' __ESCALATE = 'escalate' + __AMEND = 'amend' + __SUBMIT = 'submit' __SIMULATE = 'simulate' __AUTHORIZATIONS = 'authorizations' __PRESENTMENTS = 'presentments' @@ -115,17 +116,6 @@ def revoke_card(self, card_id: str, revoke_request: RevokeRequest): self._sdk_authorization(), revoke_request) - def schedule_card_revocation(self, card_id: str, schedule_request: ScheduleCardRevocationRequest): - return self._api_client.post( - self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SCHEDULE_REVOCATION), - self._sdk_authorization(), - schedule_request) - - def delete_card_revocation(self, card_id: str): - return self._api_client.delete( - self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SCHEDULE_REVOCATION), - self._sdk_authorization()) - def suspend_card(self, card_id: str, suspend_request: SuspendRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__CARDS, card_id, self.__SUSPEND), self._sdk_authorization(), @@ -248,6 +238,22 @@ def escalate_dispute(self, dispute_id: str, escalate_dispute_request: EscalateDi escalate_dispute_request, idempotency_key) + def amend_dispute(self, dispute_id: str, amend_dispute_request: AmendDisputeRequest = None, + idempotency_key: str = None): + return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id, self.__AMEND), + self._sdk_authorization(), + amend_dispute_request, + idempotency_key) + + def submit_dispute(self, dispute_id: str, submit_dispute_request: SubmitDisputeRequest = None, + idempotency_key: str = None): + # Deprecated: use create_dispute to create and submit in one step, or amend_dispute when the + # dispute status is 'action_required'. The submit endpoint is deprecated in the API swagger. + return self._api_client.post(self.build_path(self.__ISSUING, self.__DISPUTES, dispute_id, self.__SUBMIT), + self._sdk_authorization(), + submit_dispute_request, + idempotency_key) + def simulate_authorization(self, authorization_request: CardAuthorizationRequest): return self._api_client.post(self.build_path(self.__ISSUING, self.__SIMULATE, self.__AUTHORIZATIONS), self._sdk_authorization(), diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 9dd0684..d7c7128 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -75,7 +75,9 @@ class PaymentMethodBase: # Klarna entities class KlarnaAccountHolder: - billing_address: Address + # readOnly: the account holder details returned by Klarna after the shopper completes + # verification (swagger KlarnaAccountHolder). + name: str class Klarna(PaymentMethodBase): @@ -402,6 +404,73 @@ class ApplePay(PaymentSetupPaymentMethod): account_holder: PaymentSetupAccountHolder +# Bacs entities +class BacsAccountHolderType(str, Enum): + INDIVIDUAL = 'individual' + CORPORATE = 'corporate' + + +class BacsAccountHolder: + type: BacsAccountHolderType + first_name: str + last_name: str + company_name: str + email: str + + +class Bacs(PaymentMethodBase): + instrument_id: str + account_holder: BacsAccountHolder + account_number: str + bank_code: str + country: str + currency: Currency + allow_partial_match: bool + + +# Card Present entities +class CardPresentPin: + key_set_id: str + block: str + block_format: str + + +class CardPresent(PaymentSetupPaymentMethod): + track2: str + emv: str + entry_mode: str + pin: CardPresentPin + store_for_future_use: bool + name: str + + +# Pay by Bank entities +class PayByBankBank: + bank_id: str + display_name: str + logo_url: str + available: bool + + +class PayByBankActionType(str, Enum): + SELECT_BANK = 'select_bank' + + +class PayByBankAction: + type: PayByBankActionType + banks: list # list of PayByBankBank + + +class PayByBank(PaymentSetupPaymentMethod): + bank_id: str + action: PayByBankAction + + +# Stablecoin entities +class Stablecoin(PaymentSetupPaymentMethod): + pass + + # Card entities class Card(PaymentSetupPaymentMethod): number: str @@ -459,6 +528,10 @@ class PaymentMethods: googlepay: GooglePay applepay: ApplePay card: Card + bacs: Bacs + card_present: CardPresent + pay_by_bank: PayByBank + stablecoin: Stablecoin # Settings entity @@ -477,6 +550,18 @@ class OrderSubMerchant: registration_date: datetime +class AmountAllocationCommission: + amount: int + percentage: float + + +class PaymentSetupAmountAllocation: + id: str # required + amount: int # required + reference: str + commission: AmountAllocationCommission + + class Order: items: list # list of PaymentContextsItems shipping: ShippingDetails @@ -485,6 +570,9 @@ class Order: invoice_id: str shipping_amount: int tax_amount: int + tipping_amount: int + surcharge_amount: int + amount_allocations: list # list of PaymentSetupAmountAllocation # Industry entities @@ -504,6 +592,22 @@ class PaymentSetupBilling: address: Address +class PaymentSetupBillingDescriptor: + name: str + city: str + reference: str + + +class PaymentSetupPresentmentDetails: + amount: int + currency: str + + +class PaymentSetupTerminal: + id: str + local_date_time: str + + class AccountFundingTransactionIdentificationType(str, Enum): PASSPORT = 'passport' DRIVING_LICENSE = 'driving_license' @@ -573,4 +677,7 @@ class PaymentSetupsRequest: order: Order industry: Industry billing: PaymentSetupBilling + billing_descriptor: PaymentSetupBillingDescriptor + presentment_details: PaymentSetupPresentmentDetails + terminal: PaymentSetupTerminal account_funding_transaction: PaymentSetupAccountFundingTransaction diff --git a/tests/accounts/instrument_details_ach_serialization_test.py b/tests/accounts/instrument_details_ach_serialization_test.py new file mode 100644 index 0000000..c163b88 --- /dev/null +++ b/tests/accounts/instrument_details_ach_serialization_test.py @@ -0,0 +1,28 @@ +import json + +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.accounts.accounts import ( + InstrumentDetails, InstrumentDetailsAch, InstrumentAccountType, +) + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +class TestInstrumentDetailsAchSerialization: + + def test_is_instrument_details(self): + assert isinstance(InstrumentDetailsAch(), InstrumentDetails) + + def test_serializes_all_three_fields(self): + details = InstrumentDetailsAch() + details.account_number = '12345100' + details.routing_number = '026009593' + details.account_type = InstrumentAccountType.SAVINGS + + assert _serialize(details) == { + 'account_number': '12345100', + 'routing_number': '026009593', + 'account_type': 'savings', + } diff --git a/tests/issuing/cards_issuing_integration_test.py b/tests/issuing/cards_issuing_integration_test.py index c80df2f..fcb7210 100644 --- a/tests/issuing/cards_issuing_integration_test.py +++ b/tests/issuing/cards_issuing_integration_test.py @@ -4,7 +4,7 @@ from checkout_sdk.issuing.cards import PasswordEnrollmentRequest, SecurityPair, UpdateThreeDsEnrollmentRequest, \ CardCredentialsQuery, RevokeRequest, RevokeReason, SuspendRequest, SuspendReason, UpdateCardRequest, CardMetadata, \ - VirtualCardRenewRequest, ScheduleCardRevocationRequest + VirtualCardRenewRequest from tests.checkout_test_utils import assert_response, phone @@ -66,24 +66,15 @@ def test_should_renew_card(self, issuing_checkout_api, card): assert_response(response, 'id') assert response.http_metadata.status_code == 201 - def test_should_schedule_card_revocation(self, issuing_checkout_api, active_card): - request = ScheduleCardRevocationRequest() + def test_should_schedule_card_revocation_via_update(self, issuing_checkout_api, active_card): + # Card revocation scheduling is now expressed via update_card.revocation_date + # (the dedicated schedule-revocation endpoint was removed in the 2026-06-29 API). + request = UpdateCardRequest() request.revocation_date = (datetime.utcnow() + timedelta(days=7)).strftime('%Y-%m-%d') - response = issuing_checkout_api.issuing.schedule_card_revocation(active_card.id, request) + response = issuing_checkout_api.issuing.update_card(active_card.id, request) assert_response(response) - assert response.http_metadata.status_code == 200 - - def test_should_delete_card_revocation(self, issuing_checkout_api, active_card): - request = ScheduleCardRevocationRequest() - request.revocation_date = (datetime.utcnow() + timedelta(days=7)).strftime('%Y-%m-%d') - issuing_checkout_api.issuing.schedule_card_revocation(active_card.id, request) - - response = issuing_checkout_api.issuing.delete_card_revocation(active_card.id) - - assert_response(response) - assert response.http_metadata.status_code == 200 def test_should_get_digital_card(self, issuing_checkout_api): digital_card_id = 'dcr_5ngxzsynm2me3oxf73esbhda6q' diff --git a/tests/issuing/conftest.py b/tests/issuing/conftest.py index d0b4c85..b097c16 100644 --- a/tests/issuing/conftest.py +++ b/tests/issuing/conftest.py @@ -2,8 +2,8 @@ import pytest from checkout_sdk import CheckoutSdk -from checkout_sdk.common.enums import DocumentType, Currency -from checkout_sdk.issuing.cardholders import CardholderRequest, CardholderDocument, CardholderType +from checkout_sdk.common.enums import Currency +from checkout_sdk.issuing.cardholders import CardholderRequest, CardholderType from checkout_sdk.issuing.cards import CardLifetime, LifetimeUnit, VirtualCardRequest from checkout_sdk.issuing.controls import VelocityControlRequest, VelocityLimit, VelocityWindow, VelocityWindowType, \ CreateControlGroupRequest, FailIfType, MccControlGroupControl, MccLimit, MccLimitType, ControlProfileRequest @@ -28,11 +28,6 @@ def issuing_checkout_api(): @pytest.fixture(scope='module') def cardholder(issuing_checkout_api): - document = CardholderDocument() - document.type = DocumentType.NATIONAL_IDENTITY_CARD - document.front_document_id = 'file_6lbss42ezvoufcb2beo76rvwly' - document.back_document_id = 'file_aaz5pemp6326zbuvevp6qroqu4' - request = CardholderRequest() request.type = CardholderType.INDIVIDUAL request.reference = 'X-123456-N11' @@ -45,7 +40,6 @@ def cardholder(issuing_checkout_api): request.date_of_birth = '1985-05-15' request.billing_address = address() request.residency_address = address() - request.document = document cardholder = issuing_checkout_api.issuing.create_cardholder(request) diff --git a/tests/issuing/issuing_client_test.py b/tests/issuing/issuing_client_test.py index 3449358..3e98eb9 100644 --- a/tests/issuing/issuing_client_test.py +++ b/tests/issuing/issuing_client_test.py @@ -3,11 +3,11 @@ from tests._assertions import assert_api_call from checkout_sdk.issuing.cardholders import CardholderRequest from checkout_sdk.issuing.cards import PhysicalCardRequest, PasswordEnrollmentRequest, UpdateThreeDsEnrollmentRequest, \ - CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, VirtualCardRenewRequest, \ - ScheduleCardRevocationRequest + CardCredentialsQuery, RevokeRequest, SuspendRequest, UpdateCardRequest, VirtualCardRenewRequest from checkout_sdk.issuing.controls import MccControlRequest, CardControlsQuery, UpdateCardControlRequest, \ CreateControlGroupRequest, ControlGroupQueryTarget, ControlProfileRequest -from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest +from checkout_sdk.issuing.disputes import CreateDisputeRequest, EscalateDisputeRequest, AmendDisputeRequest, \ + SubmitDisputeRequest from checkout_sdk.issuing.issuing_client import IssuingClient from checkout_sdk.issuing.testing import CardAuthorizationRequest, SimulationRequest, \ CardRefundAuthorizationRequest, SimulateOobAuthenticationRequest @@ -116,19 +116,6 @@ def test_should_revoke_card(self, mocker, client: IssuingClient): assert client.revoke_card('card_id', body) == 'response' assert_api_call(mock, 'issuing/cards/card_id/revoke', body) - def test_should_schedule_card_revocation(self, mocker, client: IssuingClient): - mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') - body = ScheduleCardRevocationRequest() - - assert client.schedule_card_revocation('card_id', body) == 'response' - assert_api_call(mock, 'issuing/cards/card_id/schedule-revocation', body) - - def test_should_delete_card_revocation(self, mocker, client: IssuingClient): - mock = mocker.patch('checkout_sdk.api_client.ApiClient.delete', return_value='response') - - assert client.delete_card_revocation('card_id') == 'response' - assert_api_call(mock, 'issuing/cards/card_id/schedule-revocation') - def test_should_suspend_card(self, mocker, client: IssuingClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') body = SuspendRequest() @@ -287,6 +274,20 @@ def test_should_escalate_dispute(self, mocker, client: IssuingClient): assert client.escalate_dispute('dispute_id', body) == 'response' assert_api_call(mock, 'issuing/disputes/dispute_id/escalate', body) + def test_should_amend_dispute(self, mocker, client: IssuingClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = AmendDisputeRequest() + + assert client.amend_dispute('dispute_id', body) == 'response' + assert_api_call(mock, 'issuing/disputes/dispute_id/amend', body) + + def test_should_submit_dispute(self, mocker, client: IssuingClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = SubmitDisputeRequest() + + assert client.submit_dispute('dispute_id', body) == 'response' + assert_api_call(mock, 'issuing/disputes/dispute_id/submit', body) + def test_should_simulate_authorization(self, mocker, client: IssuingClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') body = CardAuthorizationRequest() diff --git a/tests/issuing/issuing_serialization_test.py b/tests/issuing/issuing_serialization_test.py new file mode 100644 index 0000000..b42d412 --- /dev/null +++ b/tests/issuing/issuing_serialization_test.py @@ -0,0 +1,112 @@ +import json + +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.issuing.cards import CardType, VirtualCardRequest, UpdateCardRequest +from checkout_sdk.issuing.disputes import ( + IssuingDisputeFraudType, IssuingDisputeFraudDetails, CreateDisputeRequest, + EscalateDisputeRequest, AmendDisputeRequest, SubmitDisputeRequest, +) + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +class TestIssuingSerialization: + + def test_fraud_type_enum_matches_swagger_strings(self): + expected = { + 'CARD_LOST': 'card_lost', + 'CARD_STOLEN': 'card_stolen', + 'CARD_NEVER_RECEIVED': 'card_never_received', + 'FRAUDULENT_ACCOUNT': 'fraudulent_account', + 'COUNTERFEIT_CARD': 'counterfeit_card', + 'ACCOUNT_TAKEOVER': 'account_takeover', + 'CARD_NOT_PRESENT_FRAUD': 'card_not_present_fraud', + 'MERCHANT_MISREPRESENTATION': 'merchant_misrepresentation', + 'CARDHOLDER_MANIPULATION': 'cardholder_manipulation', + 'INCORRECT_PROCESSING': 'incorrect_processing', + 'OTHER': 'other', + } + actual = {member.name: member.value for member in IssuingDisputeFraudType} + assert actual == expected + + def test_update_card_serializes_activation_and_revocation_date(self): + request = UpdateCardRequest() + request.reference = 'ref' + request.activation_date = '2026-06-01T10:00Z' + request.revocation_date = '2027-03-12' + + assert _serialize(request) == { + 'reference': 'ref', + 'activation_date': '2026-06-01T10:00Z', + 'revocation_date': '2027-03-12', + } + + def test_create_card_serializes_activation_date(self): + request = VirtualCardRequest() + request.cardholder_id = 'crh_1' + request.activation_date = '2026-06-01T10:00Z' + request.revocation_date = '2027-03-12' + + result = _serialize(request) + assert result['type'] == CardType.VIRTUAL.value + assert result['activation_date'] == '2026-06-01T10:00Z' + assert result['revocation_date'] == '2027-03-12' + + def test_create_dispute_serializes_fraud_details(self): + fraud_details = IssuingDisputeFraudDetails() + fraud_details.fraud_type = IssuingDisputeFraudType.COUNTERFEIT_CARD + fraud_details.description = 'duplicate card used' + + request = CreateDisputeRequest() + request.transaction_id = 'txn_1' + request.reason = '4808' + request.fraud_details = fraud_details + + assert _serialize(request) == { + 'transaction_id': 'txn_1', + 'reason': '4808', + 'fraud_details': {'fraud_type': 'counterfeit_card', 'description': 'duplicate card used'}, + } + + def test_escalate_dispute_serializes_fraud_details(self): + fraud_details = IssuingDisputeFraudDetails() + fraud_details.fraud_type = IssuingDisputeFraudType.ACCOUNT_TAKEOVER + + request = EscalateDisputeRequest() + request.justification = 'reason' + request.fraud_details = fraud_details + + assert _serialize(request) == { + 'justification': 'reason', + 'fraud_details': {'fraud_type': 'account_takeover'}, + } + + def test_amend_dispute_serializes_all_fields(self): + fraud_details = IssuingDisputeFraudDetails() + fraud_details.fraud_type = IssuingDisputeFraudType.OTHER + + request = AmendDisputeRequest() + request.reason = '4807' + request.amount = 1500 + request.evidence = [{'evidence_type': 'proof_of_purchase'}] + request.fraud_details = fraud_details + request.reason_change_justification = 'updated reason' + request.action_response = 'answering requested changes' + + assert _serialize(request) == { + 'reason': '4807', + 'amount': 1500, + 'evidence': [{'evidence_type': 'proof_of_purchase'}], + 'fraud_details': {'fraud_type': 'other'}, + 'reason_change_justification': 'updated reason', + 'action_response': 'answering requested changes', + } + + def test_submit_dispute_serializes_fields(self): + request = SubmitDisputeRequest() + request.reason = '4807' + request.amount = 100 + + assert _serialize(request) == {'reason': '4807', 'amount': 100} diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index f6629b1..3bd26b3 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -3,8 +3,8 @@ import os import pytest -from checkout_sdk.common.common import Address, Phone -from checkout_sdk.common.enums import Currency, Country +from checkout_sdk.common.common import Phone +from checkout_sdk.common.enums import Currency from checkout_sdk.payments.payments import PaymentType from checkout_sdk.payments.setups.setups import ( PaymentSetupsRequest, Settings, Customer, CustomerEmail, CustomerDevice, @@ -208,12 +208,7 @@ def create_payment_setups_request() -> PaymentSetupsRequest: klarna.initialization = PaymentMethodInitialization.DISABLED account_holder = KlarnaAccountHolder() - billing_address = Address() - billing_address.address_line1 = "123 High Street" - billing_address.city = "London" - billing_address.zip = "SW1A 1AA" - billing_address.country = Country.GB - account_holder.billing_address = billing_address + account_holder.name = "John Smith" klarna.account_holder = account_holder payment_methods.klarna = klarna diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index 15c5ba7..49d1efd 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -10,6 +10,10 @@ AchAccountHolderIdentification, Sepa, SepaAccountHolder, SepaMandate, SepaMandateType, GooglePay, GooglePayTokenData, ApplePay, ApplePayTokenData, ApplePayTokenDataHeader, Card, PaymentSetupAccountHolder, PaymentSetupAccountHolderType, + KlarnaAccountHolder, Bacs, BacsAccountHolder, BacsAccountHolderType, CardPresent, + CardPresentPin, PayByBank, PayByBankAction, PayByBankActionType, PayByBankBank, Stablecoin, + PaymentSetupBillingDescriptor, PaymentSetupPresentmentDetails, PaymentSetupTerminal, + PaymentSetupAmountAllocation, AmountAllocationCommission, Order, ) @@ -232,3 +236,131 @@ def test_payment_methods_container_serializes_only_set_methods(self): payment_methods.ideal = ideal assert _serialize(payment_methods) == {'ideal': {'description': 'order'}} + + # ── 2026-06-29 additions ───────────────────────────────────────────────── + + def test_klarna_account_holder_serializes_name(self): + holder = KlarnaAccountHolder() + holder.name = 'John Smith' + + assert _serialize(holder) == {'name': 'John Smith'} + + def test_bacs_serializes_nested_account_holder(self): + bacs = Bacs() + holder = BacsAccountHolder() + holder.type = BacsAccountHolderType.INDIVIDUAL + holder.first_name = 'John' + holder.last_name = 'Smith' + holder.email = 'john.smith@example.com' + bacs.account_holder = holder + bacs.account_number = '12345678' + bacs.bank_code = '200000' + bacs.country = 'GB' + bacs.currency = Currency.GBP + bacs.allow_partial_match = True + + assert _serialize(bacs) == { + 'initialization': 'disabled', + 'account_holder': { + 'type': 'individual', 'first_name': 'John', 'last_name': 'Smith', + 'email': 'john.smith@example.com', + }, + 'account_number': '12345678', + 'bank_code': '200000', + 'country': 'GB', + 'currency': 'GBP', + 'allow_partial_match': True, + } + + def test_card_present_serializes_nested_pin(self): + card_present = CardPresent() + card_present.track2 = 'track2-data' + card_present.emv = 'emv-data' + card_present.entry_mode = 'contactless' + pin = CardPresentPin() + pin.key_set_id = 'ks_1' + pin.block = 'block' + pin.block_format = 'iso0' + card_present.pin = pin + card_present.store_for_future_use = True + card_present.name = 'John Smith' + + assert _serialize(card_present) == { + 'track2': 'track2-data', + 'emv': 'emv-data', + 'entry_mode': 'contactless', + 'pin': {'key_set_id': 'ks_1', 'block': 'block', 'block_format': 'iso0'}, + 'store_for_future_use': True, + 'name': 'John Smith', + } + + def test_pay_by_bank_serializes_bank_id_and_action(self): + pay_by_bank = PayByBank() + pay_by_bank.bank_id = 'ob-natwest' + action = PayByBankAction() + action.type = PayByBankActionType.SELECT_BANK + bank = PayByBankBank() + bank.bank_id = 'ob-natwest' + bank.display_name = 'NatWest' + bank.available = True + action.banks = [bank] + pay_by_bank.action = action + + assert _serialize(pay_by_bank) == { + 'bank_id': 'ob-natwest', + 'action': { + 'type': 'select_bank', + 'banks': [{'bank_id': 'ob-natwest', 'display_name': 'NatWest', 'available': True}], + }, + } + + def test_stablecoin_serializes_status_and_flags(self): + stablecoin = Stablecoin() + stablecoin.status = 'available' + stablecoin.flags = ['flag_a'] + + assert _serialize(stablecoin) == {'status': 'available', 'flags': ['flag_a']} + + def test_billing_descriptor_presentment_terminal_serialize(self): + billing_descriptor = PaymentSetupBillingDescriptor() + billing_descriptor.name = 'ACME' + billing_descriptor.city = 'London' + billing_descriptor.reference = 'REF-1' + + presentment = PaymentSetupPresentmentDetails() + presentment.amount = 1000 + presentment.currency = 'GBP' + + terminal = PaymentSetupTerminal() + terminal.id = 'term_1' + terminal.local_date_time = '2026-06-01T10:00:00' + + assert _serialize(billing_descriptor) == {'name': 'ACME', 'city': 'London', 'reference': 'REF-1'} + assert _serialize(presentment) == {'amount': 1000, 'currency': 'GBP'} + assert _serialize(terminal) == {'id': 'term_1', 'local_date_time': '2026-06-01T10:00:00'} + + def test_order_serializes_amount_allocations(self): + allocation = PaymentSetupAmountAllocation() + allocation.id = 'ent_w4jelhppmfiufdnatam37wrfc4' + allocation.amount = 1000 + allocation.reference = 'ORD-5023-4E89' + commission = AmountAllocationCommission() + commission.amount = 100 + commission.percentage = 2.5 + allocation.commission = commission + + order = Order() + order.tipping_amount = 200 + order.surcharge_amount = 50 + order.amount_allocations = [allocation] + + assert _serialize(order) == { + 'tipping_amount': 200, + 'surcharge_amount': 50, + 'amount_allocations': [{ + 'id': 'ent_w4jelhppmfiufdnatam37wrfc4', + 'amount': 1000, + 'reference': 'ORD-5023-4E89', + 'commission': {'amount': 100, 'percentage': 2.5}, + }] + }