diff --git a/checkout_sdk/common/common.py b/checkout_sdk/common/common.py index 4cef37d4..6406ed90 100644 --- a/checkout_sdk/common/common.py +++ b/checkout_sdk/common/common.py @@ -38,28 +38,128 @@ class ResidentialStatusType(str, Enum): class AccountHolderIdentification: + """The account holder's or the sender's identification. + + Shared by several positions. In the card payout and unreferenced refund destinations, providing + identification details for the recipient increases the likelihood of a successful payout. In the + card payout sender, it is required if the card's issuer_country is one of AR, BR, CO or PE. In the + bank payout destination it is the account holder's government document identification, for + example the social security number (SSN). Other positions (payment sources, instruments, accounts) + follow their own schemas. + """ + # The type of identification. + # [Required] for card payouts and unreferenced refunds, [Optional] for bank payouts and regular + # payments + # Enum: "passport" "driving_license" "national_id" "company_registration" "tax_id" for card payouts + # and unreferenced refunds; "passport" "driving_licence" "national_id" for the bank payout and + # regular payment sender; free text (for example SSN) for the bank payout destination type: AccountHolderIdentificationType + # The identification number. + # [Required] for card payouts and unreferenced refunds, [Optional] for bank payouts and regular + # payments + # max 25 characters for the card payout and unreferenced refund destinations number: str + # If applicable, the country that issued the identification, as a two-letter ISO country code. + # Providing it increases the likelihood of a successful identity verification. + # [Optional] + # min 2 characters for card payouts and unreferenced refunds + # max 2 characters for card payouts and unreferenced refunds issuing_country: Country + # If applicable, the expiration date of the identification, in the format YYYY-MM-DD. Card + # payouts and unreferenced refunds only. + # [Optional] + # min 10 characters + # max 10 characters date_of_expiry: str class AccountHolder: + """The account holder details. + + Shared by the card payout destinations (card, network_token, id, token), the unreferenced refund + destinations (card, network_token, id, token), the bank payout bank_account destination, and by + payment sources, instruments and payment contexts, whose own schemas decide which properties + apply. The tags below describe the payout and unreferenced refund positions. The card payout id + and token destinations accept only type and the name properties. + """ + # The type of account holder. + # [Required] for card payouts, bank payouts and the unreferenced refund card and network_token + # destinations, [Optional] for the unreferenced refund id and token destinations + # Enum: "individual" "corporate" "government" type: AccountHolderType + # The account holder's full name. + # [Optional] + # Deprecated: not in the request schema of any account holder. Still returned in the PayPal + # payment details source account_holder. Retained for backwards compatibility. Will be removed + # in the next major version. There is no replacement. full_name: str + # The account holder's first name (individual). For card payouts, the combination of + # first_name, middle_name and last_name should exceed 3 letters in length. This must be a valid + # legal name. + # [Required] for individual account holders in card payouts, bank payouts and the unreferenced + # refund card and network_token destinations, [Optional] for the unreferenced refund id and token + # destinations + # max 50 characters for card payouts and unreferenced refunds first_name: str + # The account holder's middle name (individual). Card payouts and unreferenced refunds only. + # [Optional] + # max 50 characters middle_name: str + # The account holder's last name (individual). For card payouts, the combination of first_name, + # middle_name and last_name should exceed 3 letters in length. This must be a valid legal name. + # [Required] for individual account holders in card payouts, bank payouts and the unreferenced + # refund card and network_token destinations, [Optional] for the unreferenced refund id and token + # destinations + # max 50 characters for card payouts and unreferenced refunds last_name: str + # The account holder's email address. Card payouts and unreferenced refunds only. + # [Optional] + # max 255 characters email: str + # The account holder's gender. + # [Optional] + # Deprecated: not in the request schema of any account holder. Still returned in the Klarna + # payment details source account_holder. Retained for backwards compatibility. Will be removed + # in the next major version. There is no replacement. gender: str + # The account holder's company name (corporate, government). This must be a valid legal name. + # For bank payouts, the full legal name of a registered company that holds the account. + # [Required] for corporate and government account holders + # max 50 characters for card payouts and unreferenced refunds company_name: str + # The legal account holder's tax number or reference. Bank payouts only. + # [Optional] tax_id: str + # The account holder's date of birth, in the format YYYY-MM-DD (individual). Strongly + # recommended where available for bank payouts. + # [Optional] + # min 10 characters for card payouts and unreferenced refunds + # max 10 characters for card payouts and unreferenced refunds date_of_birth: str + # The account holder's country of birth, as a two-letter ISO country code (individual). + # [Optional] + # min 2 characters for card payouts and unreferenced refunds + # max 2 characters for card payouts and unreferenced refunds country_of_birth: Country + # The account holder's residential status (individual). Bank payouts only. + # [Optional] + # Enum: "resident" "non_resident" residential_status: ResidentialStatusType + # The account holder's billing address. For card payouts and unreferenced refunds, required if + # your company is incorporated in the United States. For bank payouts, the current billing + # address associated with the destination account. + # [Required] for individual account holders in bank payouts, [Optional] otherwise billing_address: Address + # The account holder's phone number. When set, country_code (min 1, max 7 characters) and + # number (min 6, max 25 characters) are required for card payouts and unreferenced refunds. + # [Optional] phone: Phone + # The account holder's identification. + # [Optional] identification: AccountHolderIdentification + # Whether the ANI (account name inquiry) check should be performed with the scheme (Visa or + # Mastercard). Used by payment sources; not part of the payout or unreferenced refund positions. + # [Optional] account_name_inquiry: bool diff --git a/checkout_sdk/payments/payments.py b/checkout_sdk/payments/payments.py index d4d0a400..d1bee1e7 100644 --- a/checkout_sdk/payments/payments.py +++ b/checkout_sdk/payments/payments.py @@ -22,6 +22,10 @@ class CaptureType(str, Enum): class InstructionScheme(str, Enum): + """The payment scheme type of a bank payout instruction. Enum: "local" "instant".""" + # Deprecated: not in the request schema, which only lists local and instant; still returned in + # PayoutDetails (instruction.scheme). Retained for backwards compatibility. Will be removed in the + # next major version. There is no replacement. SWIFT = 'swift' LOCAL = 'local' INSTANT = 'instant' @@ -58,12 +62,26 @@ class PaymentType(str, Enum): class PaymentDestinationType(str, Enum): + """The payout destination type. + + Shared by two discriminators: the card payout destination (card, network_token, id, token) + and the bank payout destination (id, bank_account). + """ BANK_ACCOUNT = 'bank_account' CARD = 'card' ID = 'id' + NETWORK_TOKEN = 'network_token' TOKEN = 'token' +class NetworkTokenType(str, Enum): + """The network token type.""" + VTS = 'vts' + MDES = 'mdes' + APPLEPAY = 'applepay' + GOOGLEPAY = 'googlepay' + + class Exemption(str, Enum): LOW_VALUE = 'low_value' SECURE_CORPORATE_PAYMENT = 'secure_corporate_payment' @@ -198,26 +216,105 @@ class BillingDescriptor: class Remitance: + """The remittance details of a bank payout instruction.""" + # A merchant-provided reference for the remittance payment. This value is used for payment + # reconciliation and is reviewed by our banking partner. + # [Optional] + # max 80 characters reference: str class PaymentInstruction: + """Additional details about the instruction of a payout, an unreferenced refund or a payment. + + Card payouts and unreferenced refunds use purpose and funds_transfer_type; bank payouts use + scheme and remittance. Regular payments (PaymentRequest), hosted payments + (HostedPaymentsSessionRequest) and payment links (PaymentLinkRequest) use purpose only. Payment + sessions use their own sessions.Instruction class, which also carries purpose only. + """ + # The purpose of the card payout, unreferenced refund or payment. Required if the card's + # issuer_country is one of AR, BD, CL, CO, EG, IN, MX or SA for card payouts, and AR, BD, CL, + # CO, EG, IN or MX for unreferenced refunds. For payments, it might be required for AFT + # transactions depending on the card's issuer_country. + # [Optional] + # Enum: "family_support" "expatriation" "travel_and_tourism" "education" "medical_treatment" + # "emergency_need" "leisure" "savings" "gifts" "donations" "financial_services" "it_services" + # "investment" "insurance" "loan_payment" "pension" "royalties" "other" "income" purpose: str + # The charge bearer of a bank payout. + # [Optional] + # Deprecated: not in the request schema; still returned in PayoutDetails (instruction.charge_bearer). + # Retained for backwards compatibility. Will be removed in the next major version. There is no + # replacement. charge_bearer: str + # Whether the bank payout may be repaired. + # [Optional] + # Deprecated: not in the request schema; still returned in PayoutDetails (instruction.repair). + # Retained for backwards compatibility. Will be removed in the next major version. There is no + # replacement. repair: bool + # An option to restrict the payment scheme type of a bank payout, when not available payouts + # will fail. + # [Optional] + # Enum: "local" "instant" scheme: InstructionScheme + # The remittance details of a bank payout. + # [Optional] remittance: Remitance + # The FX quote identifier of a bank payout. + # [Optional] + # Deprecated: not in the request schema; still returned in PayoutDetails (instruction.quote_id). + # Retained for backwards compatibility. Will be removed in the next major version. There is no + # replacement. quote_id: str + # The funds transfer type code for the type of card payout you're performing. You can only use + # codes that have been assigned to you by the card schemes and Checkout.com, based on your + # business case and requirements. If you have only been assigned one funds transfer type code, + # this field is optional. For unreferenced refunds it is required if + # processing.processing_speed is fast: use MI for Visa and C60 for Mastercard. + # [Required] for card payouts, [Optional] for unreferenced refunds funds_transfer_type: str class PayoutBillingDescriptor: + """The billing descriptor of a bank payout. + + Beneficiary banks typically construct the billing descriptor using the billing descriptor + reference and sender information. For direct sellers, the sender is typically your entity's legal + company name. If the ultimate sender is provided, then it is the values of the sender object. + """ + # The reference shown on the beneficiary's bank statement. The length varies per beneficiary + # bank; in general, fewer than 11 characters are recommended. References are not supported in + # the US. + # [Required] + # min 11 characters + # max 15 characters reference: str # Payment Sender class PaymentSender: + """The sender of a payout or of a payment's funds. Set type through a subclass. + + Shared by three positions, each with its own rules: + - card payout sender (PayoutRequest.sender): PaymentIndividualSender, PaymentCorporateSender or + PaymentGovernmentSender. Required for money transfer card payouts. + - bank payout sender (PayoutRequest.sender): PaymentInstrumentSender, PaymentIndividualSender or + PaymentCorporateSender. The ultimate sender of the payout, that is, your customer; not + supported for Platform or Direct Seller Checkout.com clients. + - regular payment sender (PaymentRequest, hosted payments, payment links and payment sessions): + PaymentIndividualSender, PaymentCorporateSender or PaymentInstrumentSender. + """ + # The sender type. Set by each subclass. + # [Required] + # Enum: "individual" "corporate" "instrument" "government" (government for card payouts only) type: PaymentSenderType + # The unique reference for the sender. For card payouts, the unique identifier for the sender, + # for example a customer number; only alphanumeric characters are supported. For bank payouts, a + # unique reference to the underlying client's customer (a global KYC requirement). + # [Required] for card payouts and bank payouts, [Optional] for regular payments + # min 5 characters for card payouts + # max 15 characters for card payouts reference: str def __init__(self, type_p: PaymentSenderType): @@ -225,11 +322,33 @@ def __init__(self, type_p: PaymentSenderType): class PaymentCorporateSender(PaymentSender): + """A corporate sender (type corporate), for card payouts, bank payouts and regular payments.""" + # The sender's company name. For card payouts, this must be a valid legal name. + # [Required] + # max 35 characters for card payouts company_name: str + # The sender's registered corporate address. For card payouts, address_line1, city and country + # are required; state is required if country is US or CA, and zip if country is US. + # [Required] address: Address + # The unique reference for the sender. For card payouts, for example a customer number; only + # alphanumeric characters are supported. For bank payouts, a unique reference to the underlying + # client's customer (a global KYC requirement). + # [Required] for card payouts and bank payouts, [Optional] for regular payments + # min 5 characters for card payouts + # max 15 characters for card payouts reference: str + # The type of identifier used as the reference. Card payouts only. + # [Required] for card payouts, not used by bank payouts or regular payments + # Enum: "other" reference_type: str + # The source of the funds used to fund the card payout. Card payouts only. + # [Required] for card payouts, not used by bank payouts or regular payments + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" source_of_funds: SourceOfFunds + # The sender's identification. Card payouts only. Required if the card's issuer_country is one + # of AR, BR, CO or PE. + # [Optional] identification: AccountHolderIdentification def __init__(self): @@ -244,11 +363,31 @@ def __init__(self): # today because Python's PaymentSender is shared across all three slots; a # future split into a dedicated CardPayoutSender hierarchy would fix it. class PaymentGovernmentSender(PaymentSender): + """A government sender (type government). Card payouts only.""" + # The sender's company name. This must be a valid legal name. + # [Required] + # max 35 characters company_name: str + # The sender's registered corporate address. address_line1, city and country are required; + # state is required if country is US or CA, and zip if country is US. + # [Required] address: Address + # The unique identifier for the sender. For example, a customer number. Only alphanumeric + # characters are supported. + # [Required] + # min 5 characters + # max 15 characters reference: str + # The type of identifier used as the reference. + # [Required] + # Enum: "other" reference_type: str + # The source of the funds used to fund the card payout. + # [Required] + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" source_of_funds: SourceOfFunds + # The sender's identification. Required if the card's issuer_country is one of AR, BR, CO or PE. + # [Optional] identification: AccountHolderIdentification def __init__(self): @@ -262,17 +401,70 @@ def __init__(self): class PaymentIndividualSender(PaymentSender): + """An individual sender (type individual), for card payouts, bank payouts and regular payments.""" + # The sender's first name. For card payouts, the combination of first_name, middle_name and + # last_name should exceed 3 letters in length, and this must be a valid legal name. For bank + # payouts, the legal first name of the ultimate sender. + # [Required] + # max 50 characters for card payouts first_name: str + # The sender's middle name. Card payouts only. + # [Optional] + # max 50 characters middle_name: str + # The sender's last name. For card payouts, the combination of first_name, middle_name and + # last_name must exceed 3 letters in length, and this must be a valid legal name. For bank + # payouts, the legal last name of the ultimate sender. + # [Required] + # max 50 characters for card payouts last_name: str + # The sender's date of birth in a YYYY-MM-DD format. Bank payouts only (card payouts and regular + # payments use date_of_birth). Strongly recommended where available. + # [Optional] + # Format: date + # max 10 characters dob: str + # The sender's address. For card payouts, the sender's residential address, where + # address_line1, city and country are required; state is required if country is US or CA, and + # zip if country is US. + # [Required] address: Address + # The sender's identification. For card payouts, required if the card's issuer_country is one + # of AR, BR, CO or PE, and accepts type, number, issuing_country and date_of_expiry. For bank + # payouts and regular payments, accepts type (passport, driving_licence, national_id), number and + # issuing_country. + # [Optional] identification: AccountHolderIdentification + # The unique reference for the sender. For card payouts, for example a customer number; only + # alphanumeric characters are supported. For bank payouts, a unique reference to the underlying + # client's customer (a global KYC requirement). + # [Required] for card payouts and bank payouts, [Optional] for regular payments + # min 5 characters for card payouts + # max 15 characters for card payouts reference: str + # The type of identifier used as the sender.reference. Card payouts only. + # [Required] for card payouts, not used by bank payouts or regular payments + # Enum: "other" reference_type: str + # The source of the funds used to fund the card payout. Card payouts only. + # [Required] for card payouts, not used by bank payouts or regular payments + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" source_of_funds: SourceOfFunds + # The sender's date of birth, in the format YYYY-MM-DD. For card payouts, required for + # cross-border money transfers. Card payouts and regular payments (bank payouts use dob). + # [Optional] + # min 10 characters for card payouts + # max 10 characters date_of_birth: str + # The sender's country of birth, as a two-letter ISO country code. Card payouts only. + # [Optional] + # min 2 characters + # max 2 characters country_of_birth: Country + # The sender's nationality, as a two-letter ISO country code. Card payouts only. + # [Optional] + # min 2 characters + # max 2 characters nationality: Country def __init__(self): @@ -280,6 +472,12 @@ def __init__(self): class PaymentInstrumentSender(PaymentSender): + """An instrument sender (type instrument), for bank payouts and regular payments. + + The sender information is retrieved from the destination (bank payouts) or from the payment + instrument (regular payments). reference is inherited from PaymentSender: [Required] for bank + payouts, [Optional] for regular payments. + """ def __init__(self): super().__init__(PaymentSenderType.INSTRUMENT) @@ -323,18 +521,59 @@ def __init__(self): class PaymentRequestNetworkTokenSource(PaymentRequestSource): + """A network token payment source.""" + # The network token PAN. + # [Required] token: str + # The expiry month of the token. + # [Required] + # min 1 expiry_month: int + # The expiry year of the token. + # [Required] expiry_year: int - token_type: str + # The type of token. + # [Required] + # Enum: "vts" "mdes" "applepay" "googlepay" + token_type: NetworkTokenType + # The Base64 encoded cryptographic identifier (TAVV) used by card schemes to validate the token + # verification result. Optional if previous_payment_id is specified and 3ds.enabled is false. + # [Required] + # max 100 characters cryptogram: str + # The Electronic Commerce Indicator (ECI) security level associated with the token. Optional if + # previous_payment_id is specified and 3ds.enabled is false. + # [Required] + # max 2 characters eci: 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. Set to true when you provide card details + # that you already have on file for the customer. + # [Optional] + # Default: false stored: bool + # This must be set to true if you intend to reuse the payment credentials in subsequent payments. + # Setting the field as false will mean that a payment instrument will not be included in the payment + # response. + # [Optional] + # Default: true store_for_future_use: bool + # The customer's name. + # [Optional] name: str + # The card verification value/code. 3 digits, except for American Express (4 digits). + # [Optional] + # min 3 characters + # max 4 characters cvv: str + # The customer's billing address. + # [Optional] billing_address: Address + # The customer's phone number. + # [Optional] phone: Phone + # Information about the account holder of the card. + # [Optional] account_holder: AccountHolder def __init__(self): @@ -990,9 +1229,11 @@ class ProcessingSettings: # pattern ^\d{6}$ # 6 characters partner_code: str - # Not declared on any processing component schema; it appears only in inline schemas. - # 'fast' (only for unreferenced refunds / card payouts) + # The speed at which the payout or the unreferenced refund is processed. Only applicable for + # unreferenced refunds. Not declared on any processing component schema; it appears only in + # the inline processing objects of card payouts and unreferenced refunds. # [Optional] + # Enum: "fast" processing_speed: str # The scheme transaction link identifier. # [Optional] @@ -1031,13 +1272,38 @@ class Product: sku: str +class CardPayoutItem: + """The order's line item or product that is being paid out (card payouts only).""" + # The item type. For example, physical or digital. This field is used to provide item level + # metadata where applicable. + # [Optional] + # Enum: "digital" "physical" + type: ItemType + # The digital item type. If type is set to digital, this field is required. + # [Optional] + # Enum: "blockchain" "cbdc" "cryptocurrency" "nft" "stablecoin" + sub_type: ProductSubType + + class PaymentCustomerRequest(CustomerRequest): tax_number: str class PaymentSegment: + """The dimension details about business segment for payment request. At least one dimension + required. + """ + # The brand of business segment. + # [Optional] + # max 50 characters brand: str + # The category of business segment. + # [Optional] + # max 50 characters business_category: str + # The market of business segment. + # [Optional] + # max 50 characters market: str @@ -1143,6 +1409,9 @@ class PaymentRequest: # Payout Request Source class PayoutRequestSource: + """The source of a card payout, bank payout or unreferenced refund.""" + # The payout or unreferenced refund source type. Set by each subclass. + # [Required] type: PayoutSourceType def __init__(self, type_p: PayoutSourceType): @@ -1150,6 +1419,16 @@ def __init__(self, type_p: PayoutSourceType): class PayoutRequestCurrencyAccountSource(PayoutRequestSource): + """A currency account source (type currency_account). + + Used by card payouts, bank payouts and unreferenced refunds. + """ + # The ID of the currency account that will fund the payout or the unreferenced refund. For + # bank payouts, its holding currency must match the payout's currency. + # [Required] + # ^(ca)_(\w{26})$ + # min 29 characters + # max 29 characters id: str def __init__(self): @@ -1157,6 +1436,10 @@ def __init__(self): class PayoutRequestEntitySource(PayoutRequestSource): + """An entity source (type entity). Bank payouts only.""" + # The entity or sub-entity's identifier, only applies to Platform Clients. + # [Required] + # ^(ent)_(\w{26})$ id: str def __init__(self): @@ -1165,6 +1448,13 @@ def __init__(self): # Payment Request Destination class PaymentRequestDestination: + """The destination of the payout or of the unreferenced refund. + + You can encrypt this field by using the Cko-Encryption-Key-Id and Cko-Encrypted-Properties + headers. + """ + # The payout or unreferenced refund destination type. Set by each subclass. + # [Required] type: PaymentDestinationType def __init__(self, type_p: PaymentDestinationType): @@ -1172,15 +1462,44 @@ def __init__(self, type_p: PaymentDestinationType): class PaymentBankAccountDestination(PaymentRequestDestination): + """A bank account payout destination (bank payout, type bank_account).""" + # The type of the bank account. Note that some beneficiary banks do not accept payouts to + # savings accounts. + # [Required] account_type: AccountType + # Number (which can contain letters) that identifies the local account. + # [Optional] account_number: str + # Code that identifies the bank. Also referred as Routing Number in the United States. + # [Optional] bank_code: str + # The combination of bank code and/or branch code and account number. + # [Optional] bban: str + # Code that identifies the bank branch. + # [Optional] branch_code: str + # Internationally agreed standard for identifying bank accounts. Lengths vary per + # destination_country. + # [Optional] iban: str + # 8 or 11 character code which identifies the bank or bank branch. + # [Optional] swift_bic: str + # The two-letter ISO country code of where the account is based. Note that not all + # countries are available. + # [Required] + # min 2 characters + # max 2 characters country: Country + # The account holder details. + # [Optional] account_holder: AccountHolder + # The bank details. + # [Optional] + # Deprecated: not in the request schema of the bank account payout destination; bank only exists + # on the Instruments bank account schemas. Retained for backwards compatibility. Will be removed in + # the next major version. There is no replacement. bank: BankDetails def __init__(self): @@ -1188,30 +1507,277 @@ def __init__(self): class PaymentRequestIdDestination(PaymentRequestDestination): - id: str + """A payment instrument payout destination (type id). + Valid for card payouts, bank payouts and unreferenced refunds. + """ + # The payment source ID. This will be an ID with the prefix src_. + # [Required] + # ^(src)_(\w{26})$ + # min 30 characters + # max 30 characters + id: str + # The payout destination account holder. Required for card payouts, where this position + # accepts only type and first_name, middle_name, last_name (individual) or company_name + # (corporate, government). Optional for unreferenced refunds, where this position accepts + # type (individual, corporate, government); first_name, middle_name and last_name (max 50 + # characters each), date_of_birth (YYYY-MM-DD) and country_of_birth for individual; + # company_name for corporate and government; and billing_address, phone, identification and + # email (max 255 characters) for all three. Not part of bank payouts, whose id destination + # accepts only type and id. + # [Required] for card payouts, [Optional] for unreferenced refunds account_holder: AccountHolder def __init__(self): super().__init__(PaymentDestinationType.ID) +class PaymentRequestCardDestination(PaymentRequestDestination): + """A card destination (type card), for card payouts and unreferenced refunds.""" + # The card number. You can encrypt this field by using the Cko-Encryption-Key-Id and + # Cko-Encrypted-Properties headers. + # [Required] + # max 19 characters + number: str + # The card's expiration month. + # [Required] + expiry_month: int + # The card's expiration year. + # [Required] + expiry_year: int + # The destination account holder, for card payouts and unreferenced refunds. This position + # accepts type (individual, corporate, government); first_name, middle_name and last_name + # (max 50 characters each), date_of_birth (YYYY-MM-DD) and country_of_birth for individual; + # company_name for corporate and government; and billing_address, phone, identification and + # email (max 255 characters) for all three. For unreferenced refunds, first_name and + # last_name are required for individual, and company_name for corporate and government. + # [Required] + account_holder: AccountHolder + + def __init__(self): + super().__init__(PaymentDestinationType.CARD) + + +class PaymentRequestNetworkTokenDestination(PaymentRequestDestination): + """A network token destination (type network_token), for card payouts and unreferenced refunds. + + Leave cryptogram and eci unset when not used: assigning None sends null. + """ + # The network token's Primary Account Number (PAN). + # [Required] + token: str + # The network token's expiration month. + # [Required] + # min 1 + expiry_month: int + # The network token's expiration year. + # [Required] + expiry_year: int + # The network token type. + # [Required] + # Enum: "vts" "mdes" "applepay" "googlepay" + token_type: NetworkTokenType + # The network token's Base64-encoded cryptographic identifier (TAVV). The cryptogram is + # used by card schemes to validate the token verification result. + # [Optional] + # max 50 characters + cryptogram: str + # The network token's Electronic Commerce Indicator (ECI) security level. + # [Optional] + # max 2 characters + eci: str + # The destination account holder, for card payouts and unreferenced refunds. This position + # accepts type (individual, corporate, government); first_name, middle_name and last_name + # (max 50 characters each), date_of_birth (YYYY-MM-DD) and country_of_birth for individual; + # company_name for corporate and government; and billing_address, phone, identification and + # email (max 255 characters) for all three. For unreferenced refunds, first_name and + # last_name are required for individual, and company_name for corporate and government. + # [Required] + account_holder: AccountHolder + + def __init__(self): + super().__init__(PaymentDestinationType.NETWORK_TOKEN) + + +class PaymentRequestTokenDestination(PaymentRequestDestination): + """A token destination (type token), for card payouts and unreferenced refunds.""" + # The Checkout.com card token ID. This will be an ID with the prefix tok_. + # [Required] + # ^(tok)_(\w{26})$ + # min 30 characters + # max 30 characters + token: str + # The destination account holder. Required for card payouts, where this position accepts + # only type and first_name, middle_name, last_name (individual) or company_name (corporate, + # government). Optional for unreferenced refunds, where this position accepts type + # (individual, corporate, government); first_name, middle_name and last_name (max 50 + # characters each), date_of_birth (YYYY-MM-DD) and country_of_birth for individual; + # company_name for corporate and government; and billing_address, phone, identification and + # email (max 255 characters) for all three. + # [Required] for card payouts, [Optional] for unreferenced refunds + account_holder: AccountHolder + + def __init__(self): + super().__init__(PaymentDestinationType.TOKEN) + + +class CardPayoutProcessing: + """Information related to the processing of a card payout or of an unreferenced refund. + + Card payouts use all four properties; unreferenced refunds use processing_speed only. + """ + # The unique identifier for Visa-registered ramp providers. Must only contain alphanumeric + # characters. If you're a Visa-registered ramp provider operating with affiliates, this field is + # required. + # [Optional] + # ^[a-zA-Z0-9]{1,15}$ + # max 15 characters + affiliate_id: str + # The affiliate URL. If you're a Visa-registered ramp provider operating with affiliates, this + # field is required. + # [Optional] + affiliate_url: str + # The speed at which the payout is processed. Only applicable for unreferenced refunds. + # [Optional] + # Enum: "fast" + processing_speed: str + # The two-letter ISO country code of the purchase country. If you're a Visa-registered ramp + # provider operating with affiliates, this field is required. + # [Optional] + # max 2 characters + purchase_country: Country + + # Request Payout class PayoutRequest: + """A card payout or bank payout request, sent to POST /payments.""" + # The source of the payout. For bank payouts, the source from which the payout is funded. + # [Required] source: PayoutRequestSource + # The destination of the payout. You can encrypt this field by using the Cko-Encryption-Key-Id + # and Cko-Encrypted-Properties headers. Card payouts take PaymentRequestCardDestination, + # PaymentRequestNetworkTokenDestination, PaymentRequestIdDestination or + # PaymentRequestTokenDestination; bank payouts take PaymentRequestIdDestination or + # PaymentBankAccountDestination. + # [Required] destination: PaymentRequestDestination + # The amount to pay out, in minor units. + # [Optional] for card payouts, [Required] for bank payouts + # min 0 amount: int + # The three-letter ISO 4217 code for the currency you want to pay out to the recipient. For bank + # payouts, it should match the currency of the destination account. + # [Required] + # min 3 characters for bank payouts + # max 3 characters for bank payouts currency: Currency + # A custom reference to identify the payout. For example, an order number. + # [Optional] + # max 50 characters for card payouts reference: str + # The billing descriptor for bank payouts. Beneficiary banks typically construct it using the + # billing descriptor reference and the sender information. + # [Required] for bank payouts, not used by card payouts billing_descriptor: PayoutBillingDescriptor + # The sender of the payout. Required for money transfer card payouts. For bank payouts, the + # ultimate sender of the payout, that is, your customer; not supported for Platform or Direct + # Seller Checkout.com clients. + # [Optional] sender: PaymentSender + # Additional details about the payout instruction. + # [Required] for card payouts, [Optional] for bank payouts instruction: PaymentInstruction + # The unique identifier for the processing channel. + # [Required] + # ^(pc)_(\w{26})$ + # min 29 characters for card payouts + # max 29 characters for card payouts processing_channel_id: str + # The dimension details about business segment for payment request. At least one dimension + # required. + # [Optional] segment: PaymentSegment - items: list # payments.Product + # The order's line items. Card payouts only. + # [Optional] + items: list # payments.CardPayoutItem + # Stores additional information about the transaction with custom fields. Card payouts only. + # [Optional] metadata: dict + # The identifier of an existing payment from the recurring series, if the payout is a recurring + # payment that uses stored card details. Card payouts only. + # [Optional] previous_payment_id: str - processing: ProcessingSettings + # Information related to the processing of the payout. Card payouts only. + # [Optional] + processing: CardPayoutProcessing + + +# Request Unreferenced Refund +class UnreferencedRefundRequest: + """An unreferenced refund request, sent to POST /payments. + + payment_type is set to UnreferencedRefund by the constructor. + """ + # The source of the unreferenced refund. Takes PayoutRequestCurrencyAccountSource. + # [Required] + source: PayoutRequestSource + # The destination of the unreferenced refund. Takes PaymentRequestCardDestination, + # PaymentRequestNetworkTokenDestination, PaymentRequestIdDestination or + # PaymentRequestTokenDestination. + # [Required] + destination: PaymentRequestDestination + # Additional details about the unreferenced refund instruction. Uses purpose and + # funds_transfer_type. funds_transfer_type is required if processing.processing_speed is + # fast; its value should be based on the destination card scheme: for Visa, use MI; for + # Mastercard, use C60. purpose is required if the card's issuer_country is one of AR, BD, + # CL, CO, EG, IN or MX; Enum: "family_support" "expatriation" "travel_and_tourism" + # "education" "medical_treatment" "emergency_need" "leisure" "savings" "gifts" "donations" + # "financial_services" "it_services" "investment" "insurance" "loan_payment" "pension" + # "royalties" "other" "income". + # [Optional] + instruction: PaymentInstruction + # The amount in minor currency. + # [Required] + # Format: int64 + # min 1 + # max 9999999999 + amount: int + # The three-letter ISO currency code of the payment. + # [Required] + # min 3 characters + # max 3 characters + currency: Currency + # The type of the payment. Set to UnreferencedRefund by the constructor. + # [Required] + # Enum: "UnreferencedRefund" + payment_type: str + # The processing channel identifier. + # [Required] + # ^(pc)_(\w{26})$ + processing_channel_id: str + # An internal reference you can later use to identify this payment. + # [Optional] + reference: str + # A set of key-value pairs that you can attach to the refund request. It can be useful for + # storing additional information in a structured format. This object only allows one level + # of depth, so cannot accept non-primitive data types such as objects or arrays. + # [Optional] + metadata: dict + # The previous related payment identifier. This could be the ID of the payment that you want + # to refund. + # [Optional] + previous_payment_id: str + # The dimension details about business segment for payment request. At least one dimension + # required. + # [Optional] + segment: PaymentSegment + # Returns information related to the processing of the payment. Only processing_speed is part + # of this position. + # [Optional] + processing: CardPayoutProcessing + + def __init__(self): + self.payment_type = 'UnreferencedRefund' # Query diff --git a/checkout_sdk/payments/payments_client.py b/checkout_sdk/payments/payments_client.py index 6b03e49b..179fa5a2 100644 --- a/checkout_sdk/payments/payments_client.py +++ b/checkout_sdk/payments/payments_client.py @@ -6,7 +6,7 @@ from checkout_sdk.client import Client from checkout_sdk.payments.payments import PaymentRequest, PayoutRequest, CaptureRequest, AuthorizationRequest, \ RefundRequest, VoidRequest, PaymentsQueryFilter, CancelScheduledRetryRequest, ReversePaymentRequest, \ - PaymentsSearchRequest + PaymentsSearchRequest, UnreferencedRefundRequest class PaymentsClient(Client): @@ -23,6 +23,19 @@ def request_payment(self, payment_request: PaymentRequest, idempotency_key: str def request_payout(self, payout_request: PayoutRequest, idempotency_key: str = None): return self._api_client.post(self.__PAYMENTS_PATH, self._sdk_authorization(), payout_request, idempotency_key) + def request_unreferenced_refund(self, unreferenced_refund_request: UnreferencedRefundRequest, + idempotency_key: str = None): + """Send an unreferenced refund request to POST /payments. + + Args: + unreferenced_refund_request: The unreferenced refund request body. + idempotency_key: Optional idempotency key. + Returns: + ResponseWrapper with the payment response data. + """ + return self._api_client.post(self.__PAYMENTS_PATH, self._sdk_authorization(), unreferenced_refund_request, + idempotency_key) + def get_payments_list(self, query: PaymentsQueryFilter): return self._api_client.get(self.__PAYMENTS_PATH, self._sdk_authorization(), query) diff --git a/checkout_sdk/payments/sessions/sessions.py b/checkout_sdk/payments/sessions/sessions.py index 87b1b861..45d34a07 100644 --- a/checkout_sdk/payments/sessions/sessions.py +++ b/checkout_sdk/payments/sessions/sessions.py @@ -101,6 +101,17 @@ class InstructionPurpose(str, Enum): class Instruction: + """Details about the payment instruction of a payment session. Payment sessions use purpose only. + + Payouts, unreferenced refunds, regular payments, hosted payments and payment links use + payments.PaymentInstruction instead. + """ + # The purpose of the payment. This field might be required for AFT transactions depending on + # the card's issuer_country. + # [Optional] + # Enum: "family_support" "expatriation" "travel_and_tourism" "education" "medical_treatment" + # "emergency_need" "leisure" "savings" "gifts" "donations" "financial_services" "it_services" + # "investment" "insurance" "loan_payment" "pension" "royalties" "other" "income" purpose: InstructionPurpose diff --git a/tests/payments/bank_payout_request_serialization_test.py b/tests/payments/bank_payout_request_serialization_test.py new file mode 100644 index 00000000..527ceb7e --- /dev/null +++ b/tests/payments/bank_payout_request_serialization_test.py @@ -0,0 +1,236 @@ +import json + +from checkout_sdk.common.common import AccountHolder, AccountHolderIdentification, Address, Phone, \ + ResidentialStatusType +from checkout_sdk.common.enums import AccountHolderIdentificationType, AccountHolderType, AccountType, Country, \ + Currency +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.payments import InstructionScheme, PaymentBankAccountDestination, \ + PaymentIndividualSender, PaymentInstruction, PaymentSegment, PayoutBillingDescriptor, PayoutRequest, \ + PayoutRequestEntitySource, Remitance + +# The spec has no request example for BankPayoutRequest; values below come from the per-property +# examples where the schema declares one and otherwise satisfy the property's pattern and length. +# The spec examples for date_of_birth and dob carry a time part although the description asks for +# YYYY-MM-DD, so the date part is used. +BILLING_ADDRESS_FIXTURE = { + 'address_line1': '123 High St.', + 'address_line2': 'Flat 456', + 'city': 'London', + 'zip': 'SW1A 1AA', + 'country': 'GB' +} + +REQUEST_FIXTURE = { + 'source': {'type': 'entity', 'id': 'ent_w4jelhppmfiufdnatam37wrfc4'}, + 'destination': { + 'type': 'bank_account', + 'account_type': 'savings', + 'account_number': '13654567455', + 'bank_code': '123-456', + 'branch_code': '6443', + 'bban': '3704 0044 0532 0130 00', + 'iban': 'HU93116000060000000012345676', + 'swift_bic': '37040044', + 'country': 'GB', + 'account_holder': { + 'type': 'individual', + 'first_name': 'John', + 'last_name': 'Smith', + 'tax_id': '123456', + 'date_of_birth': '1986-01-01', + 'country_of_birth': 'GB', + 'residential_status': 'resident', + 'billing_address': BILLING_ADDRESS_FIXTURE, + 'phone': {'country_code': '+1', 'number': '415 555 2671'}, + 'identification': {'type': 'passport', 'number': 'E12345678', 'issuing_country': 'GB'} + } + }, + 'amount': 6540, + 'currency': 'USD', + 'reference': 'Withdrawal', + 'billing_descriptor': {'reference': 'WITHDRAWAL01'}, + 'sender': { + 'type': 'individual', + 'reference': '8285282045818', + 'first_name': 'John', + 'last_name': 'Jones', + 'dob': '1985-05-15', + 'address': BILLING_ADDRESS_FIXTURE, + 'identification': {'type': 'driving_licence', 'number': '1234', 'issuing_country': 'GT'} + }, + 'instruction': {'scheme': 'local', 'remittance': {'reference': 'INV-2026-0001'}}, + 'processing_channel_id': 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', + 'segment': {'brand': 'Acme Corporation', 'business_category': 'Inward Payments', 'market': 'UK'} +} + +REQUEST_KEYS = {'source', 'destination', 'amount', 'currency', 'reference', 'billing_descriptor', 'sender', + 'instruction', 'processing_channel_id', 'segment'} + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +def _address(): + address = Address() + address.address_line1 = '123 High St.' + address.address_line2 = 'Flat 456' + address.city = 'London' + address.zip = 'SW1A 1AA' + address.country = Country.GB + return address + + +def _destination(): + phone = Phone() + phone.country_code = '+1' + phone.number = '415 555 2671' + + identification = AccountHolderIdentification() + identification.type = AccountHolderIdentificationType.PASSPORT + identification.number = 'E12345678' + identification.issuing_country = Country.GB + + account_holder = AccountHolder() + account_holder.type = AccountHolderType.INDIVIDUAL + account_holder.first_name = 'John' + account_holder.last_name = 'Smith' + account_holder.tax_id = '123456' + account_holder.date_of_birth = '1986-01-01' + account_holder.country_of_birth = Country.GB + account_holder.residential_status = ResidentialStatusType.RESIDENT + account_holder.billing_address = _address() + account_holder.phone = phone + account_holder.identification = identification + + destination = PaymentBankAccountDestination() + destination.account_type = AccountType.SAVINGS + destination.account_number = '13654567455' + destination.bank_code = '123-456' + destination.branch_code = '6443' + destination.bban = '3704 0044 0532 0130 00' + destination.iban = 'HU93116000060000000012345676' + destination.swift_bic = '37040044' + destination.country = Country.GB + destination.account_holder = account_holder + return destination + + +def _sender(): + identification = AccountHolderIdentification() + identification.type = AccountHolderIdentificationType.DRIVING_LICENSE + identification.number = '1234' + identification.issuing_country = Country.GT + + sender = PaymentIndividualSender() + sender.reference = '8285282045818' + sender.first_name = 'John' + sender.last_name = 'Jones' + sender.dob = '1985-05-15' + sender.address = _address() + sender.identification = identification + return sender + + +def _full_request(): + source = PayoutRequestEntitySource() + source.id = 'ent_w4jelhppmfiufdnatam37wrfc4' + + billing_descriptor = PayoutBillingDescriptor() + billing_descriptor.reference = 'WITHDRAWAL01' + + remittance = Remitance() + remittance.reference = 'INV-2026-0001' + instruction = PaymentInstruction() + instruction.scheme = InstructionScheme.LOCAL + instruction.remittance = remittance + + segment = PaymentSegment() + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' + + request = PayoutRequest() + request.source = source + request.destination = _destination() + request.amount = 6540 + request.currency = Currency.USD + request.reference = 'Withdrawal' + request.billing_descriptor = billing_descriptor + request.sender = _sender() + request.instruction = instruction + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + request.segment = segment + return request + + +class TestBankPayoutRequestSerialization: + + def test_request_serializes_literal_wire_keys(self): + serialized = _serialize(_full_request()) + + assert set(serialized.keys()) == REQUEST_KEYS + assert all(key == key.lower() for key in serialized.keys()) + + def test_request_matches_fixture(self): + serialized = _serialize(_full_request()) + + assert serialized == REQUEST_FIXTURE + assert isinstance(serialized['amount'], int) + + def test_request_round_trip(self): + request = _full_request() + + decoded = json.loads(json.dumps(request, cls=JsonSerializer)) + + assert decoded['source'] == {'type': request.source.type.value, 'id': request.source.id} + destination = request.destination + decoded_destination = decoded['destination'] + assert decoded_destination['type'] == destination.type.value + assert decoded_destination['account_type'] == destination.account_type.value + assert decoded_destination['account_number'] == destination.account_number + assert decoded_destination['bank_code'] == destination.bank_code + assert decoded_destination['branch_code'] == destination.branch_code + assert decoded_destination['bban'] == destination.bban + assert decoded_destination['iban'] == destination.iban + assert decoded_destination['swift_bic'] == destination.swift_bic + assert decoded_destination['country'] == destination.country.value + holder = destination.account_holder + decoded_holder = decoded_destination['account_holder'] + assert decoded_holder['type'] == holder.type.value + assert decoded_holder['first_name'] == holder.first_name + assert decoded_holder['last_name'] == holder.last_name + assert decoded_holder['tax_id'] == holder.tax_id + assert decoded_holder['date_of_birth'] == holder.date_of_birth + assert decoded_holder['country_of_birth'] == holder.country_of_birth.value + assert decoded_holder['residential_status'] == holder.residential_status.value + assert decoded_holder['billing_address']['address_line1'] == holder.billing_address.address_line1 + assert decoded_holder['phone'] == {'country_code': holder.phone.country_code, 'number': holder.phone.number} + assert decoded_holder['identification'] == { + 'type': holder.identification.type.value, + 'number': holder.identification.number, + 'issuing_country': holder.identification.issuing_country.value} + assert decoded['amount'] == request.amount + assert decoded['currency'] == request.currency.value + assert decoded['reference'] == request.reference + assert decoded['billing_descriptor'] == {'reference': request.billing_descriptor.reference} + sender = request.sender + assert decoded['sender']['type'] == sender.type.value + assert decoded['sender']['reference'] == sender.reference + assert decoded['sender']['first_name'] == sender.first_name + assert decoded['sender']['last_name'] == sender.last_name + assert decoded['sender']['dob'] == sender.dob + assert decoded['sender']['address']['city'] == sender.address.city + assert decoded['sender']['identification'] == { + 'type': sender.identification.type.value, + 'number': sender.identification.number, + 'issuing_country': sender.identification.issuing_country.value} + assert decoded['instruction'] == { + 'scheme': request.instruction.scheme.value, + 'remittance': {'reference': request.instruction.remittance.reference}} + assert decoded['processing_channel_id'] == request.processing_channel_id + assert decoded['segment'] == { + 'brand': request.segment.brand, + 'business_category': request.segment.business_category, + 'market': request.segment.market} diff --git a/tests/payments/card_payout_destination_serialization_test.py b/tests/payments/card_payout_destination_serialization_test.py new file mode 100644 index 00000000..785dcb87 --- /dev/null +++ b/tests/payments/card_payout_destination_serialization_test.py @@ -0,0 +1,240 @@ +import json + +from checkout_sdk.common.common import AccountHolder +from checkout_sdk.common.enums import AccountHolderType +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.payments import CardPayoutItem, ItemType, NetworkTokenType, PaymentDestinationType, \ + PaymentRequestCardDestination, PaymentRequestIdDestination, PaymentRequestNetworkTokenDestination, \ + PaymentRequestTokenDestination, PayoutRequest, ProductSubType +from tests.payments.card_payout_fixtures import ACCOUNT_HOLDER_FIXTURE, rich_account_holder + +# Spec example for 02_CardPayoutRequest_DestinationTypeNetworkToken; vts is the first token_type enum value. +NETWORK_TOKEN_FIXTURE = { + 'type': 'network_token', + 'token': '4543474002249996', + 'expiry_month': 6, + 'expiry_year': 2025, + 'token_type': 'vts', + 'cryptogram': 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=', + 'eci': '05', + 'account_holder': ACCOUNT_HOLDER_FIXTURE +} + +REQUIRED_NETWORK_TOKEN_KEYS = {'type', 'token', 'expiry_month', 'expiry_year', 'token_type', 'account_holder'} + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +def _network_token_destination(with_optionals=True): + destination = PaymentRequestNetworkTokenDestination() + destination.token = '4543474002249996' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.token_type = NetworkTokenType.VTS + if with_optionals: + destination.cryptogram = 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=' + destination.eci = '05' + destination.account_holder = rich_account_holder() + return destination + + +class TestCardPayoutDestinationSerialization: + + def test_network_token_destination_serializes_literal_wire_keys(self): + serialized = _serialize(_network_token_destination()) + + assert set(serialized.keys()) == REQUIRED_NETWORK_TOKEN_KEYS | {'cryptogram', 'eci'} + assert serialized['type'] == 'network_token' + assert all(key == key.lower() for key in serialized.keys()) + + def test_network_token_destination_matches_spec_fixture(self): + serialized = _serialize(_network_token_destination()) + + assert serialized == NETWORK_TOKEN_FIXTURE + assert isinstance(serialized['expiry_month'], int) + assert isinstance(serialized['expiry_year'], int) + + def test_network_token_destination_omits_unset_optional_fields(self): + serialized = _serialize(_network_token_destination(with_optionals=False)) + + assert set(serialized.keys()) == REQUIRED_NETWORK_TOKEN_KEYS + + def test_network_token_type_enum_has_exactly_the_spec_values(self): + assert {member.value for member in NetworkTokenType} == {'vts', 'mdes', 'applepay', 'googlepay'} + for member in NetworkTokenType: + destination = _network_token_destination() + destination.token_type = member + assert _serialize(destination)['token_type'] == member.value + + def test_destination_type_enum_has_network_token(self): + assert PaymentDestinationType.NETWORK_TOKEN.value == 'network_token' + assert PaymentDestinationType('network_token') is PaymentDestinationType.NETWORK_TOKEN + + def test_payout_request_serializes_network_token_destination(self): + request = PayoutRequest() + request.destination = _network_token_destination() + + serialized = _serialize(request) + + assert serialized == {'destination': NETWORK_TOKEN_FIXTURE} + assert set(serialized['destination']['account_holder'].keys()) == { + 'type', 'first_name', 'middle_name', 'last_name', 'billing_address', 'phone', + 'identification', 'email', 'date_of_birth', 'country_of_birth'} + + def test_network_token_destination_round_trip(self): + destination = _network_token_destination() + + decoded = json.loads(json.dumps(destination, cls=JsonSerializer)) + + assert decoded['type'] == destination.type.value + assert decoded['token'] == destination.token + assert decoded['expiry_month'] == destination.expiry_month + assert decoded['expiry_year'] == destination.expiry_year + assert decoded['token_type'] == destination.token_type.value + assert decoded['cryptogram'] == destination.cryptogram + assert decoded['eci'] == destination.eci + holder = destination.account_holder + decoded_holder = decoded['account_holder'] + assert decoded_holder['type'] == holder.type.value + assert decoded_holder['first_name'] == holder.first_name + assert decoded_holder['middle_name'] == holder.middle_name + assert decoded_holder['last_name'] == holder.last_name + assert decoded_holder['email'] == holder.email + assert decoded_holder['date_of_birth'] == holder.date_of_birth + assert decoded_holder['country_of_birth'] == holder.country_of_birth.value + assert decoded_holder['billing_address'] == { + 'address_line1': holder.billing_address.address_line1, + 'address_line2': holder.billing_address.address_line2, + 'city': holder.billing_address.city, + 'state': holder.billing_address.state, + 'zip': holder.billing_address.zip, + 'country': holder.billing_address.country.value} + assert decoded_holder['phone'] == { + 'country_code': holder.phone.country_code, + 'number': holder.phone.number} + assert decoded_holder['identification'] == { + 'type': holder.identification.type.value, + 'number': holder.identification.number, + 'issuing_country': holder.identification.issuing_country.value, + 'date_of_expiry': holder.identification.date_of_expiry} + + def test_card_destination_serializes_every_property(self): + destination = PaymentRequestCardDestination() + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.account_holder = rich_account_holder() + + assert _serialize(destination) == { + 'type': 'card', + 'number': '4242424242424242', + 'expiry_month': 6, + 'expiry_year': 2025, + 'account_holder': ACCOUNT_HOLDER_FIXTURE, + } + + def test_token_destination_serializes_every_property(self): + account_holder = AccountHolder() + account_holder.type = AccountHolderType.INDIVIDUAL + account_holder.first_name = 'John' + account_holder.middle_name = 'Paul' + account_holder.last_name = 'Smith' + destination = PaymentRequestTokenDestination() + destination.token = 'tok_ubfj2q76miwundwlk72vxt2i7q' + destination.account_holder = account_holder + + assert _serialize(destination) == { + 'type': 'token', + 'token': 'tok_ubfj2q76miwundwlk72vxt2i7q', + 'account_holder': {'type': 'individual', 'first_name': 'John', 'middle_name': 'Paul', + 'last_name': 'Smith'}, + } + + def test_raw_string_token_type_serializes_like_the_enum(self): + with_enum = _network_token_destination() + with_string = _network_token_destination() + with_string.token_type = 'vts' + + assert _serialize(with_string) == _serialize(with_enum) + + def test_payout_request_serializes_card_destination_and_round_trips(self): + destination = PaymentRequestCardDestination() + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.account_holder = rich_account_holder() + request = PayoutRequest() + request.destination = destination + + decoded = _serialize(request)['destination'] + + assert decoded == {'type': 'card', 'number': '4242424242424242', 'expiry_month': 6, + 'expiry_year': 2025, 'account_holder': ACCOUNT_HOLDER_FIXTURE} + assert decoded['type'] == destination.type.value + assert decoded['number'] == destination.number + assert decoded['expiry_month'] == destination.expiry_month + assert decoded['expiry_year'] == destination.expiry_year + assert decoded['account_holder']['first_name'] == destination.account_holder.first_name + + def test_payout_request_serializes_token_destination_and_round_trips(self): + account_holder = AccountHolder() + account_holder.type = AccountHolderType.INDIVIDUAL + account_holder.first_name = 'John' + account_holder.middle_name = 'Paul' + account_holder.last_name = 'Smith' + destination = PaymentRequestTokenDestination() + destination.token = 'tok_ubfj2q76miwundwlk72vxt2i7q' + destination.account_holder = account_holder + request = PayoutRequest() + request.destination = destination + + decoded = _serialize(request)['destination'] + + assert decoded == {'type': 'token', 'token': 'tok_ubfj2q76miwundwlk72vxt2i7q', + 'account_holder': {'type': 'individual', 'first_name': 'John', + 'middle_name': 'Paul', 'last_name': 'Smith'}} + assert decoded['token'] == destination.token + assert decoded['account_holder']['type'] == account_holder.type.value + assert decoded['account_holder']['last_name'] == account_holder.last_name + + def test_payout_request_serializes_id_destination_and_round_trips(self): + # The card payout id destination accepts only type and the name properties of its account holder. + account_holder = AccountHolder() + account_holder.type = AccountHolderType.CORPORATE + account_holder.company_name = 'Acme Corporation' + destination = PaymentRequestIdDestination() + destination.id = 'src_wmlfc3zyhqzehihu7giusaaawu' + destination.account_holder = account_holder + request = PayoutRequest() + request.destination = destination + + decoded = _serialize(request)['destination'] + + assert decoded == {'type': 'id', 'id': 'src_wmlfc3zyhqzehihu7giusaaawu', + 'account_holder': {'type': 'corporate', 'company_name': 'Acme Corporation'}} + assert decoded['type'] == destination.type.value + assert decoded['id'] == destination.id + assert decoded['account_holder']['type'] == account_holder.type.value + assert decoded['account_holder']['company_name'] == account_holder.company_name + + def test_payout_request_items_serialize_literal_keys_and_round_trip(self): + # Spec example for CardPayoutItems: type digital, sub_type stablecoin. + digital = CardPayoutItem() + digital.type = ItemType.DIGITAL + digital.sub_type = ProductSubType.STABLECOIN + physical = CardPayoutItem() + physical.type = ItemType.PHYSICAL + request = PayoutRequest() + request.items = [digital, physical] + + decoded = _serialize(request)['items'] + + assert decoded == [{'type': 'digital', 'sub_type': 'stablecoin'}, {'type': 'physical'}] + assert decoded[0]['type'] == digital.type.value + assert decoded[0]['sub_type'] == digital.sub_type.value + assert decoded[1]['type'] == physical.type.value + + def test_card_payout_item_omits_unset_fields_instead_of_sending_null(self): + assert _serialize(CardPayoutItem()) == {} diff --git a/tests/payments/card_payout_fixtures.py b/tests/payments/card_payout_fixtures.py new file mode 100644 index 00000000..8765bff9 --- /dev/null +++ b/tests/payments/card_payout_fixtures.py @@ -0,0 +1,62 @@ +from checkout_sdk.common.common import AccountHolder, AccountHolderIdentification, Address, Phone +from checkout_sdk.common.enums import AccountHolderIdentificationType, AccountHolderType, Country + +# Rich destination account holder shared by the card payout and unreferenced refund destination +# tests: the card and network_token positions of both requests accept these keys. +ACCOUNT_HOLDER_FIXTURE = { + 'type': 'individual', + 'first_name': 'John', + 'middle_name': 'Paul', + 'last_name': 'Smith', + 'billing_address': { + 'address_line1': '123 High St.', + 'address_line2': 'Flat 456', + 'city': 'London', + 'state': 'GB', + 'zip': 'SW1A 1AA', + 'country': 'GB' + }, + 'phone': {'country_code': '+44', 'number': '4155552671'}, + 'identification': { + 'type': 'passport', + 'number': 'E12345678', + 'issuing_country': 'GB', + 'date_of_expiry': '2030-01-01' + }, + 'email': 'john.smith@example.com', + 'date_of_birth': '1985-05-15', + 'country_of_birth': 'GB' +} + + +def rich_account_holder(): + billing_address = Address() + billing_address.address_line1 = '123 High St.' + billing_address.address_line2 = 'Flat 456' + billing_address.city = 'London' + billing_address.state = 'GB' + billing_address.zip = 'SW1A 1AA' + billing_address.country = Country.GB + + phone = Phone() + phone.country_code = '+44' + phone.number = '4155552671' + + identification = AccountHolderIdentification() + identification.type = AccountHolderIdentificationType.PASSPORT + identification.number = 'E12345678' + identification.issuing_country = Country.GB + identification.date_of_expiry = '2030-01-01' + + account_holder = AccountHolder() + account_holder.type = AccountHolderType.INDIVIDUAL + account_holder.first_name = 'John' + account_holder.middle_name = 'Paul' + account_holder.last_name = 'Smith' + account_holder.billing_address = billing_address + account_holder.phone = phone + account_holder.identification = identification + account_holder.email = 'john.smith@example.com' + account_holder.date_of_birth = '1985-05-15' + account_holder.country_of_birth = Country.GB + return account_holder diff --git a/tests/payments/card_payout_request_serialization_test.py b/tests/payments/card_payout_request_serialization_test.py new file mode 100644 index 00000000..38a0df38 --- /dev/null +++ b/tests/payments/card_payout_request_serialization_test.py @@ -0,0 +1,213 @@ +import json + +from checkout_sdk.common.common import AccountHolderIdentification, Address +from checkout_sdk.common.enums import AccountHolderIdentificationType, Country, Currency +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.payments import CardPayoutItem, CardPayoutProcessing, ItemType, \ + PaymentIndividualSender, PaymentInstruction, PaymentRequestCardDestination, PaymentSegment, PayoutRequest, \ + PayoutRequestCurrencyAccountSource, ProductSubType, SourceOfFunds +from tests.payments.card_payout_fixtures import ACCOUNT_HOLDER_FIXTURE, rich_account_holder + +# The spec has no request example for CardPayoutRequest; values below come from the per-property +# examples where the schema declares one (items, previous_payment_id, segment, affiliate_url, +# purchase_country) and otherwise satisfy the property's pattern and length. +REQUEST_FIXTURE = { + 'source': {'type': 'currency_account', 'id': 'ca_y3oqhf46pyzuxjbcn2giaqnb44'}, + 'destination': { + 'type': 'card', + 'number': '4242424242424242', + 'expiry_month': 6, + 'expiry_year': 2025, + 'account_holder': ACCOUNT_HOLDER_FIXTURE + }, + 'amount': 1000, + 'currency': 'GBP', + 'instruction': {'purpose': 'family_support', 'funds_transfer_type': 'FD'}, + 'items': [{'type': 'digital', 'sub_type': 'stablecoin'}], + 'processing_channel_id': 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', + 'sender': { + 'type': 'individual', + 'first_name': 'John', + 'last_name': 'Jones', + 'address': { + 'address_line1': '123 High St.', + 'city': 'London', + 'country': 'GB' + }, + 'reference': 'CUST8285282045', + 'reference_type': 'other', + 'source_of_funds': 'debit', + 'identification': { + 'type': 'passport', + 'number': 'E12345678', + 'issuing_country': 'GB', + 'date_of_expiry': '2030-01-01' + }, + 'date_of_birth': '1985-05-15', + 'country_of_birth': 'GB', + 'nationality': 'GB' + }, + 'reference': 'ORD-5023-4E89', + 'previous_payment_id': 'pay_fun26akvvjjerahhctaq2uzhu4', + 'metadata': {'coupon_code': 'NY2018', 'partner_id': 123989}, + 'segment': {'brand': 'Acme Corporation', 'business_category': 'Inward Payments', 'market': 'UK'}, + 'processing': { + 'affiliate_id': 'AFF12345', + 'affiliate_url': 'www.mycrypto.com', + 'purchase_country': 'GB' + } +} + +REQUEST_KEYS = {'source', 'destination', 'amount', 'currency', 'instruction', 'items', 'processing_channel_id', + 'sender', 'reference', 'previous_payment_id', 'metadata', 'segment', 'processing'} + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +def _sender(): + address = Address() + address.address_line1 = '123 High St.' + address.city = 'London' + address.country = Country.GB + + identification = AccountHolderIdentification() + identification.type = AccountHolderIdentificationType.PASSPORT + identification.number = 'E12345678' + identification.issuing_country = Country.GB + identification.date_of_expiry = '2030-01-01' + + sender = PaymentIndividualSender() + sender.first_name = 'John' + sender.last_name = 'Jones' + sender.address = address + sender.reference = 'CUST8285282045' + sender.reference_type = 'other' + sender.source_of_funds = SourceOfFunds.DEBIT + sender.identification = identification + sender.date_of_birth = '1985-05-15' + sender.country_of_birth = Country.GB + sender.nationality = Country.GB + return sender + + +def _full_request(): + source = PayoutRequestCurrencyAccountSource() + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + + destination = PaymentRequestCardDestination() + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.account_holder = rich_account_holder() + + instruction = PaymentInstruction() + instruction.purpose = 'family_support' + instruction.funds_transfer_type = 'FD' + + item = CardPayoutItem() + item.type = ItemType.DIGITAL + item.sub_type = ProductSubType.STABLECOIN + + segment = PaymentSegment() + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' + + processing = CardPayoutProcessing() + processing.affiliate_id = 'AFF12345' + processing.affiliate_url = 'www.mycrypto.com' + processing.purchase_country = Country.GB + + request = PayoutRequest() + request.source = source + request.destination = destination + request.amount = 1000 + request.currency = Currency.GBP + request.instruction = instruction + request.items = [item] + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + request.sender = _sender() + request.reference = 'ORD-5023-4E89' + request.previous_payment_id = 'pay_fun26akvvjjerahhctaq2uzhu4' + request.metadata = {'coupon_code': 'NY2018', 'partner_id': 123989} + request.segment = segment + request.processing = processing + return request + + +class TestCardPayoutRequestSerialization: + + def test_request_serializes_literal_wire_keys(self): + serialized = _serialize(_full_request()) + + assert set(serialized.keys()) == REQUEST_KEYS + assert all(key == key.lower() for key in serialized.keys()) + + def test_request_matches_fixture(self): + serialized = _serialize(_full_request()) + + assert serialized == REQUEST_FIXTURE + assert isinstance(serialized['amount'], int) + + def test_card_payout_processing_serializes_every_property(self): + processing = CardPayoutProcessing() + processing.affiliate_id = 'AFF12345' + processing.affiliate_url = 'www.mycrypto.com' + processing.processing_speed = 'fast' + processing.purchase_country = Country.GB + + assert _serialize(processing) == {'affiliate_id': 'AFF12345', 'affiliate_url': 'www.mycrypto.com', + 'processing_speed': 'fast', 'purchase_country': 'GB'} + + def test_card_payout_processing_omits_unset_fields_instead_of_sending_null(self): + assert _serialize(CardPayoutProcessing()) == {} + + def test_request_round_trip(self): + request = _full_request() + + decoded = json.loads(json.dumps(request, cls=JsonSerializer)) + + assert decoded['source'] == {'type': request.source.type.value, 'id': request.source.id} + assert decoded['destination']['type'] == request.destination.type.value + assert decoded['destination']['number'] == request.destination.number + assert decoded['destination']['account_holder']['first_name'] == request.destination.account_holder.first_name + assert decoded['amount'] == request.amount + assert decoded['currency'] == request.currency.value + assert decoded['instruction'] == { + 'purpose': request.instruction.purpose, + 'funds_transfer_type': request.instruction.funds_transfer_type} + assert decoded['items'] == [{'type': request.items[0].type.value, 'sub_type': request.items[0].sub_type.value}] + assert decoded['processing_channel_id'] == request.processing_channel_id + sender = request.sender + assert decoded['sender'] == { + 'type': sender.type.value, + 'first_name': sender.first_name, + 'last_name': sender.last_name, + 'address': { + 'address_line1': sender.address.address_line1, + 'city': sender.address.city, + 'country': sender.address.country.value}, + 'reference': sender.reference, + 'reference_type': sender.reference_type, + 'source_of_funds': sender.source_of_funds.value, + 'identification': { + 'type': sender.identification.type.value, + 'number': sender.identification.number, + 'issuing_country': sender.identification.issuing_country.value, + 'date_of_expiry': sender.identification.date_of_expiry}, + 'date_of_birth': sender.date_of_birth, + 'country_of_birth': sender.country_of_birth.value, + 'nationality': sender.nationality.value} + assert decoded['reference'] == request.reference + assert decoded['previous_payment_id'] == request.previous_payment_id + assert decoded['metadata'] == request.metadata + assert decoded['segment'] == { + 'brand': request.segment.brand, + 'business_category': request.segment.business_category, + 'market': request.segment.market} + assert decoded['processing'] == { + 'affiliate_id': request.processing.affiliate_id, + 'affiliate_url': request.processing.affiliate_url, + 'purchase_country': request.processing.purchase_country.value} diff --git a/tests/payments/deprecated_payout_members_serialization_test.py b/tests/payments/deprecated_payout_members_serialization_test.py new file mode 100644 index 00000000..8a47af37 --- /dev/null +++ b/tests/payments/deprecated_payout_members_serialization_test.py @@ -0,0 +1,52 @@ +import json + +from checkout_sdk.common.common import AccountHolder, BankDetails +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.payments import InstructionScheme, PaymentBankAccountDestination, PaymentInstruction, \ + PayoutRequest + + +# Deprecated payout members are retained for backwards compatibility until the next major version, so +# they must keep serializing under their wire keys. +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +class TestDeprecatedPayoutMembersSerialization: + + def test_deprecated_instruction_members_still_serialize(self): + instruction = PaymentInstruction() + instruction.charge_bearer = 'OUR' + instruction.repair = True + instruction.quote_id = 'qte_mbabizu24mvu3mela5njyhpit4' + instruction.scheme = InstructionScheme.SWIFT + request = PayoutRequest() + request.instruction = instruction + + assert _serialize(request)['instruction'] == { + 'charge_bearer': 'OUR', + 'repair': True, + 'quote_id': 'qte_mbabizu24mvu3mela5njyhpit4', + 'scheme': 'swift' + } + + def test_deprecated_bank_account_destination_bank_still_serializes(self): + bank = BankDetails() + bank.name = 'Lloyds TSB' + bank.branch = 'Bournemouth' + destination = PaymentBankAccountDestination() + destination.bank = bank + request = PayoutRequest() + request.destination = destination + + decoded = _serialize(request)['destination'] + + assert decoded['bank'] == {'name': 'Lloyds TSB', 'branch': 'Bournemouth'} + assert decoded['type'] == 'bank_account' + + def test_deprecated_account_holder_full_name_and_gender_still_serialize(self): + account_holder = AccountHolder() + account_holder.full_name = 'John Smith' + account_holder.gender = 'male' + + assert _serialize(account_holder) == {'full_name': 'John Smith', 'gender': 'male'} diff --git a/tests/payments/payment_request_fields_serialization_test.py b/tests/payments/payment_request_fields_serialization_test.py index f1628f27..b5366937 100644 --- a/tests/payments/payment_request_fields_serialization_test.py +++ b/tests/payments/payment_request_fields_serialization_test.py @@ -1,7 +1,8 @@ import json from checkout_sdk.json_serializer import JsonSerializer -from checkout_sdk.payments.payments import AuthorizationType, PaymentPlan +from checkout_sdk.payments.payments import AuthorizationType, NetworkTokenType, PaymentPlan, \ + PaymentRequestNetworkTokenSource from checkout_sdk.payments.hosted.hosted_payments import HostedPaymentsSessionRequest from checkout_sdk.payments.links.payments_links import PaymentLinkRequest from checkout_sdk.payments.sessions.sessions import PaymentSessionsRequest @@ -49,3 +50,10 @@ def test_payment_sessions_request_serializes_authorization_type_and_payment_plan 'authorization_type': 'Final', 'payment_plan': {'days_between_payments': 30, 'total_number_of_payments': 12}, } + + def test_network_token_source_serializes_every_token_type_literal(self): + for token_type in NetworkTokenType: + source = PaymentRequestNetworkTokenSource() + source.token_type = token_type + + assert _serialize(source) == {'type': 'network_token', 'token_type': token_type.value} diff --git a/tests/payments/payments_client_test.py b/tests/payments/payments_client_test.py index 331e5afc..6e527086 100644 --- a/tests/payments/payments_client_test.py +++ b/tests/payments/payments_client_test.py @@ -5,7 +5,7 @@ from checkout_sdk.payments.payments_client import PaymentsClient from checkout_sdk.payments.payments import PaymentRequest, PayoutRequest, CaptureRequest, AuthorizationRequest, \ RequestProviderTokenSource, RefundRequest, VoidRequest, CancelScheduledRetryRequest, ReversePaymentRequest, \ - PaymentsSearchRequest + PaymentsSearchRequest, UnreferencedRefundRequest @pytest.fixture(scope='class') @@ -43,6 +43,21 @@ def test_request_payout_idempotency_key(self, mocker, client: PaymentsClient): assert client.request_payout(body, 'idempotency_key') == 'response' assert_api_call(mock, 'payments', body) + def test_request_unreferenced_refund(self, mocker, client: PaymentsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = UnreferencedRefundRequest() + + assert client.request_unreferenced_refund(body) == 'response' + assert_api_call(mock, 'payments', body) + + def test_request_unreferenced_refund_idempotency_key(self, mocker, client: PaymentsClient): + mock = mocker.patch('checkout_sdk.api_client.ApiClient.post', return_value='response') + body = UnreferencedRefundRequest() + + assert client.request_unreferenced_refund(body, 'idempotency_key') == 'response' + assert_api_call(mock, 'payments', body) + assert mock.call_args.args[3] == 'idempotency_key' + def test_get_payment_details(self, mocker, client: PaymentsClient): mock = mocker.patch('checkout_sdk.api_client.ApiClient.get', return_value='response') diff --git a/tests/payments/unreferenced_refund_serialization_test.py b/tests/payments/unreferenced_refund_serialization_test.py new file mode 100644 index 00000000..a78a4f0c --- /dev/null +++ b/tests/payments/unreferenced_refund_serialization_test.py @@ -0,0 +1,249 @@ +import json + +from checkout_sdk.common.enums import Currency +from checkout_sdk.json_serializer import JsonSerializer +from checkout_sdk.payments.payments import CardPayoutProcessing, NetworkTokenType, PaymentInstruction, \ + PaymentRequestCardDestination, PaymentRequestIdDestination, PaymentRequestNetworkTokenDestination, \ + PaymentRequestTokenDestination, PaymentSegment, PayoutRequestCurrencyAccountSource, UnreferencedRefundRequest +from tests.payments.card_payout_fixtures import ACCOUNT_HOLDER_FIXTURE, rich_account_holder + +# The spec has no request example for UnreferencedRefundRequest; values below come from the +# per-property examples of the schema where it declares one (amount 120, segment, metadata, +# previous_payment_id) and otherwise from its patterns (source, processing channel, ids). +CARD_DESTINATION_FIXTURE = { + 'type': 'card', + 'number': '4242424242424242', + 'expiry_month': 6, + 'expiry_year': 2025, + 'account_holder': ACCOUNT_HOLDER_FIXTURE +} + +NETWORK_TOKEN_DESTINATION_FIXTURE = { + 'type': 'network_token', + 'token': '4543474002249996', + 'expiry_month': 6, + 'expiry_year': 2025, + 'token_type': 'vts', + 'cryptogram': 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=', + 'eci': '05', + 'account_holder': ACCOUNT_HOLDER_FIXTURE +} + +ID_DESTINATION_FIXTURE = { + 'type': 'id', + 'id': 'src_wmlfc3zyhqzehihu7giusaaawu', + 'account_holder': ACCOUNT_HOLDER_FIXTURE +} + +TOKEN_DESTINATION_FIXTURE = { + 'type': 'token', + 'token': 'tok_ubfj2q76miwundwlk72vxt2i7q', + 'account_holder': ACCOUNT_HOLDER_FIXTURE +} + +REQUEST_FIXTURE = { + 'source': {'type': 'currency_account', 'id': 'ca_y3oqhf46pyzuxjbcn2giaqnb44'}, + 'destination': CARD_DESTINATION_FIXTURE, + 'instruction': {'funds_transfer_type': 'MI', 'purpose': 'family_support'}, + 'amount': 120, + 'currency': 'GBP', + 'payment_type': 'UnreferencedRefund', + 'processing_channel_id': 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', + 'reference': 'ORD-5023-4E89', + 'metadata': {'coupon_code': 'NY2018', 'partner_id': 123989}, + 'previous_payment_id': 'pay_fun26akvvjjerahhctaq2uzhu4', + 'segment': {'brand': 'Acme Corporation', 'business_category': 'Inward Payments', 'market': 'UK'}, + 'processing': {'processing_speed': 'fast'} +} + +REQUEST_KEYS = {'source', 'destination', 'instruction', 'amount', 'currency', 'payment_type', + 'processing_channel_id', 'reference', 'metadata', 'previous_payment_id', 'segment', + 'processing'} + +REQUIRED_REQUEST_KEYS = {'source', 'destination', 'amount', 'currency', 'payment_type', 'processing_channel_id'} + + +def _serialize(obj): + return json.loads(json.dumps(obj, cls=JsonSerializer)) + + +def _card_destination(): + destination = PaymentRequestCardDestination() + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.account_holder = rich_account_holder() + return destination + + +def _network_token_destination(): + destination = PaymentRequestNetworkTokenDestination() + destination.token = '4543474002249996' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.token_type = NetworkTokenType.VTS + destination.cryptogram = 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=' + destination.eci = '05' + destination.account_holder = rich_account_holder() + return destination + + +def _id_destination(with_account_holder=True): + destination = PaymentRequestIdDestination() + destination.id = 'src_wmlfc3zyhqzehihu7giusaaawu' + if with_account_holder: + destination.account_holder = rich_account_holder() + return destination + + +def _token_destination(with_account_holder=True): + destination = PaymentRequestTokenDestination() + destination.token = 'tok_ubfj2q76miwundwlk72vxt2i7q' + if with_account_holder: + destination.account_holder = rich_account_holder() + return destination + + +def _full_request(destination=None): + source = PayoutRequestCurrencyAccountSource() + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + + instruction = PaymentInstruction() + instruction.funds_transfer_type = 'MI' + instruction.purpose = 'family_support' + + segment = PaymentSegment() + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' + + processing = CardPayoutProcessing() + processing.processing_speed = 'fast' + + request = UnreferencedRefundRequest() + request.source = source + request.destination = destination if destination is not None else _card_destination() + request.instruction = instruction + request.amount = 120 + request.currency = Currency.GBP + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + request.reference = 'ORD-5023-4E89' + request.metadata = {'coupon_code': 'NY2018', 'partner_id': 123989} + request.previous_payment_id = 'pay_fun26akvvjjerahhctaq2uzhu4' + request.segment = segment + request.processing = processing + return request + + +class TestUnreferencedRefundSerialization: + + def test_constructor_sets_payment_type_literal(self): + request = UnreferencedRefundRequest() + + assert request.payment_type == 'UnreferencedRefund' + assert _serialize(request) == {'payment_type': 'UnreferencedRefund'} + + def test_request_serializes_literal_wire_keys(self): + serialized = _serialize(_full_request()) + + assert set(serialized.keys()) == REQUEST_KEYS + assert all(key == key.lower() for key in serialized.keys()) + + def test_request_matches_fixture(self): + serialized = _serialize(_full_request()) + + assert serialized == REQUEST_FIXTURE + assert isinstance(serialized['amount'], int) + + def test_request_with_only_required_properties(self): + request = UnreferencedRefundRequest() + source = PayoutRequestCurrencyAccountSource() + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + request.source = source + request.destination = _token_destination(with_account_holder=False) + request.amount = 120 + request.currency = Currency.GBP + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + + serialized = _serialize(request) + + assert set(serialized.keys()) == REQUIRED_REQUEST_KEYS + assert serialized['source'] == REQUEST_FIXTURE['source'] + assert serialized['destination'] == {'type': 'token', 'token': 'tok_ubfj2q76miwundwlk72vxt2i7q'} + + def test_network_token_destination_nested_under_destination(self): + serialized = _serialize(_full_request(_network_token_destination())) + + assert serialized['destination'] == NETWORK_TOKEN_DESTINATION_FIXTURE + + def test_id_destination_nested_under_destination(self): + serialized = _serialize(_full_request(_id_destination())) + + assert serialized['destination'] == ID_DESTINATION_FIXTURE + + def test_token_destination_nested_under_destination(self): + serialized = _serialize(_full_request(_token_destination())) + + assert serialized['destination'] == TOKEN_DESTINATION_FIXTURE + + def test_id_destination_omits_account_holder_when_unset(self): + serialized = _serialize(_full_request(_id_destination(with_account_holder=False))) + + assert serialized['destination'] == {'type': 'id', 'id': 'src_wmlfc3zyhqzehihu7giusaaawu'} + + def test_token_destination_omits_account_holder_when_unset(self): + serialized = _serialize(_full_request(_token_destination(with_account_holder=False))) + + assert serialized['destination'] == {'type': 'token', 'token': 'tok_ubfj2q76miwundwlk72vxt2i7q'} + + def test_request_round_trip(self): + request = _full_request() + + decoded = json.loads(json.dumps(request, cls=JsonSerializer)) + + assert decoded['source'] == {'type': request.source.type.value, 'id': request.source.id} + assert decoded['instruction'] == { + 'funds_transfer_type': request.instruction.funds_transfer_type, + 'purpose': request.instruction.purpose} + assert decoded['amount'] == request.amount + assert decoded['currency'] == request.currency.value + assert decoded['payment_type'] == request.payment_type + assert decoded['processing_channel_id'] == request.processing_channel_id + assert decoded['reference'] == request.reference + assert decoded['metadata'] == request.metadata + assert decoded['previous_payment_id'] == request.previous_payment_id + assert decoded['segment'] == { + 'brand': request.segment.brand, + 'business_category': request.segment.business_category, + 'market': request.segment.market} + assert decoded['processing'] == {'processing_speed': request.processing.processing_speed} + destination = request.destination + decoded_destination = decoded['destination'] + assert decoded_destination['type'] == destination.type.value + assert decoded_destination['number'] == destination.number + assert decoded_destination['expiry_month'] == destination.expiry_month + assert decoded_destination['expiry_year'] == destination.expiry_year + holder = destination.account_holder + decoded_holder = decoded_destination['account_holder'] + assert decoded_holder['type'] == holder.type.value + assert decoded_holder['first_name'] == holder.first_name + assert decoded_holder['middle_name'] == holder.middle_name + assert decoded_holder['last_name'] == holder.last_name + assert decoded_holder['email'] == holder.email + assert decoded_holder['date_of_birth'] == holder.date_of_birth + assert decoded_holder['country_of_birth'] == holder.country_of_birth.value + assert decoded_holder['billing_address'] == { + 'address_line1': holder.billing_address.address_line1, + 'address_line2': holder.billing_address.address_line2, + 'city': holder.billing_address.city, + 'state': holder.billing_address.state, + 'zip': holder.billing_address.zip, + 'country': holder.billing_address.country.value} + assert decoded_holder['phone'] == { + 'country_code': holder.phone.country_code, + 'number': holder.phone.number} + assert decoded_holder['identification'] == { + 'type': holder.identification.type.value, + 'number': holder.identification.number, + 'issuing_country': holder.identification.issuing_country.value, + 'date_of_expiry': holder.identification.date_of_expiry}