From f93da769bc767665d471d212124c620b498d7dd2 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Mon, 26 Oct 2026 15:21:07 +0100 Subject: [PATCH 1/7] feat(setups): add klarna and stcpay actions, tamara fields, payment type Adds Klarna iban and action, stc pay action and the Tamara fields; payment_type gets its own enum with the six lowercase spec values; local_date_time and last_payment_amount follow the spec types. Members the spec does not have are deprecated. --- checkout_sdk/payments/setups/setups.py | 235 ++++++++++--- .../setups/payment_setups_integration_test.py | 9 +- .../payment_setups_serialization_test.py | 330 +++++++++++++++++- 3 files changed, 516 insertions(+), 58 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 661a7e0c..e69479ca 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -4,7 +4,7 @@ from checkout_sdk.common.common import Address, Phone from checkout_sdk.common.enums import Currency -from checkout_sdk.payments.payments import PaymentType, PreferredSchema, ShippingDetails +from checkout_sdk.payments.payments import PreferredSchema, ShippingDetails # Enums @@ -14,6 +14,21 @@ class PaymentMethodInitialization(str, Enum): ENABLED = 'enabled' +class PaymentSetupPaymentType(str, Enum): + """The type of payment on a Payment Setup. + + The Payment Setup schema declares these values lowercase. The /payments request uses + payments.payments.PaymentType, whose capitalized values (for example PayLater) the Payment + Setups API rejects, so the two enums are kept apart. + """ + REGULAR = 'regular' + RECURRING = 'recurring' + MOTO = 'moto' + INSTALLMENT = 'installment' + PAY_LATER = 'pay_later' + UNSCHEDULED = 'unscheduled' + + # Shared payment-method enums class TerminalType(str, Enum): """The client-side terminal type of the wallet payment methods.""" @@ -126,9 +141,10 @@ class MerchantAccount: # The total number of orders made by the customer. # [Optional] total_order_count: int - # The payment amount of the customer's most recent transaction. + # The payment amount of the customer's most recent transaction. A decimal number, for + # example 55.99. # [Optional] - last_payment_amount: int + last_payment_amount: float class Customer: @@ -164,15 +180,17 @@ class Customer: # Payment Method Common entities class PaymentMethodAction: - """The next available action for the Klarna, PayPal or instrument payment method. + """The next available action for the PayPal or instrument payment method. - The Cash App and Pay by Bank methods have their own action types (CashAppAction, - PayByBankAction), because their actions carry different properties. + The Klarna, stc pay, Cash App and Pay by Bank methods have their own action types + (KlarnaAction, StcpayAction, CashAppAction, PayByBankAction), because their actions carry + different properties. client_token and session_id are not part of the PayPal or instrument + action schemas. """ # The type of action. # [Optional] # readOnly - # Enum: "sdk" (Klarna, PayPal) "set_instrument" (instrument) + # Enum: "sdk" (PayPal) "set_instrument" (instrument) type: str # The client token for the Klarna SDK. # [Optional] @@ -189,7 +207,12 @@ class PaymentMethodAction: class PaymentMethodOption: - """A payment method option. Not present in the current Payment Setup schema.""" + """A payment method option. + + Deprecated: Not part of any payment method schema in the Payment Setups API specification; + the API ignores it on requests. There is no replacement. Will be removed in the next major + version. + """ # The identifier of the payment method option. # [Optional] id: str @@ -202,7 +225,12 @@ class PaymentMethodOption: class PaymentMethodOptions: - """The payment method options. Not present in the current Payment Setup schema.""" + """The payment method options. + + Deprecated: Not part of any payment method schema in the Payment Setups API specification; + the API ignores it on requests. Klarna and stc pay expose action instead; the other methods + have no replacement. Will be removed in the next major version. + """ # The SDK payment method option. # [Optional] sdk: PaymentMethodOption @@ -217,6 +245,22 @@ class PaymentMethodOptions: pay_now: PaymentMethodOption +# Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). +# Distinct from PaymentMethodBase, which also carries the writable `initialization` field. +class PaymentSetupPaymentMethod: + """Base for payment methods that expose only status and flags (swagger + PaymentSetupPaymentMethod).""" + # The payment method status. + # [Optional] + # readOnly + # Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" + status: str + # The list of error codes or indicators that highlight missing or invalid information. + # [Optional] + # readOnly + flags: list # list of str + + class PaymentMethodBase: """Base for payment methods that carry status, flags and the writable initialization field (swagger PaymentSetupPaymentMethod plus PaymentMethodInitialization).""" @@ -230,7 +274,8 @@ class PaymentMethodBase: # readOnly flags: list # list of str # The initialization state of the payment method. When you create a Payment Setup, this - # defaults to disabled. + # defaults to disabled. On update the API replaces the payment methods, so send enabled + # again to keep the method enabled. # [Optional] # Default: "disabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED @@ -245,8 +290,37 @@ class KlarnaAccountHolder: name: str +class KlarnaActionType(str, Enum): + """The type of the Klarna action.""" + SDK = 'sdk' + + +class KlarnaAction: + """The next available action for the Klarna payment method. Response only.""" + # The type of action. + # [Optional] + # readOnly + type: KlarnaActionType + # The client token for the Klarna SDK. + # [Optional] + # readOnly + client_token: str + # The session ID. + # [Optional] + # readOnly + session_id: str + + class Klarna(PaymentMethodBase): """The Klarna payment method's details and configuration.""" + # The next available action for the payment method. + # [Optional] + # readOnly + action: KlarnaAction + # The IBAN of the account used to complete Klarna verification. + # [Optional] + # readOnly + iban: str def __init__(self): super().__init__() @@ -255,21 +329,45 @@ def __init__(self): # [Optional] # readOnly self.account_holder: KlarnaAccountHolder - # The payment method options. Not present in the current Klarna schema. + # Deprecated: Not part of the Klarna schema in the Payment Setups API specification; the + # API ignores it on requests. Use action instead. Will be removed in the next major + # version. + # The payment method options. # [Optional] self.payment_method_options: PaymentMethodOptions # Stcpay entities +class StcpayActionType(str, Enum): + """The type of the stc pay action.""" + OTP = 'otp' + + +class StcpayAction: + """The next available action for the stc pay payment method. Response only.""" + # The type of action. + # [Optional] + # readOnly + type: StcpayActionType + + class Stcpay(PaymentMethodBase): """The stc pay payment method's details and configuration.""" + # The next available action for the payment method. + # [Optional] + # readOnly + action: StcpayAction def __init__(self): super().__init__() # The one-time password (OTP) for stc pay. # [Optional] + # writeOnly self.otp: str - # The payment method options. Not present in the current stc pay schema. + # Deprecated: Not part of the stc pay schema in the Payment Setups API specification; + # the API ignores it on requests. Use action instead. Will be removed in the next major + # version. + # The payment method options. # [Optional] self.payment_method_options: PaymentMethodOptions @@ -283,24 +381,42 @@ class Tabby(PaymentMethodBase): def __init__(self): super().__init__() - # The payment method options. Not present in the current Tabby schema. + # Deprecated: Not part of the Tabby schema in the Payment Setups API specification; the + # API ignores it on requests. There is no replacement. Will be removed in the next major + # version. + # The payment method options. # [Optional] self.payment_method_options: PaymentMethodOptions # Bizum entities -class Bizum(PaymentMethodBase): +class Bizum(PaymentSetupPaymentMethod): """The Bizum payment method's details and configuration.""" + # Deprecated: Not part of the Bizum schema in the Payment Setups API specification; the API + # ignores it on requests. There is no replacement. Will be removed in the next major version. + # The initialization state of the payment method. + # [Optional] + # Default: "disabled" + initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED def __init__(self): super().__init__() - # The payment method options. Not present in the current Bizum schema. + # Deprecated: Not part of the Bizum schema in the Payment Setups API specification; the + # API ignores it on requests. There is no replacement. Will be removed in the next major + # version. + # The payment method options. # [Optional] self.payment_method_options: PaymentMethodOptions -class Blik(PaymentMethodBase): +class Blik(PaymentSetupPaymentMethod): """The Blik payment method's details and configuration.""" + # Deprecated: Not part of the Blik schema in the Payment Setups API specification; the API + # ignores it on requests. There is no replacement. Will be removed in the next major version. + # The initialization state of the payment method. + # [Optional] + # Default: "disabled" + initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED # The 6-digit BLIK code generated by the customer's banking app. # [Optional] # ^[0-9]{6}$ @@ -339,22 +455,6 @@ class Paypal(PaymentMethodBase): action: PaymentMethodAction -# Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). -# Distinct from PaymentMethodBase, which also carries the writable `initialization` field. -class PaymentSetupPaymentMethod: - """Base for payment methods that expose only status and flags (swagger - PaymentSetupPaymentMethod).""" - # The payment method status. - # [Optional] - # readOnly - # Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" - status: str - # The list of error codes or indicators that highlight missing or invalid information. - # [Optional] - # readOnly - flags: list # list of str - - class KnetLanguage(str, Enum): """The customer's preferred KNET language, as an ISO 639-1 code.""" EN = 'en' @@ -403,14 +503,22 @@ class PaymentSetupInstrument(PaymentSetupPaymentMethod): # [Optional] # readOnly type: str - # The customer's phone number. Not present in the current instrument schema. + # Deprecated: Not part of the instrument schema in the Payment Setups API + # specification; the API ignores it on requests. There is no replacement. Will be removed in + # the next major version. + # The customer's phone number. # [Optional] phone: Phone - # Account holder details for the payment. Not present in the current instrument schema. + # Deprecated: Not part of the instrument schema in the Payment Setups API + # specification; the API ignores it on requests. There is no replacement. Will be removed in + # the next major version. + # Account holder details for the payment. # [Optional] account_holder: PaymentSetupAccountHolder + # Deprecated: Not part of the instrument schema in the Payment Setups API + # specification; the API ignores it on requests. There is no replacement. Will be removed in + # the next major version. # Indicates whether to use the Real-Time Account Updater to update the card information. - # Not present in the current instrument schema. # [Optional] allow_update: bool # The next available action for the payment method. Its type is set_instrument. @@ -450,9 +558,31 @@ class MobilePay(PaymentSetupPaymentMethod): pass -class Tamara(PaymentSetupPaymentMethod): - """The Tamara payment method's details and configuration.""" - pass +class TamaraRiskAssessment: + """Risk signals about the customer, forwarded to Tamara with the payment.""" + # Specifies whether the customer is a premium customer. + # [Optional] + premium_customer: bool + # The customer's risk score. + # [Optional] + risk_score: int + # The number of orders the customer has made in the last 24 hours. + # [Optional] + order_count_last24h: int + # The number of devices linked to the customer. + # [Optional] + linked_device_count: int + + +class Tamara(PaymentMethodBase): + """The Tamara payment method's details and configuration. + + initialization is inherited from PaymentMethodBase: it defaults to disabled, and on update + the API replaces the payment methods, so send enabled again to keep the method enabled. + """ + # Risk signals about the customer, forwarded to Tamara with the payment. + # [Optional] + risk_assessment: TamaraRiskAssessment class MBWay(PaymentSetupPaymentMethod): @@ -937,8 +1067,11 @@ class Bacs(PaymentMethodBase): # Card Present entities class CardPresentPin: - """The PIN block details of a card-present payment. Not present in the current Payment Setup - schema.""" + """The PIN block details of a card-present payment. + + Deprecated: card_present is no longer part of the Payment Setups API specification. There is + no replacement. Will be removed in the next major version. + """ # The identifier of the key set used to encrypt the PIN block. # [Optional] key_set_id: str @@ -951,7 +1084,11 @@ class CardPresentPin: class CardPresent(PaymentSetupPaymentMethod): - """The card-present payment method. Not present in the current Payment Setup schema.""" + """The card-present payment method. + + Deprecated: card_present is no longer part of the Payment Setups API specification. There is + no replacement. Will be removed in the next major version. + """ # The track 2 data read from the card. # [Optional] track2: str @@ -1463,7 +1600,9 @@ class PaymentMethods: # The Bacs payment method's details and configuration. # [Optional] bacs: Bacs - # The card-present payment method. Not present in the current Payment Setup schema. + # Deprecated: card_present is no longer part of the Payment Setups API specification. There is + # no replacement. Will be removed in the next major version. + # The card-present payment method. # [Optional] card_present: CardPresent # The Pay by Bank (Open Banking) payment method's details and configuration. @@ -1912,10 +2051,11 @@ class PaymentSetupTerminal: # [Optional] # min 8 characters, max 8 characters id: str - # The local date and time on the terminal, in ISO 8601 format. + # The local date and time on the terminal, in ISO 8601 format. Pass a timezone-aware + # datetime so the offset is sent; microseconds are not sent. # [Optional] - # Format: date-time - local_date_time: str + # Format: date-time (RFC 3339) + local_date_time: datetime class AccountFundingTransactionIdentificationType(str, Enum): @@ -2033,9 +2173,10 @@ class PaymentSetupsRequest: currency: Currency # The type of payment. You must provide this field for card payments in which the # cardholder is not present. For example, if the transaction is a recurring payment, or a - # mail order/telephone order (MOTO) payment. Defaults to regular. + # mail order/telephone order (MOTO) payment. # [Optional] - payment_type: PaymentType + # Default: "regular" + payment_type: PaymentSetupPaymentType # A reference you can use to identify the payment. For example, an order number. # [Optional] # max 80 characters diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index 40b5fa94..13e17808 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -5,12 +5,11 @@ 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, PaymentMethods, Klarna, KlarnaAccountHolder, PaymentMethodInitialization, Ideal, Knet, KnetLanguage, Bancontact, P24, P24AccountHolder, - CashApp, CustomerDeviceClient, OsType + CashApp, CustomerDeviceClient, OsType, PaymentSetupPaymentType ) from tests.checkout_test_utils import assert_response, new_uuid @@ -37,7 +36,7 @@ def test_should_create_payment_setup(default_api): assert response.processing_channel_id == request.processing_channel_id assert response.amount == request.amount assert response.currency == request.currency - assert str(response.payment_type).lower() == request.payment_type.lower() + assert response.payment_type == request.payment_type assert response.reference == request.reference assert response.description == request.description @@ -91,7 +90,7 @@ def test_should_get_payment_setup(default_api): assert response.processing_channel_id == create_request.processing_channel_id assert response.amount == create_request.amount assert response.currency == create_request.currency - assert str(response.payment_type).lower() == create_request.payment_type.lower() + assert response.payment_type == create_request.payment_type assert response.reference == create_request.reference assert response.description == create_request.description @@ -248,7 +247,7 @@ def create_payment_setups_request() -> PaymentSetupsRequest: request.processing_channel_id = os.getenv('CHECKOUT_PROCESSING_CHANNEL_ID') request.amount = 1000 request.currency = Currency.GBP - request.payment_type = PaymentType.REGULAR + request.payment_type = PaymentSetupPaymentType.REGULAR request.reference = f"TEST-REF-{new_uuid()[:6]}" request.description = "Integration test payment setup" diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index cdcfe0c7..d3efb303 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -1,5 +1,5 @@ import json -from datetime import datetime +from datetime import datetime, timedelta, timezone import pytest @@ -27,9 +27,11 @@ PaymentSetupsRequest, Customer, CustomerEmail, CustomerDevice, CustomerDeviceClient, PaymentMethodInitialization, CashApp, CashAppAction, CashAppActionType, CashAppAddress, CashAppCustomerProfile, PaymentSetupRisk, CardDetails, CardToken, WalletToken, PaymentSetupCardScheme, - GooglePayPreferredScheme, + GooglePayPreferredScheme, Klarna, KlarnaAction, KlarnaActionType, Stcpay, StcpayAction, + StcpayActionType, Tabby, Tamara, TamaraRiskAssessment, Bizum, Blik, PaymentMethodBase, + PaymentSetupPaymentMethod, PaymentMethodOptions, PaymentMethodOption, PaymentSetupPaymentType, ) -from checkout_sdk.payments.payments import PreferredSchema +from checkout_sdk.payments.payments import PaymentType, PreferredSchema def _serialize(obj): @@ -235,7 +237,11 @@ def test_card_serializes_writable_fields_and_account_holder(self): 'account_holder': {'type': 'individual', 'first_name': 'John', 'last_name': 'Smith'}, } - def test_instrument_serializes_id_and_phone(self): + def test_instrument_deprecated_fields_are_still_declared_and_sent(self): + # phone, account_holder and allow_update are not in the instrument schema. They stay + # declared, deprecated, and are still sent when set. + for field in ('phone', 'account_holder', 'allow_update'): + assert field in PaymentSetupInstrument.__annotations__, field instrument = PaymentSetupInstrument() instrument.id = 'src_wmlfc3zttb4uzmk6snpwb43jbi' instrument.allow_update = True @@ -243,11 +249,15 @@ def test_instrument_serializes_id_and_phone(self): phone.country_code = '+44' phone.number = '207 946 0000' instrument.phone = phone + holder = PaymentSetupAccountHolder() + holder.first_name = 'John' + instrument.account_holder = holder assert _serialize(instrument) == { 'id': 'src_wmlfc3zttb4uzmk6snpwb43jbi', 'allow_update': True, 'phone': {'country_code': '+44', 'number': '207 946 0000'}, + 'account_holder': {'first_name': 'John'}, } def test_payment_methods_container_serializes_only_set_methods(self): @@ -293,7 +303,8 @@ def test_bacs_serializes_nested_account_holder(self): 'allow_partial_match': True, } - def test_card_present_serializes_nested_pin(self): + def test_deprecated_card_present_still_serializes_nested_pin(self): + # card_present is not in the Payment Setup schema. It stays, deprecated, and is still sent. card_present = CardPresent() card_present.track2 = 'track2-data' card_present.emv = 'emv-data' @@ -314,6 +325,10 @@ def test_card_present_serializes_nested_pin(self): 'store_for_future_use': True, 'name': 'John Smith', } + assert 'card_present' in PaymentMethods.__annotations__ + payment_methods = PaymentMethods() + payment_methods.card_present = CardPresent() + assert '"card_present"' in json.dumps(payment_methods, cls=JsonSerializer) def test_pay_by_bank_serializes_bank_id_and_action(self): pay_by_bank = PayByBank() @@ -897,7 +912,7 @@ def test_customer_round_trip_keeps_every_property(self): merchant_account.first_transaction_date = '2023-09-15' merchant_account.last_transaction_date = '2025-03-28' merchant_account.total_order_count = 6 - merchant_account.last_payment_amount = 5599 + merchant_account.last_payment_amount = 55.99 customer = Customer() customer.country = 'GB' @@ -1410,3 +1425,306 @@ def test_auto_confirm_and_metadata_serialize_under_the_spec_keys(self): assert read.metadata.coupon_code == 'NY2018' assert read.metadata.partner_id == 123989 assert read.metadata.vip is True + + +class TestPaymentSetupMethodFieldsSerialization: + """Klarna iban and action, stc pay action, Tamara initialization and risk_assessment, and the + deprecated payment method members that stay declared and sent.""" + + def test_klarna_serializes_action_and_iban_under_the_spec_keys(self): + action = KlarnaAction() + action.type = KlarnaActionType.SDK + action.client_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzZXNzaW9uX2lkIiA6ICIw' + action.session_id = '0b1d9815-165e-42e2-8867-35bc03789e00' + holder = KlarnaAccountHolder() + holder.name = 'John Smith' + klarna = Klarna() + klarna.status = 'ready' + klarna.flags = [] + klarna.initialization = PaymentMethodInitialization.ENABLED + klarna.account_holder = holder + klarna.action = action + klarna.iban = 'DE89370400440532013000' + + body = json.dumps(klarna, cls=JsonSerializer) + + for key in ('"iban"', '"action"', '"client_token"', '"session_id"', '"account_holder"', + '"initialization"'): + assert key in body, key + assert 'clientToken' not in body + assert 'sessionId' not in body + assert json.loads(body) == { + 'status': 'ready', + 'flags': [], + 'initialization': 'enabled', + 'account_holder': {'name': 'John Smith'}, + 'action': { + 'type': 'sdk', + 'client_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzZXNzaW9uX2lkIiA6ICIw', + 'session_id': '0b1d9815-165e-42e2-8867-35bc03789e00', + }, + 'iban': 'DE89370400440532013000', + } + + read = ResponseWrapper(None, json.loads(body)) + assert read.status == klarna.status + assert read.flags == klarna.flags + assert read.initialization == klarna.initialization + assert read.account_holder.name == holder.name + assert read.action.type == action.type + assert read.action.client_token == action.client_token + assert read.action.session_id == action.session_id + assert read.iban == klarna.iban + + def test_klarna_spec_example_reads(self): + read = ResponseWrapper(None, { + 'status': 'ready', + 'initialization': 'disabled', + 'action': { + 'type': 'sdk', + 'client_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzZXNzaW9uX2lkIiA6ICIw', + 'session_id': '0b1d9815-165e-42e2-8867-35bc03789e00', + }, + 'iban': 'DE89370400440532013000', + }) + + assert read.action.type == KlarnaActionType.SDK + assert read.action.client_token == 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJzZXNzaW9uX2lkIiA6ICIw' + assert read.action.session_id == '0b1d9815-165e-42e2-8867-35bc03789e00' + assert read.iban == 'DE89370400440532013000' + assert read.initialization == PaymentMethodInitialization.DISABLED + + def test_stcpay_serializes_otp_and_action_under_the_spec_keys(self): + action = StcpayAction() + action.type = StcpayActionType.OTP + stcpay = Stcpay() + stcpay.otp = '123456' + stcpay.action = action + + body = json.dumps(stcpay, cls=JsonSerializer) + + assert '"otp": "123456"' in body + assert '"action": {"type": "otp"}' in body + assert json.loads(body) == { + 'initialization': 'disabled', + 'otp': '123456', + 'action': {'type': 'otp'}, + } + + read = ResponseWrapper(None, json.loads(body)) + assert read.otp == stcpay.otp + assert read.action.type == StcpayActionType.OTP + assert read.initialization == PaymentMethodInitialization.DISABLED + + def test_stcpay_spec_example_reads(self): + read = ResponseWrapper(None, {'status': 'action_required', 'action': {'type': 'otp'}}) + + assert read.status == 'action_required' + assert read.action.type == StcpayActionType.OTP + + def test_new_fields_are_declared_with_their_types(self): + # Assigning an undeclared attribute still serializes, so the declarations are checked here. + assert Klarna.__annotations__['action'] is KlarnaAction + assert Klarna.__annotations__['iban'] is str + assert KlarnaAction.__annotations__ == { + 'type': KlarnaActionType, 'client_token': str, 'session_id': str, + } + assert Stcpay.__annotations__['action'] is StcpayAction + assert StcpayAction.__annotations__ == {'type': StcpayActionType} + assert Tamara.__annotations__['risk_assessment'] is TamaraRiskAssessment + assert TamaraRiskAssessment.__annotations__ == { + 'premium_customer': bool, 'risk_score': int, 'order_count_last24h': int, + 'linked_device_count': int, + } + + def test_action_type_enums_have_every_spec_value(self): + assert [t.value for t in KlarnaActionType] == ['sdk'] + assert [t.value for t in StcpayActionType] == ['otp'] + + def test_tamara_serializes_initialization_and_risk_assessment_under_the_spec_keys(self): + risk = TamaraRiskAssessment() + risk.premium_customer = False + risk.risk_score = 42 + risk.order_count_last24h = 0 + risk.linked_device_count = 2 + tamara = Tamara() + tamara.status = 'ready' + tamara.flags = [] + tamara.initialization = PaymentMethodInitialization.ENABLED + tamara.risk_assessment = risk + + body = json.dumps(tamara, cls=JsonSerializer) + + for key in ('"initialization": "enabled"', '"risk_assessment"', '"premium_customer"', + '"risk_score"', '"order_count_last24h"', '"linked_device_count"'): + assert key in body, key + assert 'riskAssessment' not in body + assert 'order_count_last_24h' not in body + assert json.loads(body) == { + 'status': 'ready', + 'flags': [], + 'initialization': 'enabled', + 'risk_assessment': { + 'premium_customer': False, + 'risk_score': 42, + 'order_count_last24h': 0, + 'linked_device_count': 2, + }, + } + + read = ResponseWrapper(None, json.loads(body)) + assert read.status == tamara.status + assert read.flags == tamara.flags + assert read.initialization == tamara.initialization + for field in ('premium_customer', 'risk_score', 'order_count_last24h', 'linked_device_count'): + assert getattr(read.risk_assessment, field) == getattr(risk, field), field + + def test_tamara_initialization_defaults_to_disabled(self): + assert isinstance(Tamara(), PaymentMethodBase) + assert _serialize(Tamara()) == {'initialization': 'disabled'} + + def test_tamara_spec_example_reads(self): + read = ResponseWrapper(None, { + 'status': 'initialization_required', + 'flags': [], + 'initialization': 'disabled', + 'risk_assessment': { + 'premium_customer': False, + 'risk_score': 42, + 'order_count_last24h': 0, + 'linked_device_count': 2, + }, + }) + + assert read.initialization == PaymentMethodInitialization.DISABLED + assert read.risk_assessment.premium_customer is False + assert read.risk_assessment.risk_score == 42 + assert read.risk_assessment.order_count_last24h == 0 + assert read.risk_assessment.linked_device_count == 2 + + def test_payment_methods_container_sends_the_new_fields_under_the_method_keys(self): + payment_methods = PaymentMethods() + klarna = Klarna() + klarna.iban = 'DE89370400440532013000' + payment_methods.klarna = klarna + tamara = Tamara() + risk = TamaraRiskAssessment() + risk.risk_score = 42 + tamara.risk_assessment = risk + payment_methods.tamara = tamara + + assert _serialize(payment_methods) == { + 'klarna': {'initialization': 'disabled', 'iban': 'DE89370400440532013000'}, + 'tamara': {'initialization': 'disabled', 'risk_assessment': {'risk_score': 42}}, + } + + @pytest.mark.parametrize('method_type', [Bizum, Blik]) + def test_deprecated_initialization_is_still_declared_and_sent(self, method_type): + # Bizum and Blik schemas have no initialization. The member stays, deprecated, with the + # same default, so the wire is unchanged. + assert issubclass(method_type, PaymentSetupPaymentMethod) + assert not issubclass(method_type, PaymentMethodBase) + assert 'initialization' in method_type.__annotations__ + assert _serialize(method_type()) == {'initialization': 'disabled'} + method = method_type() + method.initialization = PaymentMethodInitialization.ENABLED + assert _serialize(method) == {'initialization': 'enabled'} + + def test_blik_still_serializes_partner_code(self): + blik = Blik() + blik.partner_code = '123456' + + assert _serialize(blik) == {'initialization': 'disabled', 'partner_code': '123456'} + + @pytest.mark.parametrize('method_type', [Klarna, Stcpay, Tabby, Bizum]) + def test_deprecated_payment_method_options_are_still_sent(self, method_type): + option = PaymentMethodOption() + option.id = 'pmo_1' + options = PaymentMethodOptions() + options.sdk = option + method = method_type() + method.payment_method_options = options + + assert _serialize(method)['payment_method_options'] == {'sdk': {'id': 'pmo_1'}} + + +class TestPaymentSetupPaymentTypeSerialization: + """payment_type on the Payment Setup request uses its own lowercase enum.""" + + def test_payment_type_has_every_spec_value(self): + assert [t.value for t in PaymentSetupPaymentType] == [ + 'regular', 'recurring', 'moto', 'installment', 'pay_later', 'unscheduled', + ] + + @pytest.mark.parametrize('payment_type', list(PaymentSetupPaymentType)) + def test_every_payment_type_is_sent_lowercase_and_reads_back(self, payment_type): + request = PaymentSetupsRequest() + request.payment_type = payment_type + + body = json.dumps(request, cls=JsonSerializer) + + assert '"payment_type": "{}"'.format(payment_type.value) in body + read = ResponseWrapper(None, json.loads(body)) + assert read.payment_type == payment_type + + def test_pay_later_is_sent_as_the_spec_value(self): + request = PaymentSetupsRequest() + request.payment_type = PaymentSetupPaymentType.PAY_LATER + + body = json.dumps(request, cls=JsonSerializer) + + assert '"payment_type": "pay_later"' in body + assert 'PayLater' not in body + + def test_request_annotation_is_the_setups_enum(self): + assert PaymentSetupsRequest.__annotations__['payment_type'] is PaymentSetupPaymentType + assert PaymentSetupsRequest.__annotations__['payment_type'] is not PaymentType + + def test_spec_example_reads(self): + read = ResponseWrapper(None, {'payment_type': 'regular'}) + + assert read.payment_type == PaymentSetupPaymentType.REGULAR + + +class TestPaymentSetupTerminalAndMerchantAccountTypes: + """terminal.local_date_time as a date-time and merchant_account.last_payment_amount as a + number.""" + + def test_terminal_local_date_time_serializes_a_timezone_aware_datetime(self): + terminal = PaymentSetupTerminal() + terminal.id = '12345678' + terminal.local_date_time = datetime(2026, 5, 26, 13, 5, 14, tzinfo=timezone(timedelta(hours=1))) + + body = json.dumps(terminal, cls=JsonSerializer) + + assert '"local_date_time": "2026-05-26T13:05:14+01:00"' in body + assert 'localDateTime' not in body + read = ResponseWrapper(None, json.loads(body)) + assert read.id == terminal.id + assert datetime.fromisoformat(read.local_date_time) == terminal.local_date_time + + def test_terminal_local_date_time_still_sends_a_string_unchanged(self): + terminal = PaymentSetupTerminal() + terminal.local_date_time = '2026-05-26T13:05:14.0000000+01:00' + + assert _serialize(terminal) == {'local_date_time': '2026-05-26T13:05:14.0000000+01:00'} + + def test_terminal_spec_example_reads(self): + read = ResponseWrapper(None, {'id': '12345678', 'local_date_time': '2026-05-26T13:05:14.0000000+01:00'}) + + assert read.id == '12345678' + assert read.local_date_time == '2026-05-26T13:05:14.0000000+01:00' + + def test_terminal_annotation_is_datetime(self): + assert PaymentSetupTerminal.__annotations__['local_date_time'] is datetime + + def test_last_payment_amount_serializes_a_decimal_number(self): + merchant_account = MerchantAccount() + merchant_account.last_payment_amount = 55.99 + + body = json.dumps(merchant_account, cls=JsonSerializer) + + assert '"last_payment_amount": 55.99' in body + assert 'lastPaymentAmount' not in body + assert ResponseWrapper(None, json.loads(body)).last_payment_amount == 55.99 + assert MerchantAccount.__annotations__['last_payment_amount'] is float From 8470818e08d422f76ec7ee0d7b5b0699df36145e Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 11:02:03 +0100 Subject: [PATCH 2/7] fix(setups): keep the original method bases and the PaymentType import --- checkout_sdk/payments/setups/setups.py | 55 ++++++++++--------- .../payment_setups_serialization_test.py | 23 ++++++-- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index e69479ca..e8ef6a20 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -5,6 +5,9 @@ from checkout_sdk.common.common import Address, Phone from checkout_sdk.common.enums import Currency from checkout_sdk.payments.payments import PreferredSchema, ShippingDetails +# Deprecated: PaymentType is re-exported here only so existing imports from this module keep +# working. Payment Setups use PaymentSetupPaymentType. Will be removed in the next major version. +from checkout_sdk.payments.payments import PaymentType # noqa: F401 # Enums @@ -245,22 +248,6 @@ class PaymentMethodOptions: pay_now: PaymentMethodOption -# Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). -# Distinct from PaymentMethodBase, which also carries the writable `initialization` field. -class PaymentSetupPaymentMethod: - """Base for payment methods that expose only status and flags (swagger - PaymentSetupPaymentMethod).""" - # The payment method status. - # [Optional] - # readOnly - # Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" - status: str - # The list of error codes or indicators that highlight missing or invalid information. - # [Optional] - # readOnly - flags: list # list of str - - class PaymentMethodBase: """Base for payment methods that carry status, flags and the writable initialization field (swagger PaymentSetupPaymentMethod plus PaymentMethodInitialization).""" @@ -274,8 +261,7 @@ class PaymentMethodBase: # readOnly flags: list # list of str # The initialization state of the payment method. When you create a Payment Setup, this - # defaults to disabled. On update the API replaces the payment methods, so send enabled - # again to keep the method enabled. + # defaults to disabled. # [Optional] # Default: "disabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED @@ -390,7 +376,7 @@ def __init__(self): # Bizum entities -class Bizum(PaymentSetupPaymentMethod): +class Bizum(PaymentMethodBase): """The Bizum payment method's details and configuration.""" # Deprecated: Not part of the Bizum schema in the Payment Setups API specification; the API # ignores it on requests. There is no replacement. Will be removed in the next major version. @@ -409,7 +395,7 @@ def __init__(self): self.payment_method_options: PaymentMethodOptions -class Blik(PaymentSetupPaymentMethod): +class Blik(PaymentMethodBase): """The Blik payment method's details and configuration.""" # Deprecated: Not part of the Blik schema in the Payment Setups API specification; the API # ignores it on requests. There is no replacement. Will be removed in the next major version. @@ -455,6 +441,22 @@ class Paypal(PaymentMethodBase): action: PaymentMethodAction +# Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). +# Distinct from PaymentMethodBase, which also carries the writable `initialization` field. +class PaymentSetupPaymentMethod: + """Base for payment methods that expose only status and flags (swagger + PaymentSetupPaymentMethod).""" + # The payment method status. + # [Optional] + # readOnly + # Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" + status: str + # The list of error codes or indicators that highlight missing or invalid information. + # [Optional] + # readOnly + flags: list # list of str + + class KnetLanguage(str, Enum): """The customer's preferred KNET language, as an ISO 639-1 code.""" EN = 'en' @@ -574,12 +576,13 @@ class TamaraRiskAssessment: linked_device_count: int -class Tamara(PaymentMethodBase): - """The Tamara payment method's details and configuration. - - initialization is inherited from PaymentMethodBase: it defaults to disabled, and on update - the API replaces the payment methods, so send enabled again to keep the method enabled. - """ +class Tamara(PaymentSetupPaymentMethod): + """The Tamara payment method's details and configuration.""" + # The initialization state of the payment method. When you create a Payment Setup, this + # defaults to disabled. + # [Optional] + # Default: "disabled" + initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED # Risk signals about the customer, forwarded to Tamara with the payment. # [Optional] risk_assessment: TamaraRiskAssessment diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index d3efb303..8bb068b6 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -1580,7 +1580,11 @@ def test_tamara_serializes_initialization_and_risk_assessment_under_the_spec_key assert getattr(read.risk_assessment, field) == getattr(risk, field), field def test_tamara_initialization_defaults_to_disabled(self): - assert isinstance(Tamara(), PaymentMethodBase) + # Tamara keeps the base it had before (status and flags only) and declares + # initialization itself, so isinstance checks against that base still hold. + assert isinstance(Tamara(), PaymentSetupPaymentMethod) + assert not isinstance(Tamara(), PaymentMethodBase) + assert Tamara.__annotations__['initialization'] is PaymentMethodInitialization assert _serialize(Tamara()) == {'initialization': 'disabled'} def test_tamara_spec_example_reads(self): @@ -1620,11 +1624,12 @@ def test_payment_methods_container_sends_the_new_fields_under_the_method_keys(se @pytest.mark.parametrize('method_type', [Bizum, Blik]) def test_deprecated_initialization_is_still_declared_and_sent(self, method_type): - # Bizum and Blik schemas have no initialization. The member stays, deprecated, with the - # same default, so the wire is unchanged. - assert issubclass(method_type, PaymentSetupPaymentMethod) - assert not issubclass(method_type, PaymentMethodBase) - assert 'initialization' in method_type.__annotations__ + # Bizum and Blik schemas have no initialization. They keep PaymentMethodBase as their + # base, and redeclare initialization (deprecated) with the same type and default, so + # callers and the wire are unchanged. + assert issubclass(method_type, PaymentMethodBase) + assert method_type.__annotations__['initialization'] is PaymentMethodInitialization + assert method_type.initialization is PaymentMethodBase.initialization assert _serialize(method_type()) == {'initialization': 'disabled'} method = method_type() method.initialization = PaymentMethodInitialization.ENABLED @@ -1676,6 +1681,12 @@ def test_pay_later_is_sent_as_the_spec_value(self): assert '"payment_type": "pay_later"' in body assert 'PayLater' not in body + def test_payment_type_is_still_importable_from_the_setups_module(self): + # Deprecated re-export kept so existing imports from the setups module keep working. + from checkout_sdk.payments.setups.setups import PaymentType as SetupsModulePaymentType + + assert SetupsModulePaymentType is PaymentType + def test_request_annotation_is_the_setups_enum(self): assert PaymentSetupsRequest.__annotations__['payment_type'] is PaymentSetupPaymentType assert PaymentSetupsRequest.__annotations__['payment_type'] is not PaymentType From f61e299b65fa99104965ccada451ff88733e1d60 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 12:10:17 +0100 Subject: [PATCH 3/7] docs(setups): note that the API stores last_payment_amount as an integer The specification types last_payment_amount as a number (example 55.99), but the sandbox stores and returns 55 for 55.99. The doc says so, so the type is not changed back and forth; the specification is reported separately. --- checkout_sdk/payments/setups/setups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index e8ef6a20..a34e88ac 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -144,8 +144,8 @@ class MerchantAccount: # The total number of orders made by the customer. # [Optional] total_order_count: int - # The payment amount of the customer's most recent transaction. A decimal number, for - # example 55.99. + # The payment amount of the customer's most recent transaction. The specification types this + # as a number (example 55.99); the API currently stores it as an integer. # [Optional] last_payment_amount: float From 4791c43e50a740c06f1f5e46d5ccf8849874715c Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 13:23:39 +0100 Subject: [PATCH 4/7] deprecate(setups): PaymentMethodAction client_token and session_id --- checkout_sdk/payments/setups/setups.py | 6 +++++ .../payment_setups_serialization_test.py | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index a34e88ac..95996fe2 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -195,10 +195,16 @@ class PaymentMethodAction: # readOnly # Enum: "sdk" (PayPal) "set_instrument" (instrument) type: str + # Deprecated: Not part of the PayPal or instrument action schema in the Payment Setups API + # specification; the API ignores it on requests. There is no replacement. Will be removed in + # the next major version. # The client token for the Klarna SDK. # [Optional] # readOnly client_token: str + # Deprecated: Not part of the PayPal or instrument action schema in the Payment Setups API + # specification; the API ignores it on requests. There is no replacement. Will be removed in + # the next major version. # The session ID. # [Optional] # readOnly diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index 8bb068b6..e50175a7 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -30,6 +30,7 @@ GooglePayPreferredScheme, Klarna, KlarnaAction, KlarnaActionType, Stcpay, StcpayAction, StcpayActionType, Tabby, Tamara, TamaraRiskAssessment, Bizum, Blik, PaymentMethodBase, PaymentSetupPaymentMethod, PaymentMethodOptions, PaymentMethodOption, PaymentSetupPaymentType, + PaymentMethodAction, Paypal, ) from checkout_sdk.payments.payments import PaymentType, PreferredSchema @@ -1641,6 +1642,30 @@ def test_blik_still_serializes_partner_code(self): assert _serialize(blik) == {'initialization': 'disabled', 'partner_code': '123456'} + def test_deprecated_action_client_token_and_session_id_are_still_declared_and_sent(self): + # Not part of the PayPal or instrument action schemas. They stay, deprecated, and are + # still sent and read under their keys. + assert PaymentMethodAction.__annotations__['client_token'] is str + assert PaymentMethodAction.__annotations__['session_id'] is str + action = PaymentMethodAction() + action.type = 'sdk' + action.client_token = 'token_1' + action.session_id = 'session_1' + action.order_id = 'order_1' + paypal = Paypal() + paypal.action = action + + body = json.dumps(paypal, cls=JsonSerializer) + + assert '"client_token": "token_1"' in body + assert '"session_id": "session_1"' in body + assert 'clientToken' not in body + assert 'sessionId' not in body + read = ResponseWrapper(None, json.loads(body)) + assert read.action.client_token == action.client_token + assert read.action.session_id == action.session_id + assert read.action.order_id == action.order_id + @pytest.mark.parametrize('method_type', [Klarna, Stcpay, Tabby, Bizum]) def test_deprecated_payment_method_options_are_still_sent(self, method_type): option = PaymentMethodOption() From 44df4809905da5bfae048665552dd795ede1b065 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 14:25:57 +0100 Subject: [PATCH 5/7] fix(setups): keep card_present undeprecated as on main --- checkout_sdk/payments/setups/setups.py | 17 ++++------------- .../setups/payment_setups_serialization_test.py | 5 +++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 95996fe2..6fb8e09c 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -1076,11 +1076,8 @@ class Bacs(PaymentMethodBase): # Card Present entities class CardPresentPin: - """The PIN block details of a card-present payment. - - Deprecated: card_present is no longer part of the Payment Setups API specification. There is - no replacement. Will be removed in the next major version. - """ + """The PIN block details of a card-present payment. Not present in the current Payment Setup + schema.""" # The identifier of the key set used to encrypt the PIN block. # [Optional] key_set_id: str @@ -1093,11 +1090,7 @@ class CardPresentPin: class CardPresent(PaymentSetupPaymentMethod): - """The card-present payment method. - - Deprecated: card_present is no longer part of the Payment Setups API specification. There is - no replacement. Will be removed in the next major version. - """ + """The card-present payment method. Not present in the current Payment Setup schema.""" # The track 2 data read from the card. # [Optional] track2: str @@ -1609,9 +1602,7 @@ class PaymentMethods: # The Bacs payment method's details and configuration. # [Optional] bacs: Bacs - # Deprecated: card_present is no longer part of the Payment Setups API specification. There is - # no replacement. Will be removed in the next major version. - # The card-present payment method. + # The card-present payment method. Not present in the current Payment Setup schema. # [Optional] card_present: CardPresent # The Pay by Bank (Open Banking) payment method's details and configuration. diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index e50175a7..c6f6d1fb 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -304,8 +304,9 @@ def test_bacs_serializes_nested_account_holder(self): 'allow_partial_match': True, } - def test_deprecated_card_present_still_serializes_nested_pin(self): - # card_present is not in the Payment Setup schema. It stays, deprecated, and is still sent. + def test_card_present_serializes_nested_pin(self): + # card_present is not listed in the public API reference but is still supported by the + # API, so it is still sent under its key. card_present = CardPresent() card_present.track2 = 'track2-data' card_present.emv = 'emv-data' From 6fe66a47ad9aeb7bbf13188adddfbcf1589a1832 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 14:59:24 +0100 Subject: [PATCH 6/7] docs(setups): note that an update replaces the payment methods --- checkout_sdk/payments/setups/setups.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 6fb8e09c..ddc49e20 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -267,7 +267,9 @@ class PaymentMethodBase: # readOnly flags: list # list of str # The initialization state of the payment method. When you create a Payment Setup, this - # defaults to disabled. + # defaults to disabled. On update the API replaces the payment methods, so send enabled + # again to keep the method enabled. The specification does not state this; it is the API's + # behaviour. # [Optional] # Default: "disabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED @@ -585,7 +587,9 @@ class TamaraRiskAssessment: class Tamara(PaymentSetupPaymentMethod): """The Tamara payment method's details and configuration.""" # The initialization state of the payment method. When you create a Payment Setup, this - # defaults to disabled. + # defaults to disabled. On update the API replaces the payment methods, so send enabled + # again to keep the method enabled. The specification does not state this; it is the API's + # behaviour. # [Optional] # Default: "disabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED From c7104af41b0247ee6f34b2d6b5d5aca9f0a7b883 Mon Sep 17 00:00:00 2001 From: rodrigojara-cko Date: Tue, 27 Oct 2026 16:32:12 +0100 Subject: [PATCH 7/7] docs(setups): note the ideal, knet and bancontact fields the API needs --- checkout_sdk/payments/setups/setups.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index ddc49e20..c1d5bac3 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -685,7 +685,8 @@ class Qpay(PaymentSetupPaymentMethod): class Ideal(PaymentSetupPaymentMethod): """The iDEAL payment method's details and configuration.""" # A description of the products or services being paid for. Do not include HTML tags or - # special characters, as these are rejected by iDEAL. + # special characters, as these are rejected by iDEAL. The specification marks this + # optional, but the API keeps the method invalid until it is sent. # [Optional] # max 35 characters description: str @@ -694,14 +695,16 @@ class Ideal(PaymentSetupPaymentMethod): class Knet(PaymentSetupPaymentMethod): """The KNET payment method's details and configuration.""" # The customer's preferred language, as an ISO 639-1 code. For example, the language - # selected on your site, if the issuer's site supports it. + # selected on your site, if the issuer's site supports it. The specification marks this + # optional, but the API keeps the method invalid until it is sent. # [Optional] language: KnetLanguage class Bancontact(PaymentSetupPaymentMethod): """The Bancontact payment method's details and configuration.""" - # The account holder's name. + # The account holder's name. The specification marks this optional, but the API keeps the + # method invalid until it is sent. # [Optional] # min 3 characters, max 100 characters account_holder_name: str