From 130f8f2f37f9d8b9b856333285752924c6bce424 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 7 Oct 2026 16:35:15 +0200 Subject: [PATCH 1/3] CashApp functionality --- checkout_sdk/payments/setups/setups.py | 1028 ++++++++++++++++- checkout_sdk/payments/setups/setups_client.py | 41 +- .../setups/payment_setups_client_test.py | 6 + .../setups/payment_setups_integration_test.py | 75 +- .../payment_setups_serialization_test.py | 338 ++++++ 5 files changed, 1448 insertions(+), 40 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 62e91ee0..625423e0 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -9,155 +9,331 @@ # Enums class PaymentMethodInitialization(str, Enum): + """The initialization state of a payment method on a Payment Setup.""" DISABLED = 'disabled' ENABLED = 'enabled' +# Shared payment-method enums +class TerminalType(str, Enum): + """The client-side terminal type of the wallet payment methods.""" + WEB = 'web' + WAP = 'wap' + APP = 'app' + + +class OsType(str, Enum): + """An operating system type. Used by the wallet payment methods and by the customer device.""" + ANDROID = 'android' + IOS = 'ios' + + # Customer entities class CustomerEmail: + """Details of the customer's email.""" + # The customer's email address. + # [Optional] address: str + # Specifies whether the customer's email address is verified. + # [Optional] verified: bool +class CustomerDeviceClient(str, Enum): + """The type of client the customer uses to initiate the payment.""" + WEB = 'web' + MOBILE_WEB = 'mobile_web' + APP = 'app' + + class CustomerDevice: + """Details of the customer's device.""" + # The locale of the device. For example, en_GB. + # [Optional] locale: str + # A unique identifier for the customer's device. + # [Optional] + fingerprint: str + # The customer's device IPv4 address, used by some payment methods for risk and + # eligibility checks. + # [Optional] + ipv4: str + # The customer's device IPv6 address, used by some payment methods for risk and + # eligibility checks. + # [Optional] + ipv6: str + # The type of client the customer uses to initiate the payment. Required when using + # Cash App Pay. + # [Optional] + client: CustomerDeviceClient + # The operating system of the customer's device. + # [Optional] + os: OsType class MerchantAccount: + """Details of the account the customer holds with the merchant.""" + # The merchant's unique identifier for the customer's account. + # [Optional] id: str - registration_date: str # Format: yyyy-MM-dd - last_modified: str # Format: yyyy-MM-dd + # The date the customer registered their account with the merchant. + # [Optional] + # Format: date (yyyy-MM-dd) + registration_date: str + # The date the customer's account with the merchant was last modified. + # [Optional] + # Format: date (yyyy-MM-dd) + last_modified: str + # Specifies if the customer is a returning customer. + # [Optional] returning_customer: bool - first_transaction_date: str # Format: yyyy-MM-dd - last_transaction_date: str # Format: yyyy-MM-dd + # The date of the customer's first transaction. + # [Optional] + # Format: date (yyyy-MM-dd) + first_transaction_date: str + # The date of the customer's most recent transaction. + # [Optional] + # Format: date (yyyy-MM-dd) + last_transaction_date: str + # The total number of orders made by the customer. + # [Optional] total_order_count: int + # The payment amount of the customer's most recent transaction. + # [Optional] last_payment_amount: int class Customer: + """The customer's details.""" + # Details of the customer's email. + # [Optional] email: CustomerEmail + # The customer's full name. + # [Optional] + # max 100 characters name: str + # The customer's phone number. country_code is min 1, max 7 characters; number is + # min 6, max 25 characters. + # [Optional] phone: Phone + # Details of the customer's device. + # [Optional] device: CustomerDevice + # Details of the account the customer holds with the merchant. + # [Optional] merchant_account: MerchantAccount + # The two-letter ISO country code of the customer for this payment. + # [Optional] + # min 2 characters, max 2 characters country: str + # The unique identifier of the customer. + # [Optional] id: str + # The customer's tax identification number. + # [Optional] tax_number: str # Payment Method Common entities class PaymentMethodAction: + """The next available action for the Klarna, 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 type of action. + # [Optional] + # readOnly + # Enum: "sdk" (Klarna, PayPal) "set_instrument" (instrument) type: str + # The client token for the Klarna SDK. + # [Optional] + # readOnly client_token: str + # The session ID. + # [Optional] + # readOnly session_id: str + # The PayPal order ID to use with the PayPal SDK. + # [Optional] + # readOnly order_id: str class PaymentMethodOption: + """A payment method option. Not present in the current Payment Setup schema.""" + # The identifier of the payment method option. + # [Optional] id: str + # The list of error codes or indicators that highlight missing or invalid information. + # [Optional] flags: list # list of str + # The next available action for the payment method option. + # [Optional] action: PaymentMethodAction class PaymentMethodOptions: + """The payment method options. Not present in the current Payment Setup schema.""" + # The SDK payment method option. + # [Optional] sdk: PaymentMethodOption + # The pay in full payment method option. + # [Optional] pay_in_full: PaymentMethodOption + # The installments payment method option. + # [Optional] installments: PaymentMethodOption + # The pay now payment method option. + # [Optional] pay_now: PaymentMethodOption class PaymentMethodBase: + """Base for payment methods that carry status, flags and the writable initialization field + (swagger PaymentSetupPaymentMethod plus PaymentMethodInitialization).""" + # 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 + # The initialization state of the payment method. When you create a Payment Setup, this + # defaults to disabled. + # [Optional] initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED # Klarna entities class KlarnaAccountHolder: - # readOnly: the account holder details returned by Klarna after the shopper completes - # verification (swagger KlarnaAccountHolder). + """The account holder details returned by Klarna after the shopper completes verification.""" + # The full name of the account holder. + # [Optional] + # readOnly name: str class Klarna(PaymentMethodBase): + """The Klarna payment method's details and configuration.""" + def __init__(self): super().__init__() + # The account holder details returned by Klarna after the shopper completes + # verification. + # [Optional] + # readOnly self.account_holder: KlarnaAccountHolder + # The payment method options. Not present in the current Klarna schema. + # [Optional] self.payment_method_options: PaymentMethodOptions # Stcpay entities class Stcpay(PaymentMethodBase): + """The stc pay payment method's details and configuration.""" + def __init__(self): super().__init__() + # The one-time password (OTP) for stc pay. + # [Optional] self.otp: str + # The payment method options. Not present in the current stc pay schema. + # [Optional] self.payment_method_options: PaymentMethodOptions # Tabby entities class Tabby(PaymentMethodBase): + """The Tabby payment method's details and configuration.""" + # The available payment types for Tabby. For example, installments. + # [Optional] payment_types: list # list of str def __init__(self): super().__init__() + # The payment method options. Not present in the current Tabby schema. + # [Optional] self.payment_method_options: PaymentMethodOptions # Bizum entities class Bizum(PaymentMethodBase): + """The Bizum payment method's details and configuration.""" + def __init__(self): super().__init__() + # The payment method options. Not present in the current Bizum schema. + # [Optional] self.payment_method_options: PaymentMethodOptions class Blik(PaymentMethodBase): + """The Blik payment method's details and configuration.""" + # The 6-digit BLIK code generated by the customer's banking app. + # [Optional] + # ^[0-9]{6}$ partner_code: str class PaypalUserAction(str, Enum): + """The user action for the PayPal widget.""" PAY_NOW = 'pay_now' CONTINUE = 'continue' class PaypalShippingPreference(str, Enum): + """Where to obtain the shipping information.""" NO_SHIPPING = 'no_shipping' GET_FROM_FILE = 'get_from_file' SET_PROVIDED_ADDRESS = 'set_provided_address' class Paypal(PaymentMethodBase): + """The PayPal payment method's details and configuration.""" + # The user action for the PayPal widget. pay_now redirects the customer to finalize the + # payment immediately; continue lets them review the order before paying. + # [Optional] user_action: PaypalUserAction + # The brand name to display in the PayPal checkout experience. + # [Optional] + # max 127 characters brand_name: str + # Where to obtain the shipping information. + # [Optional] shipping_preference: PaypalShippingPreference + # The next available action for the payment method. + # [Optional] + # readOnly action: PaymentMethodAction # Base for payment methods that only expose status/flags (swagger PaymentSetupPaymentMethod). -# Distinct from PaymentMethodBase, which also carries the writable `initialization` field -# supported only by klarna, stcpay, tabby and paypal. +# 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 -# Shared payment-method enums -class TerminalType(str, Enum): - WEB = 'web' - WAP = 'wap' - APP = 'app' - - -class OsType(str, Enum): - ANDROID = 'android' - IOS = 'ios' - - class KnetLanguage(str, Enum): + """The customer's preferred KNET language, as an ISO 639-1 code.""" EN = 'en' AR = 'ar' class PaymentSetupAccountHolderType(str, Enum): + """The type of account holder.""" INDIVIDUAL = 'individual' CORPORATE = 'corporate' GOVERNMENT = 'government' @@ -165,174 +341,309 @@ class PaymentSetupAccountHolderType(str, Enum): # Account holder shared by card, wallet and instrument methods class PaymentSetupAccountHolder: + """Account holder details for the payment.""" + # The card account holder type. + # [Optional] type: PaymentSetupAccountHolderType + # The card account holder's first name. + # [Optional] first_name: str + # The card account holder's last name. + # [Optional] last_name: str + # The card account holder's middle name. + # [Optional] middle_name: str + # The card account holder's company name. + # [Optional] company_name: str + # Indicates whether the ANI (account name inquiry) check is performed with the card scheme + # (Visa or Mastercard). + # [Optional] account_name_inquiry: bool # Instrument entities class PaymentSetupInstrument(PaymentSetupPaymentMethod): + """The instrument payment method's details and configuration.""" + # The unique identifier of the selected instrument. + # [Optional] id: str + # The customer's phone number. Not present in the current instrument schema. + # [Optional] phone: Phone + # Account holder details for the payment. Not present in the current instrument schema. + # [Optional] account_holder: PaymentSetupAccountHolder + # 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. + # [Optional] + # readOnly action: PaymentMethodAction # Status/flags-only methods class PayNow(PaymentSetupPaymentMethod): + """The PayNow payment method's details and configuration. Read-only.""" pass class Eps(PaymentSetupPaymentMethod): + """The EPS payment method's details and configuration. Read-only.""" pass class Benefit(PaymentSetupPaymentMethod): + """The Benefit payment method's details and configuration. Read-only.""" pass class Vipps(PaymentSetupPaymentMethod): + """The Vipps payment method's details and configuration. Read-only.""" pass class Twint(PaymentSetupPaymentMethod): + """The Twint payment method's details and configuration. Read-only.""" pass class MobilePay(PaymentSetupPaymentMethod): + """The MobilePay payment method's details and configuration. Read-only.""" pass class Tamara(PaymentSetupPaymentMethod): + """The Tamara payment method's details and configuration.""" pass class MBWay(PaymentSetupPaymentMethod): + """The MBWay payment method's details and configuration. Read-only.""" pass class WeChatPay(PaymentSetupPaymentMethod): + """The WeChatPay payment method's details and configuration. Read-only.""" pass class Octopus(PaymentSetupPaymentMethod): + """The Octopus payment method's details and configuration. Read-only.""" pass class Alma(PaymentSetupPaymentMethod): + """The Alma payment method's details and configuration.""" pass class Sequra(PaymentSetupPaymentMethod): + """The Sequra payment method's details and configuration.""" pass # Wallets that share terminal_type/os_type configuration class TerminalPaymentMethod(PaymentSetupPaymentMethod): + """Base for the wallet payment methods that take a terminal type and an OS type.""" + # The client-side terminal type. Indicates whether the customer is using a PC browser, + # mobile browser, or mobile application. + # [Optional] terminal_type: TerminalType + # The customer's operating system type. Required when terminal_type is not web. + # [Optional] os_type: OsType class AlipayCn(TerminalPaymentMethod): + """The Alipay CN payment method's details and configuration.""" pass class AlipayHK(TerminalPaymentMethod): + """The Alipay HK payment method's details and configuration.""" pass class GCash(TerminalPaymentMethod): + """The GCash payment method's details and configuration.""" pass class Tng(TerminalPaymentMethod): + """The TNG payment method's details and configuration.""" pass class Dana(TerminalPaymentMethod): + """The Dana payment method's details and configuration.""" pass class KakaoPay(TerminalPaymentMethod): + """The KakaoPay payment method's details and configuration.""" pass class TrueMoney(TerminalPaymentMethod): + """The TrueMoney payment method's details and configuration.""" pass # Methods with specific fields class Qpay(PaymentSetupPaymentMethod): + """The QPay payment method's details and configuration.""" + # The Qatari national ID. Must start with 2 or 3, followed by 10 digits. + # [Optional] national_id: str + # A description of the payment order. Alphanumeric characters only. + # [Optional] + # max 255 characters description: str 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. + # [Optional] + # max 35 characters description: str 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. + # [Optional] language: KnetLanguage class Bancontact(PaymentSetupPaymentMethod): + """The Bancontact payment method's details and configuration.""" + # The account holder's name. + # [Optional] + # min 3 characters, max 100 characters account_holder_name: str class Multibanco(PaymentSetupPaymentMethod): + """The Multibanco payment method's details and configuration.""" + # The account holder's name. + # [Optional] + # min 3 characters, max 100 characters account_holder_name: str class P24AccountHolder: + """The P24 account holder's details.""" + # The account holder's name. + # [Optional] + # min 3 characters, max 100 characters name: str + # The account holder's email address. + # [Optional] + # Format: email + # max 254 characters email: str class P24(PaymentSetupPaymentMethod): + """The P24 (Przelewy24) payment method's details and configuration.""" + # The account holder's details. + # [Optional] account_holder: P24AccountHolder class SwishAccountHolder: + """The Swish account holder's details.""" + # The account holder's first name. + # [Optional] + # min 1 characters first_name: str + # The account holder's last name. + # [Optional] + # min 1 characters last_name: str class Swish(PaymentSetupPaymentMethod): + """The Swish payment method's details and configuration.""" + # A description that appears on the customer's billing statement. + # [Optional] billing_descriptor: str + # The account holder's details. + # [Optional] account_holder: SwishAccountHolder # ACH entities class AchAccountType(str, Enum): + """The type of Direct Debit account.""" SAVINGS = 'savings' CURRENT = 'current' CASH = 'cash' class AchAccountHolderIdentification: + """The account holder's government document identification, for example a Social Security + Number (SSN).""" + # The document type. + # [Optional] type: str + # The country where the document was issued. + # [Optional] issuing_country: str + # The document number. + # [Optional] number: str class AchAccountHolder: + """The ACH account holder details.""" + # The type of account holder. + # [Optional] type: PaymentSetupAccountHolderType + # The first name of the account holder. + # [Optional] first_name: str + # The last name of the account holder. + # [Optional] last_name: str + # The legal name of a registered company that holds the account. + # [Optional] company_name: str + # The account holder's date of birth. + # [Optional] date_of_birth: str + # The account holder's government document identification, for example a Social Security + # Number (SSN). + # [Optional] identification: AchAccountHolderIdentification class Ach(PaymentSetupPaymentMethod): + """The ACH payment method's details and configuration.""" + # The type of Direct Debit account. + # [Optional] account_type: AchAccountType + # The account holder details. + # [Optional] account_holder: AchAccountHolder + # The account number of the Direct Debit account. + # [Optional] + # min 4 characters, max 17 characters account_number: str + # The bank code of the Direct Debit account. + # [Optional] + # min 8 characters, max 9 characters bank_code: str + # The two-letter ISO country code of the bank account. + # [Optional] + # min 2 characters, max 2 characters country: str @@ -349,234 +660,777 @@ class SetupsSepaMandateType(str, Enum): class SepaMandate: + """The SEPA mandate details.""" + # The ID of the mandate. + # [Optional] id: str + # The type of mandate. + # [Optional] type: SetupsSepaMandateType - date_of_signature: str # Format: yyyy-MM-dd + # The date the mandate was signed. + # [Optional] + # Format: date (yyyy-MM-dd) + date_of_signature: str class SepaAccountHolder: + """The SEPA account holder details.""" + # The type of account holder. The SEPA schema allows individual and corporate only. + # [Optional] type: PaymentSetupAccountHolderType + # The first name of the account holder. + # [Optional] first_name: str + # The last name of the account holder. + # [Optional] last_name: str + # The legal name of a registered company that holds the account. + # [Optional] company_name: str class Sepa(PaymentSetupPaymentMethod): + """The SEPA payment method's details and configuration.""" + # The account holder details. + # [Optional] account_holder: SepaAccountHolder + # The account holder's IBAN. + # [Optional] account_number: str + # The account's country, as an ISO 3166-1 alpha-2 code. + # [Optional] + # min 2 characters, max 2 characters country: str + # The account holder's account currency. + # [Optional] currency: Currency + # The mandate details. + # [Optional] mandate: SepaMandate # Google Pay entities class GooglePayTokenData: + """The Google Pay token data.""" + # The encryption and signing scheme used to create this message. If not set, the default + # is ECv0. + # [Optional] protocol_version: str + # The signature that verifies the message came from Google, created using ECDSA. + # [Optional] signature: str + # A serialized JSON string containing the encryptedMessage, ephemeralPublicKey, and tag. + # To simplify the signature verification process, this value is serialized. + # [Optional] signed_message: str + # The public key provided to the Google API in the + # tokenizationSpecification.parameters.gatewayMerchantId field. + # [Optional] tokenization_key: str class GooglePay(PaymentSetupPaymentMethod): + """The Google Pay payment method's details and configuration.""" + # The Google Pay token data. + # [Optional] token_data: GooglePayTokenData + # The Checkout.com Google Pay token. The spec declares token as an object with id and + # expires_on; this SDK field carries the token identifier only. + # [Optional] token: str + # The date and time the token expires, in ISO 8601 format. The spec nests it under token. + # [Optional] + # Format: date-time (RFC 3339) expires_on: datetime + # Set to true if you intend to reuse the payment credentials in subsequent payments. If set + # to false, a payment instrument will not be included in the payment response. + # [Optional] store_for_future_use: bool + # The customer's phone number. + # [Optional] phone: Phone + # Account holder details for the payment. + # [Optional] account_holder: PaymentSetupAccountHolder # Apple Pay entities class ApplePayTokenDataHeader: + """Additional version-dependent information used to decrypt and verify the payment.""" + # The ephemeral public key. + # [Optional] ephemeral_public_key: str + # The hash of the public key. + # [Optional] public_key_hash: str + # The transaction identifier. + # [Optional] transaction_id: str class ApplePayTokenData: + """The Apple Pay token data.""" + # The version of the payment token. The token uses EC_v1 for ECC-encrypted data, and + # RSA_v1 for RSA-encrypted data. + # [Optional] version: str + # The encrypted payment data, Base64-encoded. + # [Optional] data: str + # The signature of the payment and header data. The signature includes the signing + # certificate, its intermediate CA certificate, and information about the signing algorithm. + # [Optional] signature: str + # Additional version-dependent information used to decrypt and verify the payment. + # [Optional] header: ApplePayTokenDataHeader class ApplePay(PaymentSetupPaymentMethod): + """The Apple Pay payment method's details and configuration.""" + # The Apple Pay token data. + # [Optional] token_data: ApplePayTokenData + # The Checkout.com Apple Pay token. The spec declares token as an object with id and + # expires_on; this SDK field carries the token identifier only. + # [Optional] token: str + # The date and time the token expires, in ISO 8601 format. The spec nests it under token. + # [Optional] + # Format: date-time (RFC 3339) expires_on: datetime + # Set to true if you intend to reuse the payment credentials in subsequent payments. If set + # to false, a payment instrument will not be included in the payment response. + # [Optional] store_for_future_use: bool + # The customer's phone number. + # [Optional] phone: Phone + # Account holder details for the payment. + # [Optional] account_holder: PaymentSetupAccountHolder # Bacs entities class BacsAccountHolderType(str, Enum): + """The type of Bacs account holder.""" INDIVIDUAL = 'individual' CORPORATE = 'corporate' class BacsAccountHolder: + """The Bacs account holder details.""" + # The type of account holder. + # [Optional] type: BacsAccountHolderType + # The first name of the account holder. + # [Optional] first_name: str + # The last name of the account holder. + # [Optional] last_name: str + # The legal name of a registered company that holds the account. + # [Optional] company_name: str + # The email address of the account holder. + # [Optional] email: str class Bacs(PaymentMethodBase): + """The Bacs payment method's details and configuration.""" + # The ID of the Bacs instrument used for the payment. + # [Optional] + # readOnly instrument_id: str + # The account holder details. + # [Optional] account_holder: BacsAccountHolder + # The account number of the Bacs Direct Debit account. + # [Optional] account_number: str + # The sort code of the Bacs Direct Debit account. + # [Optional] bank_code: str + # The account's country, as an ISO 3166-1 alpha-2 code. + # [Optional] + # min 2 characters, max 2 characters country: str + # The account holder's account currency. + # [Optional] currency: Currency + # Indicates whether the Bacs instrument is created when account validation returns a + # partial match. When true, the instrument is created on a partial match; when false, + # instrument creation fails on a partial match. Defaults to false. + # [Optional] allow_partial_match: bool # Card Present entities class CardPresentPin: + """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 + # The encrypted PIN block. + # [Optional] block: str + # The format of the PIN block. + # [Optional] block_format: str class CardPresent(PaymentSetupPaymentMethod): + """The card-present payment method. Not present in the current Payment Setup schema.""" + # The track 2 data read from the card. + # [Optional] track2: str + # The EMV data read from the card. + # [Optional] emv: str + # The way the card details were entered. + # [Optional] entry_mode: str + # The PIN block details. + # [Optional] pin: CardPresentPin + # Set to true if you intend to reuse the payment credentials in subsequent payments. + # [Optional] store_for_future_use: bool + # The cardholder's name. + # [Optional] name: str # Pay by Bank entities class PayByBankBank: + """A bank available for the customer to select.""" + # The unique identifier of the bank. + # [Optional] + # readOnly bank_id: str + # The display name of the bank. + # [Optional] + # readOnly display_name: str + # The URL of the bank's logo. + # [Optional] + # readOnly logo_url: str + # Whether the bank is currently available for selection. + # [Optional] + # readOnly available: bool class PayByBankActionType(str, Enum): + """The type of the Pay by Bank action.""" SELECT_BANK = 'select_bank' class PayByBankAction: + """The next available action for the Pay by Bank payment method. Response only.""" + # The type of action. + # [Optional] + # readOnly type: PayByBankActionType + # The list of banks available for the customer to select. + # [Optional] + # readOnly banks: list # list of PayByBankBank class PayByBank(PaymentSetupPaymentMethod): + """The Pay by Bank (Open Banking) payment method's details and configuration.""" + # The identifier of the bank the customer has selected for the payment. + # [Optional] bank_id: str + # The next available action for the payment method. + # [Optional] + # readOnly action: PayByBankAction +# Cash App Pay entities +class CashAppActionType(str, Enum): + """The type of the Cash App action.""" + REDIRECT = 'redirect' + + +class CashAppAction: + """The next available action for the Cash App payment method. Response only.""" + # The type of action. + # [Optional] + # readOnly + type: CashAppActionType + # The URL to redirect the customer to so they can authorize the payment with Cash App. + # [Optional] + # readOnly + # Format: uri + redirect_url: str + + +class CashAppAddress: + """The customer's address in their Cash App profile. Response only. + + These are Cash App's key names (address_line_1, administrative_district_level_1), not the + Checkout.com Address keys (address_line1, city, state, zip), so the common Address type + does not apply. + """ + # The first line of the address. + # [Optional] + # readOnly + address_line_1: str + # The second line of the address. + # [Optional] + # readOnly + address_line_2: str + # The third line of the address. + # [Optional] + # readOnly + address_line_3: str + # The address locality, such as the city or town. + # [Optional] + # readOnly + locality: str + # The address sublocality, such as the district or neighborhood. + # [Optional] + # readOnly + sublocality: str + # The address's top-level administrative district, such as the state or province. + # [Optional] + # readOnly + administrative_district_level_1: str + # The postal or zip code. + # [Optional] + # readOnly + postal_code: str + # The address country, in ISO 3166-1 alpha-2 format. + # [Optional] + # readOnly + # max 2 characters + country: str + + +class CashAppCustomerProfile: + """The customer's Cash App profile that they consented to share. Response only.""" + # Cash App's identifier for the customer. This is not a Checkout.com customer identifier. + # [Optional] + # readOnly + customer_id: str + # The customer's $Cashtag. + # [Optional] + # readOnly + cashtag: str + # Cash App's reference for the customer profile. + # [Optional] + # readOnly + reference_id: str + # The customer's full name. + # [Optional] + # readOnly + full_name: str + # The customer's given name. + # [Optional] + # readOnly + given_name: str + # The customer's middle name. + # [Optional] + # readOnly + middle_name: str + # The customer's family name. + # [Optional] + # readOnly + family_name: str + # The suffix of the customer's name. + # [Optional] + # readOnly + suffix: str + # The customer's date of birth. Kept as the raw string returned by the API, because the + # provider's value is not always a plain yyyy-MM-dd date. + # [Optional] + # readOnly + # Format: date + birth_date: str + # The customer's address. + # [Optional] + # readOnly + address: CashAppAddress + # The customer's phone number. + # [Optional] + # readOnly + phone_number: str + # The customer's email address. + # [Optional] + # readOnly + email_address: str + # The date and time the customer's Cash App account was created. Kept as the raw string + # returned by the API, because the provider's format varies. + # [Optional] + # readOnly + # Format: date-time + customer_since: str + + +class CashApp(PaymentMethodBase): + """The Cash App payment method's details and configuration. + + Serialized under the key cashapp, one lowercase word. Assigning None to an attribute sends + null; leave an attribute unset to omit it from the request. + """ + # Indicates whether the customer consents to share their Cash App customer profile with + # Checkout.com. + # [Optional] + customer_profile_sharing: bool + # The customer's Cash App profile that they consented to share. Included in the response + # when customer_profile_sharing is enabled. Cash App releases this profile only once. It's + # present in the first successful response when you get the Payment Setup after the + # customer authorizes the payment. Every subsequent response omits it, so store it on + # first read. + # [Optional] + # readOnly + customer_profile: CashAppCustomerProfile + # A reference for the Cash App Pay transaction, returned by the provider. + # [Optional] + # readOnly + # max 80 characters + reference: str + # The next available action for the payment method. When its type is redirect, send the + # customer to redirect_url to authorize the payment with Cash App. + # [Optional] + # readOnly + action: CashAppAction + + # Stablecoin entities class Stablecoin(PaymentSetupPaymentMethod): + """The Stablecoin payment method's details and configuration.""" pass # Card entities class Card(PaymentSetupPaymentMethod): + """The Card payment method's details and configuration. + + The spec nests the card fields under details (and the token under token.id); this class + keeps them at the top level of the card object. + """ + # The card number (without separators). The spec nests it under details. + # [Optional] number: str + # The last four digits of the card number. The spec nests it under details. + # [Optional] + # readOnly last4: str + # The card issuer's Bank Identification Number (BIN). The spec nests it under details. + # [Optional] + # readOnly bin: str + # The card scheme. The spec nests it under details. + # [Optional] + # readOnly scheme: str + # The expiry month of the card. The spec nests it under details. + # [Optional] expiry_month: int + # The expiry year of the card. The spec nests it under details. + # [Optional] expiry_year: int + # The cardholder's name. The spec nests it under details. + # [Optional] name: str + # The card verification value/code. 3 digits, except for American Express (4 digits). + # The spec nests it under details. + # [Optional] cvv: str + # Indicates whether this card is being submitted from your own stored card-on-file system, + # rather than being entered by the customer at the time of payment. The spec nests it + # under details. + # [Optional] stored: bool + # The time by which the card details must be confirmed. The spec nests it under details. + # [Optional] + # readOnly + # Format: date-time (RFC 3339) expires_on: datetime + # Set to true if you intend to reuse the payment credentials in subsequent payments. If set + # to false, a payment instrument will not be included in the payment response. + # [Optional] store_for_future_use: bool + # The customer's phone number. + # [Optional] phone: Phone + # Account holder details for the payment. + # [Optional] account_holder: PaymentSetupAccountHolder + # Indicates whether to use the Real-Time Account Updater to update the card information. + # The spec nests it under details. + # [Optional] allow_update: bool class PaymentMethods: + """The payment methods that are enabled on your account and available for use.""" + # The instrument payment method's details and configuration. + # [Optional] instrument: PaymentSetupInstrument + # The Klarna payment method's details and configuration. + # [Optional] klarna: Klarna + # The stc pay payment method's details and configuration. + # [Optional] stcpay: Stcpay + # The Tabby payment method's details and configuration. + # [Optional] tabby: Tabby + # The Bizum payment method's details and configuration. + # [Optional] + # readOnly bizum: Bizum + # The PayNow payment method's details and configuration. + # [Optional] + # readOnly paynow: PayNow + # The QPay payment method's details and configuration. + # [Optional] qpay: Qpay + # The EPS payment method's details and configuration. + # [Optional] + # readOnly eps: Eps + # The iDEAL payment method's details and configuration. + # [Optional] ideal: Ideal + # The KNET payment method's details and configuration. + # [Optional] knet: Knet + # The Bancontact payment method's details and configuration. + # [Optional] bancontact: Bancontact + # The Benefit payment method's details and configuration. + # [Optional] + # readOnly benefit: Benefit + # The Blik payment method's details and configuration. + # [Optional] blik: Blik + # The Vipps payment method's details and configuration. + # [Optional] + # readOnly vipps: Vipps + # The Twint payment method's details and configuration. + # [Optional] + # readOnly twint: Twint + # The Alipay CN payment method's details and configuration. + # [Optional] alipay_cn: AlipayCn + # The Alipay HK payment method's details and configuration. + # [Optional] alipay_hk: AlipayHK + # The GCash payment method's details and configuration. + # [Optional] gcash: GCash + # The TNG payment method's details and configuration. + # [Optional] tng: Tng + # The Dana payment method's details and configuration. + # [Optional] dana: Dana + # The MobilePay payment method's details and configuration. + # [Optional] + # readOnly mobilepay: MobilePay + # The Tamara payment method's details and configuration. + # [Optional] tamara: Tamara + # The MBWay payment method's details and configuration. + # [Optional] + # readOnly mbway: MBWay + # The Multibanco payment method's details and configuration. + # [Optional] multibanco: Multibanco + # The WeChatPay payment method's details and configuration. + # [Optional] + # readOnly wechatpay: WeChatPay + # The KakaoPay payment method's details and configuration. + # [Optional] kakaopay: KakaoPay + # The TrueMoney payment method's details and configuration. + # [Optional] truemoney: TrueMoney + # The Octopus payment method's details and configuration. + # [Optional] + # readOnly octopus: Octopus + # The P24 (Przelewy24) payment method's details and configuration. + # [Optional] p24: P24 + # The Alma payment method's details and configuration. + # [Optional] alma: Alma + # The Swish payment method's details and configuration. + # [Optional] swish: Swish + # The Sequra payment method's details and configuration. + # [Optional] sequra: Sequra + # The ACH payment method's details and configuration. + # [Optional] ach: Ach + # The SEPA payment method's details and configuration. + # [Optional] sepa: Sepa + # The PayPal payment method's details and configuration. + # [Optional] paypal: Paypal + # The Google Pay payment method's details and configuration. + # [Optional] googlepay: GooglePay + # The Apple Pay payment method's details and configuration. + # [Optional] applepay: ApplePay + # The Card payment method's details and configuration. + # [Optional] card: Card + # The Bacs payment method's details and configuration. + # [Optional] bacs: Bacs + # 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. The spec + # names this key paybybank. + # [Optional] pay_by_bank: PayByBank + # The Stablecoin payment method's details and configuration. + # [Optional] stablecoin: Stablecoin + # The Cash App payment method's details and configuration. Serialized as cashapp. + # [Optional] + cashapp: CashApp # Settings entity class Settings: + """Settings for the Payment Setup.""" + # The URL to redirect the customer to, if the payment is successful. For payment methods + # with a redirect, this value overrides the default success redirect URL configured on + # your account. + # [Optional] + # Format: uri + # max 255 characters success_url: str + # The URL to redirect the customer to, if the payment is unsuccessful. For payment methods + # with a redirect, this value overrides the default failure redirect URL configured on + # your account. + # [Optional] + # Format: uri + # max 255 characters failure_url: str + # Indicates whether to capture the payment immediately. Defaults to true. + # [Optional] capture: bool + # The list of payment methods excluded from the Payment Setup. + # [Optional] excluded_payment_methods: list # list of str (PaymentSourceType values) # Order entities class OrderSubMerchant: + """The details of a sub-merchant.""" + # The unique identifier for the sub-merchant. + # [Optional] id: str + # The product category for the sub-merchant. + # [Optional] product_category: str + # The number of orders the sub-merchant has processed. + # [Optional] number_of_sales: int - registration_date: str # Format: yyyy-MM-dd + # The date the sub-merchant was registered. + # [Optional] + # Format: date (yyyy-MM-dd) + registration_date: str class AmountAllocationCommission: + """Commission you'd like to collect from this split, credited to your currency account. The + commission cannot exceed the split amount. The commission is the split amount multiplied by + commission.percentage, plus commission.amount.""" + # Optional fixed amount of commission to collect, in the minor currency unit. + # [Optional] + # Format: int64 + # min 0 amount: int + # Optional percentage of commission to collect. Supports up to 8 decimal places. + # [Optional] + # min 0, max 100 percentage: float class PaymentSetupAmountAllocation: - id: str # required - amount: int # required + """A sub-entity that the payment is being processed on behalf of.""" + # The id of the sub-entity. + # [Required] + id: str + # The split amount, credited to your sub-entity's currency account. The sum of all split + # amounts must be equal to the payment amount. The amount must be provided in the minor + # currency unit. + # [Required] + # Format: int64 + # min 0, max 9999999999 + amount: int + # A reference you can later use to identify this split, such as an order number. + # [Optional] + # max 50 characters reference: str + # Commission you'd like to collect from this split, credited to your currency account. + # [Optional] commission: AmountAllocationCommission class Order: + """The customer's order details.""" + # A list of items in the order. + # [Optional] items: list # list of PaymentContextsItems + # The customer's shipping details. + # [Optional] shipping: ShippingDetails + # The details of the sub-merchants. + # [Optional] sub_merchants: list # list of OrderSubMerchant + # The discount amount the merchant applied to the transaction. + # [Optional] + # min 0 discount_amount: int + # The unique identifier for the invoice. + # [Optional] invoice_id: str + # The total shipping amount for the order. + # [Optional] + # min 0 shipping_amount: int + # The total tax amount for the order. + # [Optional] + # min 0 tax_amount: int + # The total tipping amount for the order. + # [Optional] + # Format: int64 + # min 0 tipping_amount: int + # The total surcharge amount for the order. + # [Optional] + # Format: int64 + # min 0 surcharge_amount: int + # The sub-entities that the payment is being processed on behalf of. + # [Optional] amount_allocations: list # list of PaymentSetupAmountAllocation @@ -611,7 +1465,7 @@ class PaymentSetupAccommodationGuest: last_name: str # The guest's date of birth. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) date_of_birth: str @@ -632,7 +1486,7 @@ class PaymentSetupAccommodationHost: """Details about the host of the accommodation.""" # The date the host registered. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) registration_date: str # The total number of reservations the host has received. # [Optional] @@ -651,12 +1505,12 @@ class PaymentSetupAccommodation: # For lodging bookings, the customer's check-in date. # For cruise bookings, the cruise departure date (sail date). # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) check_in_date: str # For lodging bookings, the customer's check-out date. # For cruise bookings, the cruise return date. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) check_out_date: str # The accommodation's address. # [Optional] @@ -691,7 +1545,7 @@ class PaymentSetupAirlineTicket: number: str # The date the airline ticket was issued. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) issue_date: str # The carrier code of the ticket issuer. # [Optional] @@ -726,7 +1580,7 @@ class PaymentSetupAirlinePassenger: last_name: str # The passenger's date of birth. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) date_of_birth: str # Details about the passenger's address. # [Optional] @@ -752,7 +1606,7 @@ class PaymentSetupFlightLegDetails: departure_airport: str # The date of the scheduled take-off. # [Optional] - # format: date (YYYY-MM-DD) + # Format: date (yyyy-MM-dd) departure_date: str # The time of the scheduled take-off. # [Optional] @@ -842,44 +1696,89 @@ class Industry: # Billing entity class PaymentSetupBilling: + """The billing details for the payment.""" + # A physical address. address_line1 and address_line2 are max 200 characters; city, + # state and zip are max 50 characters; country is max 2 characters (ISO 3166-1 alpha-2). + # [Optional] address: Address class PaymentSetupBillingDescriptor: + """The billing descriptor for the payment.""" + # A dynamic description of the payment. + # [Optional] + # max 25 characters name: str + # The city from which the payment was made. + # [Optional] + # max 13 characters city: str + # The reference shown on the statement. + # [Optional] + # max 50 characters reference: str class PaymentSetupPresentmentDetails: + """The amount and currency to present to the customer, when the settlement currency differs + from the customer-facing currency.""" + # The presentment amount, in the minor currency unit. + # [Optional] + # Format: int64 amount: int + # The presentment currency, as a three-letter ISO currency code. + # [Optional] currency: str class PaymentSetupTerminal: + """Terminal details.""" + # Terminal identifier. + # [Optional] + # min 8 characters, max 8 characters id: str + # The local date and time on the terminal, in ISO 8601 format. + # [Optional] + # Format: date-time local_date_time: str class AccountFundingTransactionIdentificationType(str, Enum): + """The type of identification used to identify the sender.""" PASSPORT = 'passport' DRIVING_LICENSE = 'driving_license' NATIONAL_ID = 'national_id' class AccountFundingTransactionIdentification: + """Sender identification details.""" + # The type of identification used to identify the sender. + # [Optional] type: AccountFundingTransactionIdentificationType + # The identification number. + # [Optional] number: str + # The two-letter ISO country code of the country that issued the identification. + # [Optional] issuing_country: str class AccountFundingTransactionSender: + """Account funding transaction sender details.""" + # Date of birth of the sender (yyyy-mm-dd). + # [Optional] + # Format: date (yyyy-MM-dd) date_of_birth: str + # The unique reference for the sender of the payment. + # [Optional] reference: str + # Sender identification details. + # [Optional] identification: AccountFundingTransactionIdentification class AccountFundingTransactionPurpose(str, Enum): + """The purpose of the account funding transaction.""" DONATIONS = 'donations' EDUCATION = 'education' EMERGENCY_NEED = 'emergency_need' @@ -902,35 +1801,102 @@ class AccountFundingTransactionPurpose(str, Enum): class AccountFundingTransactionRecipient: + """Account funding transaction recipient details.""" + # Date of birth of the recipient (yyyy-mm-dd). + # [Optional] + # Format: date (yyyy-MM-dd) date_of_birth: str + # Any identifier like part of the PAN (first six digits and last four digits), an IBAN, an + # internal account number, or a phone number related to the primary recipient's account. + # [Optional] + # max 34 characters account_number: str + # The recipient's first name. + # [Optional] + # max 50 characters first_name: str + # The recipient's last name. + # [Optional] + # max 50 characters last_name: str + # A physical address. address_line1 and address_line2 are max 200 characters; city, + # state and zip are max 50 characters; country is max 2 characters (ISO 3166-1 alpha-2). + # [Optional] address: Address class PaymentSetupAccountFundingTransaction: + """Account funding transaction details for the payment.""" + # Whether to process this payment as an account funding transaction. + # [Optional] enabled: bool + # The purpose of the account funding transaction. + # [Optional] purpose: AccountFundingTransactionPurpose + # Account funding transaction sender details. + # [Optional] sender: AccountFundingTransactionSender + # Account funding transaction recipient details. + # [Optional] recipient: AccountFundingTransactionRecipient # Main Request and Response classes class PaymentSetupsRequest: + """Request body for POST /payments/setups and PUT /payments/setups/{id} (swagger + PaymentSetup). The responses of the create, update, get and confirm operations are returned + as a ResponseWrapper with the same keys.""" + # The processing channel to use for the payment. + # [Required] + # ^(pc)_(\w{26})$ processing_channel_id: str + # The payment amount, in the minor currency unit. + # [Required] amount: int + # The currency of the payment, as a three-letter ISO currency code. + # [Required] 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. + # [Optional] payment_type: PaymentType + # A reference you can use to identify the payment. For example, an order number. + # [Optional] + # max 80 characters reference: str + # A description of the payment. + # [Optional] + # max 100 characters description: str + # The payment methods that are enabled on your account and available for use. + # [Optional] payment_methods: PaymentMethods + # Settings for the Payment Setup. + # [Optional] settings: Settings + # The customer's details. + # [Optional] customer: Customer + # The customer's order details. + # [Optional] order: Order + # Industry-specific information. + # [Optional] industry: Industry + # The billing details for the payment. + # [Optional] billing: PaymentSetupBilling + # The billing descriptor for the payment. + # [Optional] billing_descriptor: PaymentSetupBillingDescriptor + # The amount and currency to present to the customer, when the settlement currency differs + # from the customer-facing currency. + # [Optional] presentment_details: PaymentSetupPresentmentDetails + # Terminal details. + # [Optional] terminal: PaymentSetupTerminal + # Account funding transaction details for the payment. + # [Optional] account_funding_transaction: PaymentSetupAccountFundingTransaction diff --git a/checkout_sdk/payments/setups/setups_client.py b/checkout_sdk/payments/setups/setups_client.py index c9640603..a8461a49 100644 --- a/checkout_sdk/payments/setups/setups_client.py +++ b/checkout_sdk/payments/setups/setups_client.py @@ -18,8 +18,15 @@ def __init__(self, api_client: ApiClient, configuration: CheckoutConfiguration): authorization_type=AuthorizationType.SECRET_KEY_OR_OAUTH) def create_payment_setup(self, payment_setups_request: PaymentSetupsRequest): - """ - Creates a Payment Setup + """Create a Payment Setup (POST /payments/setups). + + Args: + payment_setups_request: The Payment Setup request body. To offer Cash App Pay, set + payment_methods.cashapp (initialization, customer_profile_sharing) and + customer.device.client. + Returns: + ResponseWrapper with the Payment Setup. For Cash App Pay, the customer is sent to + payment_methods.cashapp.action.redirect_url to authorize the payment. """ return self._api_client.post( self.build_path(self.__PAYMENTS_PATH, self.__SETUPS_PATH), @@ -28,8 +35,13 @@ def create_payment_setup(self, payment_setups_request: PaymentSetupsRequest): ) def update_payment_setup(self, setup_id: str, payment_setups_request: PaymentSetupsRequest): - """ - Updates a Payment Setup + """Update a Payment Setup (PUT /payments/setups/{id}). + + Args: + setup_id: The unique identifier of the Payment Setup to update. + payment_setups_request: The Payment Setup request body. + Returns: + ResponseWrapper with the updated Payment Setup. """ return self._api_client.put( self.build_path(self.__PAYMENTS_PATH, self.__SETUPS_PATH, setup_id), @@ -38,8 +50,15 @@ def update_payment_setup(self, setup_id: str, payment_setups_request: PaymentSet ) def get_payment_setup(self, setup_id: str): - """ - Gets a Payment Setup + """Retrieve a Payment Setup (GET /payments/setups/{id}). + + Args: + setup_id: The unique identifier of the Payment Setup to retrieve. + Returns: + ResponseWrapper with the Payment Setup. For Cash App Pay with + customer_profile_sharing enabled, payment_methods.cashapp.customer_profile is + present only in the first successful response after the customer authorizes the + payment, so store it on first read. """ return self._api_client.get( self.build_path(self.__PAYMENTS_PATH, self.__SETUPS_PATH, setup_id), @@ -47,8 +66,14 @@ def get_payment_setup(self, setup_id: str): ) def confirm_payment_setup(self, setup_id: str, payment_method_name: str): - """ - Confirms a Payment Setup + """Confirm a Payment Setup (POST /payments/setups/{id}/confirm/{payment_method_name}). + + Args: + setup_id: The unique identifier of the Payment Setup to confirm. + payment_method_name: The name of the payment method to confirm the Payment Setup + with. For example, card, klarna, tabby or cashapp. + Returns: + ResponseWrapper with the Payment Setup. """ return self._api_client.post( self.build_path(self.__PAYMENTS_PATH, self.__SETUPS_PATH, setup_id, diff --git a/tests/payments/setups/payment_setups_client_test.py b/tests/payments/setups/payment_setups_client_test.py index 909fd60b..af0ed87e 100644 --- a/tests/payments/setups/payment_setups_client_test.py +++ b/tests/payments/setups/payment_setups_client_test.py @@ -37,3 +37,9 @@ def test_should_confirm_payment_setup(self, mocker, client: PaymentSetupsClient) assert client.confirm_payment_setup('setup_id', 'card') == 'response' assert_api_call(mock, 'payments/setups/setup_id/confirm/card') + + def test_should_confirm_payment_setup_with_cash_app(self, mocker, client: PaymentSetupsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + + assert client.confirm_payment_setup('setup_id', 'cashapp') == 'response' + assert_api_call(mock, 'payments/setups/setup_id/confirm/cashapp') diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index 1dd272d0..d0df2ad8 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -9,7 +9,8 @@ from checkout_sdk.payments.setups.setups import ( PaymentSetupsRequest, Settings, Customer, CustomerEmail, CustomerDevice, PaymentMethods, Klarna, KlarnaAccountHolder, PaymentMethodInitialization, - Ideal, Knet, KnetLanguage, Bancontact, P24, P24AccountHolder + Ideal, Knet, KnetLanguage, Bancontact, P24, P24AccountHolder, + CashApp, CustomerDeviceClient, OsType ) from tests.checkout_test_utils import assert_response, new_uuid @@ -137,6 +138,78 @@ def test_should_create_payment_setup_with_additional_payment_methods(default_api assert response.currency == request.currency +def test_should_create_payment_setup_with_customer_device_fields(default_api): + """The five customer.device fields added with Cash App Pay are accepted and echoed.""" + # Arrange + request = create_payment_setups_request() + device = request.customer.device + device.fingerprint = "fp_abc123xyz" + device.ipv4 = "203.0.113.0" + device.ipv6 = "2001:db8:85a3::8a2e:370:7334" + device.client = CustomerDeviceClient.WEB + device.os = OsType.ANDROID + + # Act + response = default_api.setups.create_payment_setup(request) + + # Assert + assert_response(response, 'http_metadata', 'id', 'customer.device') + assert response.customer.device.locale == device.locale + assert response.customer.device.fingerprint == device.fingerprint + assert response.customer.device.ipv4 == device.ipv4 + assert response.customer.device.ipv6 == device.ipv6 + assert response.customer.device.client == CustomerDeviceClient.WEB + assert response.customer.device.os == OsType.ANDROID + + +def test_should_create_payment_setup_with_customer_identifiers(default_api): + """customer.id, customer.country and customer.tax_number are accepted and echoed.""" + # Arrange + request = create_payment_setups_request() + request.customer.id = "cus_123456789" + request.customer.country = "GB" + request.customer.tax_number = "GB123456789" + + # Act + response = default_api.setups.create_payment_setup(request) + + # Assert + assert_response(response, 'http_metadata', 'id', 'customer') + assert response.customer.id == "cus_123456789" + assert response.customer.country == "GB" + assert response.customer.tax_number == "GB123456789" + + +def test_should_create_payment_setup_with_cash_app(default_api): + """Cash App Pay: the response carries payment_methods.cashapp and its redirect action. + + Skipped at runtime when the sandbox processing channel does not offer Cash App Pay. + """ + # Arrange + request = create_payment_setups_request() + request.currency = Currency.USD + cashapp = CashApp() + cashapp.initialization = PaymentMethodInitialization.ENABLED + cashapp.customer_profile_sharing = True + request.payment_methods.cashapp = cashapp + request.customer.device.client = CustomerDeviceClient.WEB + + # Act + response = default_api.setups.create_payment_setup(request) + + # Assert + assert_response(response, 'http_metadata', 'id') + if 'cashapp' not in getattr(response, 'available_payment_methods', []): + pytest.skip("Cash App Pay is not enabled on the sandbox processing channel") + + assert_response(response, 'payment_methods.cashapp.status') + assert response.payment_methods.cashapp.initialization == PaymentMethodInitialization.ENABLED + assert response.payment_methods.cashapp.customer_profile_sharing is True + if hasattr(response.payment_methods.cashapp, 'action'): + assert response.payment_methods.cashapp.action.type == 'redirect' + assert response.payment_methods.cashapp.action.redirect_url + + @pytest.mark.skip(reason="Integration test - requires a payment setup ready to be confirmed") def test_should_confirm_payment_setup(default_api): """Test confirming a payment setup""" diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index 7c475209..d2d6453e 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -1,5 +1,8 @@ import json +import pytest + +from checkout_sdk.checkout_response import ResponseWrapper from checkout_sdk.json_serializer import JsonSerializer from checkout_sdk.common.common import Phone from checkout_sdk.common.enums import Currency @@ -20,6 +23,9 @@ PaymentSetupAirlinePassengerAddress, PaymentSetupFlightLegDetails, PaymentSetupAirlineInsurance, PaymentSetupAirlineInsurancePrice, MerchantAccount, OrderSubMerchant, + PaymentSetupsRequest, Customer, CustomerEmail, CustomerDevice, CustomerDeviceClient, + PaymentMethodInitialization, CashApp, CashAppAction, CashAppActionType, CashAppAddress, + CashAppCustomerProfile, ) @@ -595,3 +601,335 @@ def test_a_datetime_would_not_serialize_in_the_declared_format(self): account.registration_date = datetime(2023, 5, 1) assert _serialize(account)['registration_date'] == '2023-05-01T00:00:00' + + +_CASH_APP_REDIRECT_URL = ('https://sandbox.api.cash.app/customer-request/v1/requests/' + 'GRR_f5xg6wrxhtv3p4w24g0wrexa/interstitial?validity_token=bap03y') + +_CASH_APP_CUSTOMER_ID = 'CST_AYVkuLzfsRqEhf4OyQFxQNv22m7IjNFjO6f2J5CDE2nxAC4-21wJ2H8_2kvsdIsDZMN4' + + +class TestCashAppSerialization: + """Cash App Pay on Payment Setups: payment_methods.cashapp and the customer device fields.""" + + def test_cash_app_request_serializes_to_the_spec_body(self): + cashapp = CashApp() + cashapp.initialization = PaymentMethodInitialization.ENABLED + cashapp.customer_profile_sharing = True + payment_methods = PaymentMethods() + payment_methods.cashapp = cashapp + + device = CustomerDevice() + device.locale = 'en_US' + device.fingerprint = 'fp_abc123xyz' + device.ipv4 = '203.0.113.0' + device.ipv6 = '2001:db8:85a3::8a2e:370:7334' + device.client = CustomerDeviceClient.WEB + device.os = OsType.ANDROID + customer = Customer() + customer.device = device + + request = PaymentSetupsRequest() + request.processing_channel_id = 'pc_aaaaaaaaaaaaaaaaaaaaaaaaaa' + request.amount = 1000 + request.currency = Currency.USD + request.payment_methods = payment_methods + request.customer = customer + + body = json.dumps(request, cls=JsonSerializer) + + # The wire key is one lowercase word. Checked on the raw string, case-sensitively. + assert '"cashapp"' in body + assert 'cash_app' not in body + assert 'cashApp' not in body + assert '"customer_profile_sharing"' in body + assert 'customerProfileSharing' not in body + assert json.loads(body) == { + 'processing_channel_id': 'pc_aaaaaaaaaaaaaaaaaaaaaaaaaa', + 'amount': 1000, + 'currency': 'USD', + 'payment_methods': { + 'cashapp': {'initialization': 'enabled', 'customer_profile_sharing': True}, + }, + 'customer': { + 'device': { + 'locale': 'en_US', + 'fingerprint': 'fp_abc123xyz', + 'ipv4': '203.0.113.0', + 'ipv6': '2001:db8:85a3::8a2e:370:7334', + 'client': 'web', + 'os': 'android', + }, + }, + } + + def test_cash_app_initialization_defaults_to_disabled(self): + assert _serialize(CashApp()) == {'initialization': 'disabled'} + + @pytest.mark.parametrize('client, expected', [ + (CustomerDeviceClient.WEB, 'web'), + (CustomerDeviceClient.MOBILE_WEB, 'mobile_web'), + (CustomerDeviceClient.APP, 'app'), + ]) + def test_device_client_serializes_each_spec_value(self, client, expected): + device = CustomerDevice() + device.client = client + + assert _serialize(device) == {'client': expected} + + @pytest.mark.parametrize('os_type, expected', [ + (OsType.ANDROID, 'android'), + (OsType.IOS, 'ios'), + ]) + def test_device_os_serializes_each_spec_value(self, os_type, expected): + device = CustomerDevice() + device.os = os_type + + assert _serialize(device) == {'os': expected} + + def test_device_with_only_locale_serializes_only_locale(self): + device = CustomerDevice() + device.locale = 'en_US' + + assert _serialize(device) == {'locale': 'en_US'} + + def test_cash_app_response_reads_every_field(self): + # The Payment Setup response, as returned by create, update, get and confirm. + response = ResponseWrapper(None, { + 'id': 'pset_123', + 'processing_channel_id': 'pc_aaaaaaaaaaaaaaaaaaaaaaaaaa', + 'amount': 1000, + 'currency': 'USD', + 'customer': { + 'device': { + 'locale': 'en_US', + 'fingerprint': 'fp_abc123xyz', + 'ipv4': '203.0.113.0', + 'ipv6': '2001:db8:85a3::8a2e:370:7334', + 'client': 'web', + 'os': 'android', + }, + }, + 'payment_methods': { + 'cashapp': { + 'status': 'action_required', + 'flags': [], + 'initialization': 'enabled', + 'customer_profile_sharing': True, + 'reference': 'ORDER-99', + 'action': {'type': 'redirect', 'redirect_url': _CASH_APP_REDIRECT_URL}, + 'customer_profile': { + 'customer_id': _CASH_APP_CUSTOMER_ID, + 'cashtag': '$CASHTAG_C_TOKEN', + 'reference_id': 'value', + 'full_name': 'John Middle Doe', + 'given_name': 'John', + 'middle_name': 'Middle', + 'family_name': 'Doe', + 'suffix': 'Jr.', + 'birth_date': '1990-01-01T00:00:00.0000000', + 'address': { + 'address_line_1': '123 Main St', + 'address_line_2': 'Apt 2', + 'address_line_3': 'Floor 3', + 'locality': 'Springfield', + 'sublocality': 'Downtown', + 'administrative_district_level_1': 'IL', + 'postal_code': '62701', + 'country': 'US', + }, + 'phone_number': '5555555555', + 'email_address': 'cash@cash.com', + 'customer_since': '1970-01-18T12:46:04.8000000+00:00', + }, + }, + }, + 'available_payment_methods': ['cashapp'], + }) + + device = response.customer.device + assert device.locale == 'en_US' + assert device.fingerprint == 'fp_abc123xyz' + assert device.ipv4 == '203.0.113.0' + assert device.ipv6 == '2001:db8:85a3::8a2e:370:7334' + assert device.client == CustomerDeviceClient.WEB + assert device.os == OsType.ANDROID + + cashapp = response.payment_methods.cashapp + assert cashapp.status == 'action_required' + assert cashapp.flags == [] + assert cashapp.initialization == PaymentMethodInitialization.ENABLED + assert cashapp.customer_profile_sharing is True + assert cashapp.reference == 'ORDER-99' + assert cashapp.action.type == CashAppActionType.REDIRECT + assert cashapp.action.redirect_url == _CASH_APP_REDIRECT_URL + + profile = cashapp.customer_profile + assert profile.customer_id == _CASH_APP_CUSTOMER_ID + assert profile.cashtag == '$CASHTAG_C_TOKEN' + assert profile.reference_id == 'value' + assert profile.full_name == 'John Middle Doe' + assert profile.given_name == 'John' + assert profile.middle_name == 'Middle' + assert profile.family_name == 'Doe' + assert profile.suffix == 'Jr.' + assert profile.birth_date == '1990-01-01T00:00:00.0000000' + assert profile.phone_number == '5555555555' + assert profile.email_address == 'cash@cash.com' + assert profile.customer_since == '1970-01-18T12:46:04.8000000+00:00' + + address = profile.address + assert address.address_line_1 == '123 Main St' + assert address.address_line_2 == 'Apt 2' + assert address.address_line_3 == 'Floor 3' + assert address.locality == 'Springfield' + assert address.sublocality == 'Downtown' + assert address.administrative_district_level_1 == 'IL' + assert address.postal_code == '62701' + assert address.country == 'US' + + def test_cash_app_round_trip_keeps_every_property_and_the_literal_address_keys(self): + address = CashAppAddress() + address.address_line_1 = '123 Main St' + address.address_line_2 = 'Apt 2' + address.address_line_3 = 'Floor 3' + address.locality = 'Springfield' + address.sublocality = 'Downtown' + address.administrative_district_level_1 = 'IL' + address.postal_code = '62701' + address.country = 'US' + + profile = CashAppCustomerProfile() + profile.customer_id = _CASH_APP_CUSTOMER_ID + profile.cashtag = '$CASHTAG_C_TOKEN' + profile.reference_id = 'value' + profile.full_name = 'John Middle Doe' + profile.given_name = 'John' + profile.middle_name = 'Middle' + profile.family_name = 'Doe' + profile.suffix = 'Jr.' + profile.birth_date = '1990-01-01T00:00:00.0000000' + profile.address = address + profile.phone_number = '5555555555' + profile.email_address = 'cash@cash.com' + profile.customer_since = '1970-01-18T12:46:04.8000000+00:00' + + action = CashAppAction() + action.type = CashAppActionType.REDIRECT + action.redirect_url = _CASH_APP_REDIRECT_URL + + cashapp = CashApp() + cashapp.status = 'action_required' + cashapp.flags = [] + cashapp.initialization = PaymentMethodInitialization.ENABLED + cashapp.customer_profile_sharing = True + cashapp.reference = 'ORDER-99' + cashapp.action = action + cashapp.customer_profile = profile + + body = json.dumps(cashapp, cls=JsonSerializer) + + for key in ('"address_line_1"', '"address_line_2"', '"address_line_3"', + '"administrative_district_level_1"', '"redirect_url"'): + assert key in body, key + assert 'address_line1' not in body + + read = ResponseWrapper(None, json.loads(body)) + + assert read.status == cashapp.status + assert read.flags == cashapp.flags + assert read.initialization == cashapp.initialization + assert read.customer_profile_sharing is True + assert read.reference == cashapp.reference + assert read.action.type == action.type + assert read.action.redirect_url == action.redirect_url + for field in ('customer_id', 'cashtag', 'reference_id', 'full_name', 'given_name', + 'middle_name', 'family_name', 'suffix', 'birth_date', 'phone_number', + 'email_address', 'customer_since'): + assert getattr(read.customer_profile, field) == getattr(profile, field), field + for field in ('address_line_1', 'address_line_2', 'address_line_3', 'locality', + 'sublocality', 'administrative_district_level_1', 'postal_code', 'country'): + assert getattr(read.customer_profile.address, field) == getattr(address, field), field + + +class TestPaymentSetupCustomerSerialization: + """PaymentSetup.customer, all eight properties.""" + + def test_customer_round_trip_keeps_every_property(self): + email = CustomerEmail() + email.address = 'johnsmith@example.com' + email.verified = True + phone = Phone() + phone.country_code = '+44' + phone.number = '207 946 0000' + device = CustomerDevice() + device.locale = 'en_GB' + device.fingerprint = 'fp_abc123xyz' + device.ipv4 = '203.0.113.0' + device.ipv6 = '2001:db8:85a3::8a2e:370:7334' + device.client = CustomerDeviceClient.MOBILE_WEB + device.os = OsType.IOS + merchant_account = MerchantAccount() + merchant_account.id = 'acct_1' + merchant_account.registration_date = '2023-05-01' + merchant_account.last_modified = '2023-05-02' + merchant_account.returning_customer = True + 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 + + customer = Customer() + customer.country = 'GB' + customer.id = 'cus_123456789' + customer.email = email + customer.name = 'John Smith' + customer.tax_number = 'GB123456789' + customer.phone = phone + customer.device = device + customer.merchant_account = merchant_account + + body = json.dumps(customer, cls=JsonSerializer) + + assert '"id": "cus_123456789"' in body + assert '"country": "GB"' in body + assert '"tax_number": "GB123456789"' in body + assert 'taxNumber' not in body + + read = ResponseWrapper(None, json.loads(body)) + + assert read.country == 'GB' + assert read.id == 'cus_123456789' + assert read.name == 'John Smith' + assert read.tax_number == 'GB123456789' + assert read.email.address == email.address + assert read.email.verified is True + assert read.phone.country_code == phone.country_code + assert read.phone.number == phone.number + for field in ('locale', 'fingerprint', 'ipv4', 'ipv6', 'client', 'os'): + assert getattr(read.device, field) == getattr(device, field), field + for field in ('id', 'registration_date', 'last_modified', 'returning_customer', + 'first_transaction_date', 'last_transaction_date', 'total_order_count', + 'last_payment_amount'): + assert getattr(read.merchant_account, field) == getattr(merchant_account, field), field + + def test_customer_spec_example_reads(self): + read = ResponseWrapper(None, { + 'country': 'GB', + 'id': 'cus_123456789', + 'email': {'address': 'johnsmith@example.com', 'verified': True}, + 'name': 'John Smith', + 'tax_number': 'GB123456789', + 'phone': {'country_code': '+44', 'number': '207 946 0000'}, + 'device': {'locale': 'en_GB'}, + }) + + assert read.country == 'GB' + assert read.id == 'cus_123456789' + assert read.email.address == 'johnsmith@example.com' + assert read.email.verified is True + assert read.name == 'John Smith' + assert read.tax_number == 'GB123456789' + assert read.phone.country_code == '+44' + assert read.phone.number == '207 946 0000' + assert read.device.locale == 'en_GB' From aa98b48861a02a6d8474e9e47c9da1b2d187ae52 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 7 Oct 2026 16:43:56 +0200 Subject: [PATCH 2/3] Docs and tests --- checkout_sdk/payments/setups/setups.py | 5 +++++ .../setups/payment_setups_integration_test.py | 10 ++++------ .../setups/payment_setups_serialization_test.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 625423e0..7ff50fb4 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -65,9 +65,11 @@ class CustomerDevice: # The type of client the customer uses to initiate the payment. Required when using # Cash App Pay. # [Optional] + # Enum: "web" "mobile_web" "app" client: CustomerDeviceClient # The operating system of the customer's device. # [Optional] + # Enum: "android" "ios" os: OsType @@ -204,6 +206,8 @@ class PaymentMethodBase: # The initialization state of the payment method. When you create a Payment Setup, this # defaults to disabled. # [Optional] + # Default: "disabled" + # Enum: "disabled" "enabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED @@ -963,6 +967,7 @@ class CashAppAction: # The type of action. # [Optional] # readOnly + # Enum: "redirect" type: CashAppActionType # The URL to redirect the customer to so they can authorize the payment with Cash App. # [Optional] diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index d0df2ad8..1fddf0bd 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -202,12 +202,10 @@ def test_should_create_payment_setup_with_cash_app(default_api): if 'cashapp' not in getattr(response, 'available_payment_methods', []): pytest.skip("Cash App Pay is not enabled on the sandbox processing channel") - assert_response(response, 'payment_methods.cashapp.status') - assert response.payment_methods.cashapp.initialization == PaymentMethodInitialization.ENABLED - assert response.payment_methods.cashapp.customer_profile_sharing is True - if hasattr(response.payment_methods.cashapp, 'action'): - assert response.payment_methods.cashapp.action.type == 'redirect' - assert response.payment_methods.cashapp.action.redirect_url + fetched = default_api.setups.get_payment_setup(response.id) + assert_response(fetched, 'payment_methods.cashapp.status') + assert fetched.payment_methods.cashapp.initialization == PaymentMethodInitialization.ENABLED + assert fetched.payment_methods.cashapp.customer_profile_sharing is True @pytest.mark.skip(reason="Integration test - requires a payment setup ready to be confirmed") diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index d2d6453e..bc06ba75 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -687,6 +687,16 @@ def test_device_os_serializes_each_spec_value(self, os_type, expected): assert _serialize(device) == {'os': expected} + def test_cash_app_customer_profile_sharing_false_is_sent(self): + cashapp = CashApp() + cashapp.customer_profile_sharing = False + + body = json.loads(json.dumps(cashapp, cls=JsonSerializer)) + + # Only unset attributes are omitted; an explicit False must still reach the API. + assert 'customer_profile_sharing' in body + assert body['customer_profile_sharing'] is False + def test_device_with_only_locale_serializes_only_locale(self): device = CustomerDevice() device.locale = 'en_US' @@ -833,6 +843,7 @@ def test_cash_app_round_trip_keeps_every_property_and_the_literal_address_keys(s '"administrative_district_level_1"', '"redirect_url"'): assert key in body, key assert 'address_line1' not in body + assert 'administrative_district_level1' not in body read = ResponseWrapper(None, json.loads(body)) From 9c248ef621f8dae793fab6ec8684615f69d6c82d Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 8 Oct 2026 15:29:57 +0200 Subject: [PATCH 3/3] Fine tune comments and tests --- checkout_sdk/payments/setups/setups.py | 4 ---- .../setups/payment_setups_integration_test.py | 3 ++- .../payment_setups_serialization_test.py | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/checkout_sdk/payments/setups/setups.py b/checkout_sdk/payments/setups/setups.py index 7ff50fb4..3b9bb6c1 100644 --- a/checkout_sdk/payments/setups/setups.py +++ b/checkout_sdk/payments/setups/setups.py @@ -65,11 +65,9 @@ class CustomerDevice: # The type of client the customer uses to initiate the payment. Required when using # Cash App Pay. # [Optional] - # Enum: "web" "mobile_web" "app" client: CustomerDeviceClient # The operating system of the customer's device. # [Optional] - # Enum: "android" "ios" os: OsType @@ -207,7 +205,6 @@ class PaymentMethodBase: # defaults to disabled. # [Optional] # Default: "disabled" - # Enum: "disabled" "enabled" initialization: PaymentMethodInitialization = PaymentMethodInitialization.DISABLED @@ -967,7 +964,6 @@ class CashAppAction: # The type of action. # [Optional] # readOnly - # Enum: "redirect" type: CashAppActionType # The URL to redirect the customer to so they can authorize the payment with Cash App. # [Optional] diff --git a/tests/payments/setups/payment_setups_integration_test.py b/tests/payments/setups/payment_setups_integration_test.py index 1fddf0bd..40b5fa94 100644 --- a/tests/payments/setups/payment_setups_integration_test.py +++ b/tests/payments/setups/payment_setups_integration_test.py @@ -181,7 +181,8 @@ def test_should_create_payment_setup_with_customer_identifiers(default_api): def test_should_create_payment_setup_with_cash_app(default_api): - """Cash App Pay: the response carries payment_methods.cashapp and its redirect action. + """Cash App Pay: the fetched Payment Setup carries payment_methods.cashapp with its status, + initialization and customer_profile_sharing. Skipped at runtime when the sandbox processing channel does not offer Cash App Pay. """ diff --git a/tests/payments/setups/payment_setups_serialization_test.py b/tests/payments/setups/payment_setups_serialization_test.py index bc06ba75..62acda72 100644 --- a/tests/payments/setups/payment_setups_serialization_test.py +++ b/tests/payments/setups/payment_setups_serialization_test.py @@ -933,6 +933,16 @@ def test_customer_spec_example_reads(self): 'tax_number': 'GB123456789', 'phone': {'country_code': '+44', 'number': '207 946 0000'}, 'device': {'locale': 'en_GB'}, + 'merchant_account': { + 'id': '1234', + 'registration_date': '2023-05-01T00:00:00.0000000', + 'last_modified': '2023-05-01T00:00:00.0000000', + 'returning_customer': True, + 'first_transaction_date': '2023-09-15T00:00:00.0000000', + 'last_transaction_date': '2025-03-28T00:00:00.0000000', + 'total_order_count': 6, + 'last_payment_amount': 55.99, + }, }) assert read.country == 'GB' @@ -944,3 +954,11 @@ def test_customer_spec_example_reads(self): assert read.phone.country_code == '+44' assert read.phone.number == '207 946 0000' assert read.device.locale == 'en_GB' + assert read.merchant_account.id == '1234' + assert read.merchant_account.registration_date == '2023-05-01T00:00:00.0000000' + assert read.merchant_account.last_modified == '2023-05-01T00:00:00.0000000' + assert read.merchant_account.returning_customer is True + assert read.merchant_account.first_transaction_date == '2023-09-15T00:00:00.0000000' + assert read.merchant_account.last_transaction_date == '2025-03-28T00:00:00.0000000' + assert read.merchant_account.total_order_count == 6 + assert read.merchant_account.last_payment_amount == 55.99