From 7253cccc7d7f65025885f6ef7a10644b5e80c008 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 17:39:29 +0100 Subject: [PATCH 1/9] network_token as a card payout + test --- .../payments/destination/card_destination.rb | 20 ++ .../payments/destination/destination_type.rb | 6 + .../payments/destination/id_destination.rb | 11 ++ .../destination/network_token_destination.rb | 61 ++++++ .../payments/destination/token_destination.rb | 11 ++ lib/checkout_sdk/payments/payments.rb | 1 + ...d_payout_destination_serialization_spec.rb | 173 ++++++++++++++++++ 7 files changed, 283 insertions(+) create mode 100644 lib/checkout_sdk/payments/destination/network_token_destination.rb create mode 100644 spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb diff --git a/lib/checkout_sdk/payments/destination/card_destination.rb b/lib/checkout_sdk/payments/destination/card_destination.rb index 8a04025..06d587f 100644 --- a/lib/checkout_sdk/payments/destination/card_destination.rb +++ b/lib/checkout_sdk/payments/destination/card_destination.rb @@ -2,13 +2,33 @@ module CheckoutSdk module Payments + # A card payout destination. + # # @!attribute number + # The card number. + # You can encrypt this field by using the Cko-Encryption-Key-Id and Cko-Encrypted-Properties headers. + # [Required] + # max 19 characters # @return [String] # @!attribute expiry_month + # The card's expiration month. + # [Required] + # 1 to 2 digits # @return [Integer] # @!attribute expiry_year + # The card's expiration year. + # [Required] + # 4 digits # @return [Integer] # @!attribute account_holder + # The payout destination account holder. + # [Required] + # The type discriminates the accepted keys: + # - individual: type, first_name, middle_name, last_name, billing_address, phone, + # identification, email, date_of_birth, country_of_birth + # - corporate, government: type, company_name, billing_address, phone, identification, email + # Other AccountHolder properties (full_name, gender, tax_id, residential_status, + # account_name_inquiry) are not part of this position. # @return [CheckoutSdk::Common::AccountHolder] class CardDestination < Destination attr_accessor :number, diff --git a/lib/checkout_sdk/payments/destination/destination_type.rb b/lib/checkout_sdk/payments/destination/destination_type.rb index 2793c1d..b30cf44 100644 --- a/lib/checkout_sdk/payments/destination/destination_type.rb +++ b/lib/checkout_sdk/payments/destination/destination_type.rb @@ -2,11 +2,17 @@ module CheckoutSdk module Payments + # The payout destination type. + # + # Two destination discriminators share these values: + # - card payout destination: card, network_token, id, token + # - bank payout destination: id, bank_account module DestinationType CARD = 'card' ID = 'id' TOKEN = 'token' BANK_ACCOUNT = 'bank_account' + NETWORK_TOKEN = 'network_token' end end end diff --git a/lib/checkout_sdk/payments/destination/id_destination.rb b/lib/checkout_sdk/payments/destination/id_destination.rb index 7733728..f6607ea 100644 --- a/lib/checkout_sdk/payments/destination/id_destination.rb +++ b/lib/checkout_sdk/payments/destination/id_destination.rb @@ -2,9 +2,20 @@ module CheckoutSdk module Payments + # A payment instrument payout destination. Used by both card payouts and bank payouts. + # # @!attribute id + # The payment source ID. This will be an ID with the prefix `src_`. + # [Required] + # ^(src)_(\w{26})$ + # 30 characters (card payout) # @return [String] # @!attribute account_holder + # The payout destination account holder. + # [Required] for card payouts. Not declared by the bank payout id destination. + # The type discriminates the accepted keys: + # - individual: type, first_name, middle_name, last_name + # - corporate, government: type, company_name # @return [CheckoutSdk::Common::AccountHolder] class IdDestination < Destination attr_accessor :id, diff --git a/lib/checkout_sdk/payments/destination/network_token_destination.rb b/lib/checkout_sdk/payments/destination/network_token_destination.rb new file mode 100644 index 0000000..cf19e7d --- /dev/null +++ b/lib/checkout_sdk/payments/destination/network_token_destination.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # A network token payout destination, used as the destination of a card payout. + # + # @!attribute token + # The network token's Primary Account Number (PAN). + # [Required] + # @return [String] + # @!attribute expiry_month + # The network token's expiration month. + # [Required] + # min 1 + # 1 to 2 digits + # @return [Integer] + # @!attribute expiry_year + # The network token's expiration year. + # [Required] + # 4 digits + # @return [Integer] + # @!attribute token_type + # The network token type. + # [Required] + # @return [String] {CheckoutSdk::Payments::NetworkTokenType} + # @!attribute cryptogram + # 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 + # @return [String] + # @!attribute eci + # The network token's Electronic Commerce Indicator (ECI) security level. + # [Optional] + # max 2 characters + # @return [String] + # @!attribute account_holder + # The payout destination account holder. + # [Required] + # The type discriminates the accepted keys: + # - individual: type, first_name, middle_name, last_name, billing_address, phone, + # identification, email, date_of_birth, country_of_birth + # - corporate, government: type, company_name, billing_address, phone, identification, email + # Other AccountHolder properties (full_name, gender, tax_id, residential_status, + # account_name_inquiry) are not part of this position. + # @return [CheckoutSdk::Common::AccountHolder] + class NetworkTokenDestination < Destination + attr_accessor :token, + :expiry_month, + :expiry_year, + :token_type, + :cryptogram, + :eci, + :account_holder + + def initialize + super(CheckoutSdk::Payments::DestinationType::NETWORK_TOKEN) + end + end + end +end diff --git a/lib/checkout_sdk/payments/destination/token_destination.rb b/lib/checkout_sdk/payments/destination/token_destination.rb index 8baf388..e73b14f 100644 --- a/lib/checkout_sdk/payments/destination/token_destination.rb +++ b/lib/checkout_sdk/payments/destination/token_destination.rb @@ -2,9 +2,20 @@ module CheckoutSdk module Payments + # A token payout destination. + # # @!attribute token + # The Checkout.com card token ID. This will be an ID with the prefix `tok_`. + # [Required] + # ^(tok)_(\w{26})$ + # 30 characters # @return [String] # @!attribute account_holder + # The payout destination account holder. + # [Required] + # The type discriminates the accepted keys: + # - individual: type, first_name, middle_name, last_name + # - corporate, government: type, company_name # @return [CheckoutSdk::Common::AccountHolder] class TokenDestination < Destination attr_accessor :token, diff --git a/lib/checkout_sdk/payments/payments.rb b/lib/checkout_sdk/payments/payments.rb index 6b7b907..382afb6 100644 --- a/lib/checkout_sdk/payments/payments.rb +++ b/lib/checkout_sdk/payments/payments.rb @@ -137,6 +137,7 @@ # Destination require 'checkout_sdk/payments/destination/destination' +require 'checkout_sdk/payments/destination/network_token_destination' require 'checkout_sdk/payments/destination/token_destination' require 'checkout_sdk/payments/destination/bank_account_destination' require 'checkout_sdk/payments/destination/card_destination' diff --git a/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb b/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb new file mode 100644 index 0000000..58625aa --- /dev/null +++ b/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb @@ -0,0 +1,173 @@ +# frozen_string_literal: true + +require 'json' +require 'open3' +require 'rbconfig' + +RSpec.describe CheckoutSdk::Payments::NetworkTokenDestination do + # Built from the swagger examples of the network token card payout destination. + let(:swagger_example) do + { + 'type' => 'network_token', + 'token' => '4543474002249996', + 'expiry_month' => 6, + 'expiry_year' => 2025, + 'token_type' => 'vts', + 'cryptogram' => 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=', + 'eci' => '05', + 'account_holder' => { + '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' + } + } + end + + let(:account_holder) do + address = CheckoutSdk::Common::Address.new + address.address_line1 = '123 High St.' + address.address_line2 = 'Flat 456' + address.city = 'London' + address.state = 'GB' + address.zip = 'SW1A 1AA' + address.country = CheckoutSdk::Common::Country::GB + + phone = CheckoutSdk::Common::Phone.new + phone.country_code = '+44' + phone.number = '4155552671' + + identification = CheckoutSdk::Common::AccountHolderIdentification.new + identification.type = CheckoutSdk::Common::AccountHolderIdentificationType::PASSPORT + identification.number = 'E12345678' + identification.issuing_country = CheckoutSdk::Common::Country::GB + identification.date_of_expiry = '2030-01-01' + + holder = CheckoutSdk::Common::AccountHolder.new + holder.type = CheckoutSdk::Common::AccountHolderType::INDIVIDUAL + holder.first_name = 'John' + holder.middle_name = 'Paul' + holder.last_name = 'Smith' + holder.billing_address = address + holder.phone = phone + holder.identification = identification + holder.email = 'john.smith@example.com' + holder.date_of_birth = '1985-05-15' + holder.country_of_birth = CheckoutSdk::Common::Country::GB + holder + end + + let(:minimal_destination) do + destination = described_class.new + destination.token = '4543474002249996' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.token_type = CheckoutSdk::Payments::NetworkTokenType::VTS + destination.account_holder = account_holder + destination + end + + let(:full_destination) do + destination = minimal_destination + destination.cryptogram = 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=' + destination.eci = '05' + destination + end + + def wire(object) + JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(object))) + end + + it 'serializes exactly the eight snake case keys the schema declares' do + json = wire(full_destination) + + expect(json.keys.sort).to eq(%w[account_holder cryptogram eci expiry_month expiry_year token token_type type]) + expect(json['type']).to eq('network_token') + expect(json.keys).to all(match(/\A[a-z_]+\z/)) + end + + it 'serializes to the swagger example, with integers kept as integers' do + json = wire(full_destination) + + expect(json).to eq(swagger_example) + expect(json['expiry_month']).to be_an(Integer) + expect(json['expiry_year']).to be_an(Integer) + end + + it 'omits cryptogram and eci when they are not set' do + json = wire(minimal_destination) + + expect(json.keys.sort).to eq(%w[account_holder expiry_month expiry_year token token_type type]) + end + + it 'serializes every NetworkTokenType value to its literal' do + values = CheckoutSdk::Payments::NetworkTokenType.constants.map do |c| + CheckoutSdk::Payments::NetworkTokenType.const_get(c) + end + expect(values).to contain_exactly('vts', 'mdes', 'applepay', 'googlepay') + + values.each do |value| + minimal_destination.token_type = value + expect(wire(minimal_destination)['token_type']).to eq(value) + end + end + + it 'sets the type from DestinationType::NETWORK_TOKEN' do + expect(CheckoutSdk::Payments::DestinationType::NETWORK_TOKEN).to eq('network_token') + expect(described_class.new.type).to eq(CheckoutSdk::Payments::DestinationType::NETWORK_TOKEN) + end + + it 'serializes under destination in a payout request with the rich account holder keys' do + request = CheckoutSdk::Payments::PayoutRequest.new + request.destination = full_destination + + destination = wire(request)['destination'] + + expect(destination['type']).to eq('network_token') + expect(destination['account_holder'].keys.sort) + .to eq(%w[billing_address country_of_birth date_of_birth email first_name identification last_name + middle_name phone type]) + end + + it 'round trips every property through JSON' do + json = wire(full_destination) + + expect(json['type']).to eq(full_destination.type) + expect(json['token']).to eq(full_destination.token) + expect(json['expiry_month']).to eq(full_destination.expiry_month) + expect(json['expiry_year']).to eq(full_destination.expiry_year) + expect(json['token_type']).to eq(full_destination.token_type) + expect(json['cryptogram']).to eq(full_destination.cryptogram) + expect(json['eci']).to eq(full_destination.eci) + expect(json['account_holder']).to eq(CheckoutSdk::JsonSerializer.to_custom_hash(account_holder) + .then { |h| JSON.parse(JSON.generate(h)) }) + end + + it 'loads through require checkout_sdk alone' do + lib = File.expand_path('../../../lib', __dir__) + script = "require 'checkout_sdk'; print CheckoutSdk::Payments::NetworkTokenDestination.new.type" + + output, status = Open3.capture2(RbConfig.ruby, '-I', lib, '-e', script) + + expect(status).to be_success + expect(output).to eq('network_token') + end +end From e6d406c3c0b02d57fce22b518b36fb792aa6f572 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 18:23:06 +0100 Subject: [PATCH 2/9] Doc review + tests --- lib/checkout_sdk/payments/card_payout_item.rb | 24 ++ .../payments/card_payout_processing.rb | 38 +++ .../destination/bank_account_destination.rb | 28 ++ .../payments/destination/card_destination.rb | 4 +- .../payments/destination/destination.rb | 5 + .../payments/destination/destination_type.rb | 3 +- .../payments/destination/id_destination.rb | 15 +- .../destination/network_token_destination.rb | 5 +- .../payments/destination/token_destination.rb | 12 +- lib/checkout_sdk/payments/payments.rb | 4 + lib/checkout_sdk/payments/payments_client.rb | 10 + lib/checkout_sdk/payments/payout_request.rb | 59 +++- .../payments/unreferenced_refund_request.rb | 89 ++++++ ...d_payout_destination_serialization_spec.rb | 19 +- .../payments/payout_request_fields_spec.rb | 70 +++++ .../unreferenced_refund_request_spec.rb | 296 ++++++++++++++++++ 16 files changed, 664 insertions(+), 17 deletions(-) create mode 100644 lib/checkout_sdk/payments/card_payout_item.rb create mode 100644 lib/checkout_sdk/payments/card_payout_processing.rb create mode 100644 lib/checkout_sdk/payments/unreferenced_refund_request.rb create mode 100644 spec/checkout_sdk/payments/payout_request_fields_spec.rb create mode 100644 spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb diff --git a/lib/checkout_sdk/payments/card_payout_item.rb b/lib/checkout_sdk/payments/card_payout_item.rb new file mode 100644 index 0000000..2a833ee --- /dev/null +++ b/lib/checkout_sdk/payments/card_payout_item.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # A line item of a card payout. + # + # @!attribute type + # The item type. For example, physical or digital. + # This field is used to provide item level metadata where applicable. + # [Optional] + # Enum: "digital" "physical" + # @return [String] {ProductItemType} + # @!attribute sub_type + # The digital item type. + # If type is set to digital, this field is required. + # [Optional] + # Enum: "blockchain" "cbdc" "cryptocurrency" "nft" "stablecoin" + # @return [String] {ProductSubType} + class CardPayoutItem + attr_accessor :type, + :sub_type + end + end +end diff --git a/lib/checkout_sdk/payments/card_payout_processing.rb b/lib/checkout_sdk/payments/card_payout_processing.rb new file mode 100644 index 0000000..6f4fa7f --- /dev/null +++ b/lib/checkout_sdk/payments/card_payout_processing.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # Information related to the processing of a card payout. + # + # @!attribute affiliate_id + # 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] + # max 15 characters + # @return [String] + # @!attribute affiliate_url + # The affiliate URL. + # If you're a Visa-registered ramp provider operating with affiliates, this field is required. + # [Optional] + # @return [String] + # @!attribute processing_speed + # The speed at which the payout is processed. + # Only applicable for unreferenced refunds. + # [Optional] + # Enum: "fast" + # @return [String] + # @!attribute purchase_country + # 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 + # @return [String] {CheckoutSdk::Common::Country} + class CardPayoutProcessing + attr_accessor :affiliate_id, + :affiliate_url, + :processing_speed, + :purchase_country + end + end +end diff --git a/lib/checkout_sdk/payments/destination/bank_account_destination.rb b/lib/checkout_sdk/payments/destination/bank_account_destination.rb index 7f24241..795872f 100644 --- a/lib/checkout_sdk/payments/destination/bank_account_destination.rb +++ b/lib/checkout_sdk/payments/destination/bank_account_destination.rb @@ -2,25 +2,53 @@ module CheckoutSdk module Payments + # A bank account payout destination (bank payout, type bank_account). + # # @!attribute account_type + # The type of the bank account. Note that some beneficiary banks do not accept payouts to + # savings accounts. + # [Required] # @return [String] {CheckoutSdk::Common::AccountType} # @!attribute account_number + # Number (which can contain letters) that identifies the local account. + # [Optional] # @return [String] # @!attribute bank_code + # Code that identifies the bank. Also referred as Routing Number in the United States. + # [Optional] # @return [String] # @!attribute branch_code + # Code that identifies the bank branch. + # [Optional] # @return [String] # @!attribute iban + # Internationally agreed standard for identifying bank accounts. Lengths vary per destination_country. + # [Optional] # @return [String] # @!attribute bban + # The combination of bank code and/or branch code and account number. + # [Optional] # @return [String] # @!attribute swift_bic + # 8 or 11 character code which identifies the bank or bank branch. + # [Optional] # @return [String] # @!attribute country + # The two-letter ISO country code of where the account is based. Note that not all countries + # are available. + # [Required] + # 2 characters # @return [String] {CheckoutSdk::Common::Country} # @!attribute account_holder + # The account holder details. + # [Optional] # @return [CheckoutSdk::Common::AccountHolder] # @!attribute bank + # The bank details. + # [Optional] + # @deprecated Not in the current specification of the bank account payout destination, so the + # API does not define it. Retained for backwards compatibility and will be removed in a + # future version. # @return [CheckoutSdk::Common::BankDetails] class BankAccountDestination < Destination attr_accessor :account_type, diff --git a/lib/checkout_sdk/payments/destination/card_destination.rb b/lib/checkout_sdk/payments/destination/card_destination.rb index 06d587f..3bad618 100644 --- a/lib/checkout_sdk/payments/destination/card_destination.rb +++ b/lib/checkout_sdk/payments/destination/card_destination.rb @@ -2,7 +2,7 @@ module CheckoutSdk module Payments - # A card payout destination. + # A card payout destination. Also the card destination of an unreferenced refund. # # @!attribute number # The card number. @@ -21,7 +21,7 @@ module Payments # 4 digits # @return [Integer] # @!attribute account_holder - # The payout destination account holder. + # The payout or unreferenced refund destination account holder. # [Required] # The type discriminates the accepted keys: # - individual: type, first_name, middle_name, last_name, billing_address, phone, diff --git a/lib/checkout_sdk/payments/destination/destination.rb b/lib/checkout_sdk/payments/destination/destination.rb index e4188a0..a2243fc 100644 --- a/lib/checkout_sdk/payments/destination/destination.rb +++ b/lib/checkout_sdk/payments/destination/destination.rb @@ -2,7 +2,12 @@ module CheckoutSdk module Payments + # 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. + # # @!attribute type + # The payout or unreferenced refund destination type. Set by each subclass. + # [Required] # @return [String] {DestinationType} class Destination attr_reader :type diff --git a/lib/checkout_sdk/payments/destination/destination_type.rb b/lib/checkout_sdk/payments/destination/destination_type.rb index b30cf44..a278d73 100644 --- a/lib/checkout_sdk/payments/destination/destination_type.rb +++ b/lib/checkout_sdk/payments/destination/destination_type.rb @@ -4,8 +4,9 @@ module CheckoutSdk module Payments # The payout destination type. # - # Two destination discriminators share these values: + # Three destination discriminators share these values: # - card payout destination: card, network_token, id, token + # - unreferenced refund destination: card, network_token, id, token # - bank payout destination: id, bank_account module DestinationType CARD = 'card' diff --git a/lib/checkout_sdk/payments/destination/id_destination.rb b/lib/checkout_sdk/payments/destination/id_destination.rb index f6607ea..0569cfa 100644 --- a/lib/checkout_sdk/payments/destination/id_destination.rb +++ b/lib/checkout_sdk/payments/destination/id_destination.rb @@ -2,20 +2,25 @@ module CheckoutSdk module Payments - # A payment instrument payout destination. Used by both card payouts and bank payouts. + # A payment instrument payout destination. Used by card payouts, bank payouts and unreferenced refunds. # # @!attribute id # The payment source ID. This will be an ID with the prefix `src_`. # [Required] # ^(src)_(\w{26})$ - # 30 characters (card payout) + # 30 characters (card payout and unreferenced refund) # @return [String] # @!attribute account_holder - # The payout destination account holder. - # [Required] for card payouts. Not declared by the bank payout id destination. - # The type discriminates the accepted keys: + # The payout or unreferenced refund destination account holder. + # [Required] for card payouts, [Optional] for unreferenced refunds. + # Not declared by the bank payout id destination. + # The type discriminates the accepted keys. For card payouts: # - individual: type, first_name, middle_name, last_name # - corporate, government: type, company_name + # For unreferenced refunds, where no name is required: + # - individual: type, first_name, middle_name, last_name, billing_address, phone, + # identification, email, date_of_birth, country_of_birth + # - corporate, government: type, company_name, billing_address, phone, identification, email # @return [CheckoutSdk::Common::AccountHolder] class IdDestination < Destination attr_accessor :id, diff --git a/lib/checkout_sdk/payments/destination/network_token_destination.rb b/lib/checkout_sdk/payments/destination/network_token_destination.rb index cf19e7d..da4f394 100644 --- a/lib/checkout_sdk/payments/destination/network_token_destination.rb +++ b/lib/checkout_sdk/payments/destination/network_token_destination.rb @@ -2,7 +2,8 @@ module CheckoutSdk module Payments - # A network token payout destination, used as the destination of a card payout. + # A network token payout destination, used as the destination of a card payout or of an + # unreferenced refund. # # @!attribute token # The network token's Primary Account Number (PAN). @@ -35,7 +36,7 @@ module Payments # max 2 characters # @return [String] # @!attribute account_holder - # The payout destination account holder. + # The payout or unreferenced refund destination account holder. # [Required] # The type discriminates the accepted keys: # - individual: type, first_name, middle_name, last_name, billing_address, phone, diff --git a/lib/checkout_sdk/payments/destination/token_destination.rb b/lib/checkout_sdk/payments/destination/token_destination.rb index e73b14f..6ac0937 100644 --- a/lib/checkout_sdk/payments/destination/token_destination.rb +++ b/lib/checkout_sdk/payments/destination/token_destination.rb @@ -2,7 +2,7 @@ module CheckoutSdk module Payments - # A token payout destination. + # A token payout destination. Also the token destination of an unreferenced refund. # # @!attribute token # The Checkout.com card token ID. This will be an ID with the prefix `tok_`. @@ -11,11 +11,15 @@ module Payments # 30 characters # @return [String] # @!attribute account_holder - # The payout destination account holder. - # [Required] - # The type discriminates the accepted keys: + # The payout or unreferenced refund destination account holder. + # [Required] for card payouts, [Optional] for unreferenced refunds. + # The type discriminates the accepted keys. For card payouts: # - individual: type, first_name, middle_name, last_name # - corporate, government: type, company_name + # For unreferenced refunds, where no name is required: + # - individual: type, first_name, middle_name, last_name, billing_address, phone, + # identification, email, date_of_birth, country_of_birth + # - corporate, government: type, company_name, billing_address, phone, identification, email # @return [CheckoutSdk::Common::AccountHolder] class TokenDestination < Destination attr_accessor :token, diff --git a/lib/checkout_sdk/payments/payments.rb b/lib/checkout_sdk/payments/payments.rb index 382afb6..d9acc6d 100644 --- a/lib/checkout_sdk/payments/payments.rb +++ b/lib/checkout_sdk/payments/payments.rb @@ -5,6 +5,10 @@ require 'checkout_sdk/payments/authorization_request' require 'checkout_sdk/payments/d_local_processing_settings' require 'checkout_sdk/payments/payout_request' +require 'checkout_sdk/payments/unreferenced_refund_request' +require 'checkout_sdk/payments/card_payout_item' +require 'checkout_sdk/payments/card_payout_processing' +require 'checkout_sdk/payments/payment_segment' require 'checkout_sdk/payments/user_action' require 'checkout_sdk/payments/three_ds_request' require 'checkout_sdk/payments/product_type' diff --git a/lib/checkout_sdk/payments/payments_client.rb b/lib/checkout_sdk/payments/payments_client.rb index 8741b2d..416c538 100644 --- a/lib/checkout_sdk/payments/payments_client.rb +++ b/lib/checkout_sdk/payments/payments_client.rb @@ -27,6 +27,16 @@ def request_payout(payout_request, idempotency_key = nil) idempotency_key) end + # @param [Hash, UnreferencedRefundRequest] unreferenced_refund_request + # @param [String, nil] idempotency_key + # @return [Hash] + def request_unreferenced_refund(unreferenced_refund_request, idempotency_key = nil) + api_client.invoke_post(PAYMENTS_PATH, + sdk_authorization, + unreferenced_refund_request, + idempotency_key) + end + # @param [String] payment_id # @param [Hash, CaptureRequest] capture_request # @param [String, nil] idempotency_key diff --git a/lib/checkout_sdk/payments/payout_request.rb b/lib/checkout_sdk/payments/payout_request.rb index 0eb2051..56a6460 100644 --- a/lib/checkout_sdk/payments/payout_request.rb +++ b/lib/checkout_sdk/payments/payout_request.rb @@ -2,26 +2,79 @@ module CheckoutSdk module Payments + # A card payout or bank payout request, sent to POST /payments. + # # @!attribute source + # The source of the payout. For bank payouts, the source from which the payout is funded. + # [Required] # @return [PayoutSource] # @!attribute destination + # 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 {CardDestination}, {NetworkTokenDestination}, {IdDestination} or {TokenDestination}; + # bank payouts take {IdDestination} or {BankAccountDestination}. + # [Required] # @return [Destination] # @!attribute amount + # The amount to pay out, in minor units. + # [Optional] for card payouts, [Required] for bank payouts + # min 0 # @return [Integer] # @!attribute currency + # 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] # @return [String] {CheckoutSdk::Common::Currency} # @!attribute reference + # A custom reference to identify the payout. For example, an order number. + # [Optional] + # max 50 characters for card payouts # @return [String] # @!attribute billing_descriptor + # 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 # @return [PayoutBillingDescriptor] # @!attribute sender + # 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] # @return [Sender] # @!attribute instruction + # Additional details about the payout instruction. + # [Required] for card payouts, [Optional] for bank payouts # @return [PaymentInstruction] # @!attribute processing_channel_id + # The unique identifier for the processing channel. + # [Required] + # ^(pc)_(\w{26})$ + # 29 characters # @return [String] # @!attribute metadata + # Stores additional information about the transaction with custom fields. Card payouts only. + # [Optional] # @return [Hash{String => Object}] + # @!attribute segment + # The dimension details about business segment for payment request. At least one dimension required. + # [Optional] + # @return [PaymentSegment] + # @!attribute items + # The order's line items. Card payouts only. + # [Optional] + # @return [Array(CardPayoutItem)] + # @!attribute previous_payment_id + # The identifier of an existing payment from the recurring series, if the payout is a recurring + # payment that uses stored card details. If the card payout is linked to a previous Account + # Funding Transaction (AFT), include the previous payment's ID when Checkout.com processed the + # AFT, or the scheme's transaction ID when another payment service provider processed it. + # Card payouts only. + # [Optional] + # @return [String] + # @!attribute processing + # Information related to the processing of the payout. Card payouts only. + # [Optional] + # @return [CardPayoutProcessing] class PayoutRequest attr_accessor :source, :destination, @@ -32,7 +85,11 @@ class PayoutRequest :sender, :instruction, :processing_channel_id, - :metadata + :metadata, + :segment, + :items, + :previous_payment_id, + :processing end end end diff --git a/lib/checkout_sdk/payments/unreferenced_refund_request.rb b/lib/checkout_sdk/payments/unreferenced_refund_request.rb new file mode 100644 index 0000000..4de90f8 --- /dev/null +++ b/lib/checkout_sdk/payments/unreferenced_refund_request.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # An unreferenced refund request, sent to POST /payments. + # The payment_type is set to UnreferencedRefund by the constructor. + # + # @!attribute source + # The source of the unreferenced refund. + # Only type and id are part of this position; amount is not used. + # [Required] + # @return [CurrencyAccountSource] + # @!attribute destination + # The destination of the unreferenced refund. + # Takes {CardDestination}, {NetworkTokenDestination}, {IdDestination} or {TokenDestination}. + # [Required] + # @return [Destination] + # @!attribute instruction + # Additional details about the unreferenced refund instruction. + # Only funds_transfer_type and purpose are part of this position. + # [Optional] + # @return [PaymentInstruction] + # @!attribute amount + # The amount in minor currency. + # [Required] + # Format: int64 + # min 0 + # max 9007199254740991 + # @return [Integer] + # @!attribute currency + # The three-letter ISO currency code of the payment. + # [Required] + # 3 characters + # @return [String] {CheckoutSdk::Common::Currency} + # @!attribute payment_type + # The type of the payment. Set to UnreferencedRefund by the constructor. + # [Required] + # Enum: "UnreferencedRefund" + # @return [String] + # @!attribute processing_channel_id + # The processing channel identifier. + # [Required] + # ^(pc)_(\w{26})$ + # @return [String] + # @!attribute reference + # An internal reference you can later use to identify this payment. + # [Optional] + # @return [String] + # @!attribute metadata + # 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. Note: this object only allows one level of depth, + # so cannot accept non-primitive data types such as objects or arrays. + # [Optional] + # @return [Hash{String => Object}] + # @!attribute previous_payment_id + # The previous related payment identifier. This could be the ID of the payment that you want to refund. + # [Optional] + # @return [String] + # @!attribute segment + # The dimension details about business segment for payment request. At least one dimension required. + # [Optional] + # @return [PaymentSegment] + # @!attribute processing + # Returns information related to the processing of the payment. + # Only processing_speed is part of this position. + # [Optional] + # @return [CardPayoutProcessing] + class UnreferencedRefundRequest + PAYMENT_TYPE = 'UnreferencedRefund' + + attr_accessor :source, + :destination, + :instruction, + :amount, + :currency, + :payment_type, + :processing_channel_id, + :reference, + :metadata, + :previous_payment_id, + :segment, + :processing + + def initialize + @payment_type = PAYMENT_TYPE + end + end + end +end diff --git a/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb b/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb index 58625aa..21ef83f 100644 --- a/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb +++ b/spec/checkout_sdk/payments/card_payout_destination_serialization_spec.rb @@ -157,8 +157,23 @@ def wire(object) expect(json['token_type']).to eq(full_destination.token_type) expect(json['cryptogram']).to eq(full_destination.cryptogram) expect(json['eci']).to eq(full_destination.eci) - expect(json['account_holder']).to eq(CheckoutSdk::JsonSerializer.to_custom_hash(account_holder) - .then { |h| JSON.parse(JSON.generate(h)) }) + end + + it 'round trips every account holder property through JSON' do + holder = full_destination.account_holder + json = wire(full_destination)['account_holder'] + + %i[type first_name middle_name last_name email date_of_birth country_of_birth].each do |attr| + expect(json[attr.to_s]).to eq(holder.public_send(attr)) + end + { + 'billing_address' => %i[address_line1 address_line2 city state zip country], + 'phone' => %i[country_code number], + 'identification' => %i[type number issuing_country date_of_expiry] + }.each do |key, attrs| + nested = holder.public_send(key) + expect(json[key]).to eq(attrs.to_h { |attr| [attr.to_s, nested.public_send(attr)] }) + end end it 'loads through require checkout_sdk alone' do diff --git a/spec/checkout_sdk/payments/payout_request_fields_spec.rb b/spec/checkout_sdk/payments/payout_request_fields_spec.rb new file mode 100644 index 0000000..836be29 --- /dev/null +++ b/spec/checkout_sdk/payments/payout_request_fields_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'json' + +RSpec.describe CheckoutSdk::Payments::PayoutRequest do + def wire(object) + JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(object))) + end + + let(:item) do + item = CheckoutSdk::Payments::CardPayoutItem.new + item.type = CheckoutSdk::Payments::ProductItemType::DIGITAL + item.sub_type = CheckoutSdk::Payments::ProductSubType::STABLECOIN + item + end + + let(:processing) do + processing = CheckoutSdk::Payments::CardPayoutProcessing.new + processing.affiliate_id = 'AFF123' + processing.affiliate_url = 'https://affiliate.example.com' + processing.processing_speed = 'fast' + processing.purchase_country = CheckoutSdk::Common::Country::GB + processing + end + + let(:segment) do + segment = CheckoutSdk::Payments::PaymentSegment.new + segment.brand = 'Acme' + segment.business_category = 'Retail' + segment.market = 'GB' + segment + end + + it 'serializes the card payout items, segment, previous_payment_id and processing keys' do + request = described_class.new + request.items = [item] + request.segment = segment + request.previous_payment_id = 'pay_fun26akvvjjerahhctaq2uzhu4' + request.processing = processing + + expect(wire(request)).to eq( + 'items' => [{ 'type' => 'digital', 'sub_type' => 'stablecoin' }], + 'segment' => { 'brand' => 'Acme', 'business_category' => 'Retail', 'market' => 'GB' }, + 'previous_payment_id' => 'pay_fun26akvvjjerahhctaq2uzhu4', + 'processing' => { + 'affiliate_id' => 'AFF123', + 'affiliate_url' => 'https://affiliate.example.com', + 'processing_speed' => 'fast', + 'purchase_country' => 'GB' + } + ) + end + + it 'round trips every new property through JSON' do + request = described_class.new + request.items = [item] + request.processing = processing + + json = wire(request) + + expect(json['items'].first).to eq('type' => item.type, 'sub_type' => item.sub_type) + %i[affiliate_id affiliate_url processing_speed purchase_country].each do |attr| + expect(json['processing'][attr.to_s]).to eq(processing.public_send(attr)) + end + end + + it 'omits the new properties when they are not set' do + expect(wire(described_class.new)).to eq({}) + end +end diff --git a/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb b/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb new file mode 100644 index 0000000..7dcbf70 --- /dev/null +++ b/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb @@ -0,0 +1,296 @@ +# frozen_string_literal: true + +require 'json' +require 'open3' +require 'rbconfig' + +RSpec.describe CheckoutSdk::Payments::UnreferencedRefundRequest do + def wire(object) + JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(object))) + end + + let(:account_holder_json) do + { + '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' + } + end + + # The schema has no request example; field examples from the schema are used where it declares them. + let(:swagger_example) do + { + 'payment_type' => 'UnreferencedRefund', + 'source' => { 'type' => 'currency_account', 'id' => 'ca_y3oqhf46pyzuxjbcn2giaqnb44' }, + 'destination' => { + 'type' => 'card', + 'number' => '4242424242424242', + 'expiry_month' => 6, + 'expiry_year' => 2030, + 'account_holder' => account_holder_json + }, + 'instruction' => { 'funds_transfer_type' => 'MI', 'purpose' => 'family_support' }, + 'amount' => 1000, + 'currency' => 'GBP', + 'processing_channel_id' => 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', + 'reference' => 'ORD-5023-4E89', + 'metadata' => { 'coupon_code' => 'NY2018', 'partner_id' => 123_989 }, + 'previous_payment_id' => 'pay_fun26akvvjjerahhctaq2uzhu4', + 'segment' => { 'brand' => 'Acme Corporation', 'business_category' => 'Inward Payments', 'market' => 'UK' }, + 'processing' => { 'processing_speed' => 'fast' } + } + end + + let(:account_holder) do + address = CheckoutSdk::Common::Address.new + address.address_line1 = '123 High St.' + address.address_line2 = 'Flat 456' + address.city = 'London' + address.state = 'GB' + address.zip = 'SW1A 1AA' + address.country = CheckoutSdk::Common::Country::GB + + phone = CheckoutSdk::Common::Phone.new + phone.country_code = '+44' + phone.number = '4155552671' + + identification = CheckoutSdk::Common::AccountHolderIdentification.new + identification.type = CheckoutSdk::Common::AccountHolderIdentificationType::PASSPORT + identification.number = 'E12345678' + identification.issuing_country = CheckoutSdk::Common::Country::GB + identification.date_of_expiry = '2030-01-01' + + holder = CheckoutSdk::Common::AccountHolder.new + holder.type = CheckoutSdk::Common::AccountHolderType::INDIVIDUAL + holder.first_name = 'John' + holder.middle_name = 'Paul' + holder.last_name = 'Smith' + holder.billing_address = address + holder.phone = phone + holder.identification = identification + holder.email = 'john.smith@example.com' + holder.date_of_birth = '1985-05-15' + holder.country_of_birth = CheckoutSdk::Common::Country::GB + holder + end + + let(:card_destination) do + destination = CheckoutSdk::Payments::CardDestination.new + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2030 + destination.account_holder = account_holder + destination + end + + let(:network_token_destination) do + destination = CheckoutSdk::Payments::NetworkTokenDestination.new + destination.token = '4543474002249996' + destination.expiry_month = 6 + destination.expiry_year = 2025 + destination.token_type = CheckoutSdk::Payments::NetworkTokenType::VTS + destination.cryptogram = 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=' + destination.eci = '05' + destination.account_holder = account_holder + destination + end + + let(:id_destination) do + destination = CheckoutSdk::Payments::IdDestination.new + destination.id = 'src_wmlfc3zyhqzehihu7giusaaawu' + destination + end + + let(:token_destination) do + destination = CheckoutSdk::Payments::TokenDestination.new + destination.token = 'tok_ubfj2q76miwundwlk72vxt2i7q' + destination + end + + let(:full_request) do + source = CheckoutSdk::Payments::CurrencyAccountSource.new + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + + instruction = CheckoutSdk::Payments::PaymentInstruction.new + instruction.funds_transfer_type = 'MI' + instruction.purpose = CheckoutSdk::Payments::PaymentPurposeType::FAMILY_SUPPORT + + segment = CheckoutSdk::Payments::PaymentSegment.new + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' + + processing = CheckoutSdk::Payments::CardPayoutProcessing.new + processing.processing_speed = 'fast' + + request = described_class.new + request.source = source + request.destination = card_destination + request.instruction = instruction + request.amount = 1000 + request.currency = CheckoutSdk::Common::Currency::GBP + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + request.reference = 'ORD-5023-4E89' + request.metadata = { 'coupon_code' => 'NY2018', 'partner_id' => 123_989 } + request.previous_payment_id = 'pay_fun26akvvjjerahhctaq2uzhu4' + request.segment = segment + request.processing = processing + request + end + + it 'sets payment_type to the UnreferencedRefund literal in the constructor' do + expect(described_class.new.payment_type).to eq('UnreferencedRefund') + expect(wire(described_class.new)).to eq('payment_type' => 'UnreferencedRefund') + end + + it 'serializes exactly the twelve snake case keys the schema declares' do + json = wire(full_request) + + expect(json.keys.sort).to eq(%w[amount currency destination instruction metadata payment_type previous_payment_id + processing processing_channel_id reference segment source]) + end + + it 'serializes to the swagger example' do + expect(wire(full_request)).to eq(swagger_example) + end + + it 'nests the card destination under destination' do + destination = wire(full_request)['destination'] + + expect(destination.keys.sort).to eq(%w[account_holder expiry_month expiry_year number type]) + expect(destination['type']).to eq('card') + expect(destination['expiry_month']).to be_an(Integer) + expect(destination['expiry_year']).to be_an(Integer) + end + + it 'nests the network token destination under destination' do + full_request.destination = network_token_destination + + destination = wire(full_request)['destination'] + + expect(destination).to eq( + 'type' => 'network_token', + 'token' => '4543474002249996', + 'expiry_month' => 6, + 'expiry_year' => 2025, + 'token_type' => 'vts', + 'cryptogram' => 'hv8mUFzPzRZoCAAAAAEQBDMAAAA=', + 'eci' => '05', + 'account_holder' => account_holder_json + ) + end + + it 'nests the id destination under destination and omits the optional account holder when unset' do + full_request.destination = id_destination + + expect(wire(full_request)['destination']).to eq('type' => 'id', 'id' => 'src_wmlfc3zyhqzehihu7giusaaawu') + end + + it 'nests the token destination under destination and omits the optional account holder when unset' do + full_request.destination = token_destination + + expect(wire(full_request)['destination']).to eq('type' => 'token', 'token' => 'tok_ubfj2q76miwundwlk72vxt2i7q') + end + + it 'serializes the optional rich account holder on the id and token destinations when set' do + [id_destination, token_destination].each do |destination| + destination.account_holder = account_holder + full_request.destination = destination + + expect(wire(full_request)['destination']['account_holder']).to eq(account_holder_json) + end + end + + it 'omits every optional property when only the required ones are set' do + request = described_class.new + request.source = full_request.source + request.destination = token_destination + request.amount = 1000 + request.currency = CheckoutSdk::Common::Currency::GBP + request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' + + expect(wire(request).keys.sort).to eq(%w[amount currency destination payment_type processing_channel_id source]) + end + + it 'round trips every property through JSON' do + json = wire(full_request) + + %i[amount currency payment_type processing_channel_id reference metadata previous_payment_id].each do |attr| + expect(json[attr.to_s]).to eq(full_request.public_send(attr)) + end + expect(json['source']).to eq('type' => full_request.source.type, 'id' => full_request.source.id) + expect(json['instruction']).to eq('funds_transfer_type' => full_request.instruction.funds_transfer_type, + 'purpose' => full_request.instruction.purpose) + %i[brand business_category market].each do |attr| + expect(json['segment'][attr.to_s]).to eq(full_request.segment.public_send(attr)) + end + expect(json['processing']['processing_speed']).to eq(full_request.processing.processing_speed) + %i[type number expiry_month expiry_year].each do |attr| + expect(json['destination'][attr.to_s]).to eq(full_request.destination.public_send(attr)) + end + end + + it 'loads through require checkout_sdk alone' do + lib = File.expand_path('../../../lib', __dir__) + script = "require 'checkout_sdk'; print CheckoutSdk::Payments::UnreferencedRefundRequest.new.payment_type" + + output, status = Open3.capture2(RbConfig.ruby, '-I', lib, '-e', script) + + expect(status).to be_success + expect(output).to eq('UnreferencedRefund') + end +end + +RSpec.describe CheckoutSdk::Payments::PaymentsClient do + let(:credentials_mock) { double('credentials') } + let(:api_client_mock) { double('api_client') } + let(:configuration_mock) { double('configuration') } + let(:client) { described_class.new(api_client_mock, configuration_mock) } + + before do + allow(configuration_mock).to receive(:credentials).and_return(credentials_mock) + allow(credentials_mock).to receive(:get_authorization).and_return('secret_key') + end + + describe '#request_unreferenced_refund' do + it 'POSTs the typed request to payments' do + request = CheckoutSdk::Payments::UnreferencedRefundRequest.new + expect(api_client_mock).to receive(:invoke_post) + .with('payments', 'secret_key', request, nil).and_return('r') + expect(client.request_unreferenced_refund(request)).to eq('r') + end + + it 'passes through an idempotency key' do + request = CheckoutSdk::Payments::UnreferencedRefundRequest.new + expect(api_client_mock).to receive(:invoke_post) + .with('payments', 'secret_key', request, 'idem-1').and_return('r') + expect(client.request_unreferenced_refund(request, 'idem-1')).to eq('r') + end + + it 'also accepts a raw Hash' do + hash = { 'payment_type' => 'UnreferencedRefund' } + expect(api_client_mock).to receive(:invoke_post) + .with('payments', 'secret_key', hash, nil).and_return('r') + expect(client.request_unreferenced_refund(hash)).to eq('r') + end + end +end From 75f8cd004c5019aa6053596db792fc60d6a5fed8 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 18:24:51 +0100 Subject: [PATCH 3/9] Comment fix --- lib/checkout_sdk/payments/unreferenced_refund_request.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/checkout_sdk/payments/unreferenced_refund_request.rb b/lib/checkout_sdk/payments/unreferenced_refund_request.rb index 4de90f8..1215bbc 100644 --- a/lib/checkout_sdk/payments/unreferenced_refund_request.rb +++ b/lib/checkout_sdk/payments/unreferenced_refund_request.rb @@ -24,8 +24,8 @@ module Payments # The amount in minor currency. # [Required] # Format: int64 - # min 0 - # max 9007199254740991 + # min 1 + # max 9999999999 # @return [Integer] # @!attribute currency # The three-letter ISO currency code of the payment. From b516112e52450e2a910924c20458262962a2f31c Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 18:34:58 +0100 Subject: [PATCH 4/9] PaymentType PayLater value + more docs --- .../payments/payment_instruction.rb | 38 ++++++++++++++++++- lib/checkout_sdk/payments/payment_segment.rb | 11 ++++++ lib/checkout_sdk/payments/payment_type.rb | 5 ++- .../source/currency_account_source.rb | 8 ++++ .../payments/source/entity_source.rb | 5 +++ .../payments/source/payout_source.rb | 11 ++++++ .../payments/payment_type_spec.rb | 18 +++++++++ 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 spec/checkout_sdk/payments/payment_type_spec.rb diff --git a/lib/checkout_sdk/payments/payment_instruction.rb b/lib/checkout_sdk/payments/payment_instruction.rb index d96452a..4f89d07 100644 --- a/lib/checkout_sdk/payments/payment_instruction.rb +++ b/lib/checkout_sdk/payments/payment_instruction.rb @@ -2,21 +2,57 @@ module CheckoutSdk module Payments + # Additional details about the instruction of a payout or an unreferenced refund. + # Card payouts and unreferenced refunds use purpose and funds_transfer_type; bank payouts use scheme. + # # @!attribute purpose - # @return [String] + # The purpose of the card payout or unreferenced refund. + # Required if the card's issuer_country is one of AR, BD, CL, CO, EG, IN or MX. + # [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" + # @return [String] {PaymentPurposeType} # @!attribute charge_bearer + # The charge bearer of a bank payout. + # [Optional] + # @deprecated Not in the current request specification; it only appears on the payout details + # response. Retained for backwards compatibility and will be removed in a future version. # @return [String] {ChargeBearer} # @!attribute repair + # Whether the bank payout may be repaired. + # [Optional] + # @deprecated Not in the current request specification; it only appears on the payout details + # response. Retained for backwards compatibility and will be removed in a future version. # @return [TrueClass, FalseClass] # @!attribute scheme + # An option to restrict the payment scheme type of a bank payout, when not available payouts will fail. + # [Optional] + # Enum: "local" "instant" # @return [String] {InstructionScheme} # @!attribute quote_id + # The FX quote identifier of a bank payout. + # [Optional] + # @deprecated Not in the current request specification; it only appears on the payout details + # response. Retained for backwards compatibility and will be removed in a future version. # @return [String] # @!attribute skip_expiry + # [Optional] + # @deprecated Not in the current specification of any instruction, so the API does not define it. + # Retained for backwards compatibility and will be removed in a future version. # @return [TrueClass, FalseClass] # @!attribute funds_transfer_type + # 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 # @return [String] # @!attribute mvv + # [Optional] + # @deprecated Not in the current specification of any instruction, so the API does not define it. + # Retained for backwards compatibility and will be removed in a future version. # @return [String] class PaymentInstruction attr_accessor :purpose, diff --git a/lib/checkout_sdk/payments/payment_segment.rb b/lib/checkout_sdk/payments/payment_segment.rb index 7e77619..a0fd225 100644 --- a/lib/checkout_sdk/payments/payment_segment.rb +++ b/lib/checkout_sdk/payments/payment_segment.rb @@ -2,11 +2,22 @@ module CheckoutSdk module Payments + # The dimension details about business segment for payment request. At least one dimension required. + # # @!attribute brand + # The brand of business segment. + # [Optional] + # max 50 characters # @return [String] # @!attribute business_category + # The category of business segment. + # [Optional] + # max 50 characters # @return [String] # @!attribute market + # The market of business segment. + # [Optional] + # max 50 characters # @return [String] class PaymentSegment attr_accessor :brand, diff --git a/lib/checkout_sdk/payments/payment_type.rb b/lib/checkout_sdk/payments/payment_type.rb index 291c06c..a3b1264 100644 --- a/lib/checkout_sdk/payments/payment_type.rb +++ b/lib/checkout_sdk/payments/payment_type.rb @@ -2,11 +2,14 @@ module CheckoutSdk module Payments + # The payment type of a payment request. + # Enum: "Regular" "Recurring" "MOTO" "Installment" "PayLater" "Unscheduled" module PaymentType REGULAR = 'Regular' RECURRING = 'Recurring' - MOTO = 'Moto' + MOTO = 'MOTO' INSTALLMENT = 'Installment' + PAY_LATER = 'PayLater' UNSCHEDULED = 'Unscheduled' end end diff --git a/lib/checkout_sdk/payments/source/currency_account_source.rb b/lib/checkout_sdk/payments/source/currency_account_source.rb index 9f657d0..1fc039d 100644 --- a/lib/checkout_sdk/payments/source/currency_account_source.rb +++ b/lib/checkout_sdk/payments/source/currency_account_source.rb @@ -2,6 +2,14 @@ module CheckoutSdk module Payments + # A currency account source, with type currency_account. + # Used by card payouts, bank payouts and unreferenced refunds. + # + # id: 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})$ + # 29 characters class CurrencyAccountSource < PayoutSource def initialize super(PayoutSourceType::CURRENCY_ACCOUNT) diff --git a/lib/checkout_sdk/payments/source/entity_source.rb b/lib/checkout_sdk/payments/source/entity_source.rb index b937ceb..6490679 100644 --- a/lib/checkout_sdk/payments/source/entity_source.rb +++ b/lib/checkout_sdk/payments/source/entity_source.rb @@ -2,6 +2,11 @@ module CheckoutSdk module Payments + # An entity source, with type entity. Bank payouts only. + # + # id: the entity or sub-entity's identifier, only applies to Platform Clients. + # [Required] + # ^(ent)_(\w{26})$ class EntitySource < PayoutSource def initialize super(PayoutSourceType::ENTITY) diff --git a/lib/checkout_sdk/payments/source/payout_source.rb b/lib/checkout_sdk/payments/source/payout_source.rb index 69952ef..58b7ad8 100644 --- a/lib/checkout_sdk/payments/source/payout_source.rb +++ b/lib/checkout_sdk/payments/source/payout_source.rb @@ -2,11 +2,22 @@ module CheckoutSdk module Payments + # The source of a card payout, bank payout or unreferenced refund. + # # @!attribute type + # The payout source type. Set by each subclass. + # [Required] # @return [String] {PayoutSourceType} # @!attribute id + # The identifier of the source. See {CurrencyAccountSource} and {EntitySource} for the format. + # [Required] # @return [String] # @!attribute amount + # The amount of the source. + # [Optional] + # @deprecated Not in the current specification of any payout or unreferenced refund source, so the + # API does not define it. Retained for backwards compatibility and will be removed in a future + # version. # @return [String] class PayoutSource attr_reader :type diff --git a/spec/checkout_sdk/payments/payment_type_spec.rb b/spec/checkout_sdk/payments/payment_type_spec.rb new file mode 100644 index 0000000..5938d32 --- /dev/null +++ b/spec/checkout_sdk/payments/payment_type_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.describe CheckoutSdk::Payments::PaymentType do + it 'declares exactly the spec payment_type values' do + values = described_class.constants.map { |c| described_class.const_get(c) } + + expect(values).to contain_exactly('Regular', 'Recurring', 'MOTO', 'Installment', 'PayLater', 'Unscheduled') + end + + it 'sends MOTO in upper case and PayLater on the wire' do + request = CheckoutSdk::Payments::PaymentRequest.new + request.payment_type = described_class::MOTO + expect(CheckoutSdk::JsonSerializer.to_custom_hash(request)['payment_type']).to eq('MOTO') + + request.payment_type = described_class::PAY_LATER + expect(CheckoutSdk::JsonSerializer.to_custom_hash(request)['payment_type']).to eq('PayLater') + end +end From e7f3d084a6a821309c5d53bbf6fe36f43883ddf1 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 18:38:37 +0100 Subject: [PATCH 5/9] Small payments scheme review --- .../payments/instruction_remittance.rb | 17 +++++++++++++++++ lib/checkout_sdk/payments/instruction_scheme.rb | 4 ++++ .../payments/payment_instruction.rb | 8 +++++++- lib/checkout_sdk/payments/payments.rb | 1 + .../payments/payment_instruction_spec.rb | 17 +++++++++++++++++ 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 lib/checkout_sdk/payments/instruction_remittance.rb create mode 100644 spec/checkout_sdk/payments/payment_instruction_spec.rb diff --git a/lib/checkout_sdk/payments/instruction_remittance.rb b/lib/checkout_sdk/payments/instruction_remittance.rb new file mode 100644 index 0000000..5947324 --- /dev/null +++ b/lib/checkout_sdk/payments/instruction_remittance.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Payments + # The remittance details of a bank payout instruction, used for reconciliation. + # + # @!attribute reference + # 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 + # @return [String] + class InstructionRemittance + attr_accessor :reference + end + end +end diff --git a/lib/checkout_sdk/payments/instruction_scheme.rb b/lib/checkout_sdk/payments/instruction_scheme.rb index bcf1830..150b13b 100644 --- a/lib/checkout_sdk/payments/instruction_scheme.rb +++ b/lib/checkout_sdk/payments/instruction_scheme.rb @@ -2,7 +2,11 @@ module CheckoutSdk module Payments + # The payment scheme type of a bank payout instruction. + # Enum: "local" "instant" class InstructionScheme + # @deprecated Not in the current specification, which only lists local and instant. Retained for + # backwards compatibility and will be removed in a future version. SWIFT = 'swift' LOCAL = 'local' INSTANT = 'instant' diff --git a/lib/checkout_sdk/payments/payment_instruction.rb b/lib/checkout_sdk/payments/payment_instruction.rb index 4f89d07..cefcc96 100644 --- a/lib/checkout_sdk/payments/payment_instruction.rb +++ b/lib/checkout_sdk/payments/payment_instruction.rb @@ -3,7 +3,8 @@ module CheckoutSdk module Payments # Additional details about the instruction of a payout or an unreferenced refund. - # Card payouts and unreferenced refunds use purpose and funds_transfer_type; bank payouts use scheme. + # Card payouts and unreferenced refunds use purpose and funds_transfer_type; bank payouts use scheme and + # remittance. # # @!attribute purpose # The purpose of the card payout or unreferenced refund. @@ -30,6 +31,10 @@ module Payments # [Optional] # Enum: "local" "instant" # @return [String] {InstructionScheme} + # @!attribute remittance + # The remittance details of a bank payout, used for reconciliation. + # [Optional] + # @return [InstructionRemittance] # @!attribute quote_id # The FX quote identifier of a bank payout. # [Optional] @@ -59,6 +64,7 @@ class PaymentInstruction :charge_bearer, :repair, :scheme, + :remittance, :quote_id, :skip_expiry, :funds_transfer_type, diff --git a/lib/checkout_sdk/payments/payments.rb b/lib/checkout_sdk/payments/payments.rb index d9acc6d..06e1849 100644 --- a/lib/checkout_sdk/payments/payments.rb +++ b/lib/checkout_sdk/payments/payments.rb @@ -20,6 +20,7 @@ require 'checkout_sdk/payments/authorization_type' require 'checkout_sdk/payments/os_type' require 'checkout_sdk/payments/payment_instruction' +require 'checkout_sdk/payments/instruction_remittance' require 'checkout_sdk/payments/payment_method_details' require 'checkout_sdk/payments/refund_request' require 'checkout_sdk/payments/payments_search_request' diff --git a/spec/checkout_sdk/payments/payment_instruction_spec.rb b/spec/checkout_sdk/payments/payment_instruction_spec.rb new file mode 100644 index 0000000..e4b7a4a --- /dev/null +++ b/spec/checkout_sdk/payments/payment_instruction_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe CheckoutSdk::Payments::PaymentInstruction do + it 'serializes the bank payout scheme and remittance reference' do + remittance = CheckoutSdk::Payments::InstructionRemittance.new + remittance.reference = 'INV-2026-0001' + instruction = described_class.new + instruction.scheme = CheckoutSdk::Payments::InstructionScheme::LOCAL + instruction.remittance = remittance + request = CheckoutSdk::Payments::PayoutRequest.new + request.instruction = instruction + + hash = JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(request))) + + expect(hash['instruction']).to eq('scheme' => 'local', 'remittance' => { 'reference' => 'INV-2026-0001' }) + end +end From a201f4f18fb8402103a32c3aace404f6b9b0eeb5 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 19:37:42 +0100 Subject: [PATCH 6/9] Test enhancements --- .../payments/payout_request_fields_spec.rb | 16 ++++++++++------ .../payments/unreferenced_refund_request_spec.rb | 9 +++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/spec/checkout_sdk/payments/payout_request_fields_spec.rb b/spec/checkout_sdk/payments/payout_request_fields_spec.rb index 836be29..de097ed 100644 --- a/spec/checkout_sdk/payments/payout_request_fields_spec.rb +++ b/spec/checkout_sdk/payments/payout_request_fields_spec.rb @@ -17,7 +17,7 @@ def wire(object) let(:processing) do processing = CheckoutSdk::Payments::CardPayoutProcessing.new processing.affiliate_id = 'AFF123' - processing.affiliate_url = 'https://affiliate.example.com' + processing.affiliate_url = 'www.mycrypto.com' processing.processing_speed = 'fast' processing.purchase_country = CheckoutSdk::Common::Country::GB processing @@ -25,9 +25,9 @@ def wire(object) let(:segment) do segment = CheckoutSdk::Payments::PaymentSegment.new - segment.brand = 'Acme' - segment.business_category = 'Retail' - segment.market = 'GB' + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' segment end @@ -40,11 +40,11 @@ def wire(object) expect(wire(request)).to eq( 'items' => [{ 'type' => 'digital', 'sub_type' => 'stablecoin' }], - 'segment' => { 'brand' => 'Acme', 'business_category' => 'Retail', 'market' => 'GB' }, + 'segment' => { 'brand' => 'Acme Corporation', 'business_category' => 'Inward Payments', 'market' => 'UK' }, 'previous_payment_id' => 'pay_fun26akvvjjerahhctaq2uzhu4', 'processing' => { 'affiliate_id' => 'AFF123', - 'affiliate_url' => 'https://affiliate.example.com', + 'affiliate_url' => 'www.mycrypto.com', 'processing_speed' => 'fast', 'purchase_country' => 'GB' } @@ -54,11 +54,15 @@ def wire(object) it 'round trips every new property through JSON' do request = described_class.new request.items = [item] + request.segment = segment + request.previous_payment_id = 'pay_fun26akvvjjerahhctaq2uzhu4' request.processing = processing json = wire(request) expect(json['items'].first).to eq('type' => item.type, 'sub_type' => item.sub_type) + %i[brand business_category market].each { |attr| expect(json['segment'][attr.to_s]).to eq(segment.public_send(attr)) } + expect(json['previous_payment_id']).to eq(request.previous_payment_id) %i[affiliate_id affiliate_url processing_speed purchase_country].each do |attr| expect(json['processing'][attr.to_s]).to eq(processing.public_send(attr)) end diff --git a/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb b/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb index 7dcbf70..6b9b17b 100644 --- a/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb +++ b/spec/checkout_sdk/payments/unreferenced_refund_request_spec.rb @@ -49,7 +49,7 @@ def wire(object) 'account_holder' => account_holder_json }, 'instruction' => { 'funds_transfer_type' => 'MI', 'purpose' => 'family_support' }, - 'amount' => 1000, + 'amount' => 120, 'currency' => 'GBP', 'processing_channel_id' => 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', 'reference' => 'ORD-5023-4E89', @@ -146,7 +146,7 @@ def wire(object) request.source = source request.destination = card_destination request.instruction = instruction - request.amount = 1000 + request.amount = 120 request.currency = CheckoutSdk::Common::Currency::GBP request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' request.reference = 'ORD-5023-4E89' @@ -224,7 +224,7 @@ def wire(object) request = described_class.new request.source = full_request.source request.destination = token_destination - request.amount = 1000 + request.amount = 120 request.currency = CheckoutSdk::Common::Currency::GBP request.processing_channel_id = 'pc_q4dbxom5jbgudnjzjpz7j2z6uq' @@ -268,7 +268,8 @@ def wire(object) before do allow(configuration_mock).to receive(:credentials).and_return(credentials_mock) - allow(credentials_mock).to receive(:get_authorization).and_return('secret_key') + allow(credentials_mock).to receive(:get_authorization) + .with(CheckoutSdk::AuthorizationType::SECRET_KEY_OR_OAUTH).and_return('secret_key') end describe '#request_unreferenced_refund' do From 580df4c6985728995901cd44b57e58a75608e6b5 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 22:36:27 +0100 Subject: [PATCH 7/9] Schema fixes, doc updates + tests --- lib/checkout_sdk/common/account_holder.rb | 67 +++++- .../common/account_holder_identification.rb | 21 ++ .../payments/card_payout_processing.rb | 1 + .../destination/bank_account_destination.rb | 7 +- .../destination/network_token_destination.rb | 1 + .../hosted/hosted_payment_instruction.rb | 9 + .../payments/instruction_scheme.rb | 5 +- .../payments/payment_instruction.rb | 33 +-- .../payments/payout_billing_descriptor.rb | 10 + lib/checkout_sdk/payments/payout_request.rb | 3 +- .../payments/sender/corporate_sender.rb | 19 ++ .../payments/sender/government_sender.rb | 21 ++ .../payments/sender/individual_sender.rb | 46 ++++ .../payments/sender/instrument_sender.rb | 3 + lib/checkout_sdk/payments/sender/sender.rb | 16 ++ .../payments/source/payout_source.rb | 8 +- .../payments/unreferenced_refund_request.rb | 2 +- .../bank_payout_request_serialization_spec.rb | 203 ++++++++++++++++++ .../deprecated_payout_members_spec.rb | 55 +++++ .../payments/payout_request_fields_spec.rb | 177 ++++++++++++++- 20 files changed, 680 insertions(+), 27 deletions(-) create mode 100644 spec/checkout_sdk/payments/bank_payout_request_serialization_spec.rb create mode 100644 spec/checkout_sdk/payments/deprecated_payout_members_spec.rb diff --git a/lib/checkout_sdk/common/account_holder.rb b/lib/checkout_sdk/common/account_holder.rb index 29001f7..5b985fb 100644 --- a/lib/checkout_sdk/common/account_holder.rb +++ b/lib/checkout_sdk/common/account_holder.rb @@ -2,37 +2,102 @@ module CheckoutSdk module Common + # The account holder of a payment source, an instrument, or a payout or unreferenced refund destination. + # This class is shared by many positions; the per-position notes below cover the card payout, bank payout + # and unreferenced refund destinations. Each position only accepts the keys its schema declares, so leave + # the other properties unset. + # - Card payout card and network_token destinations: type, first_name, middle_name, last_name, + # billing_address, phone, identification, email, date_of_birth, country_of_birth. + # - Card payout id and token destinations: type, first_name, middle_name, last_name, company_name. + # - Bank payout bank_account destination: type, first_name, last_name, company_name, tax_id, + # date_of_birth, country_of_birth, residential_status, billing_address, phone, identification. + # - Unreferenced refund destinations: as the card payout card and network_token destinations. + # # @!attribute type + # The type of account holder. + # [Required] for payout and unreferenced refund destinations (when the account holder is sent) + # Enum: "individual" "corporate" "government" # @return [String] {AccountHolderType} # @!attribute full_name + # [Optional] Not declared by the payout or unreferenced refund destination schemas. # @return [String] # @!attribute first_name + # The account holder's legal first name. The combination of first_name, middle_name and last_name + # should exceed 3 letters in length for card payouts. A single character or all non-alphabetic + # characters return a field validation error. + # [Required] for individual card payout and bank payout destinations; not required for card payout id + # and token destinations when the name is already stored with the token or instrument; [Optional] for + # unreferenced refunds + # max 50 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute middle_name + # The account holder's middle name. + # [Optional] Card payouts and unreferenced refunds only. + # max 50 characters # @return [String] # @!attribute last_name + # The account holder's legal last name. The combination of first_name, middle_name and last_name + # should exceed 3 letters in length for card payouts. A single character or all non-alphabetic + # characters return a field validation error. + # [Required] for individual card payout and bank payout destinations; not required for card payout id + # and token destinations when the name is already stored with the token or instrument; [Optional] for + # unreferenced refunds + # max 50 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute email + # The account holder's email address. + # [Optional] Card payout card and network_token destinations and unreferenced refunds only. + # max 255 characters # @return [String] # @!attribute gender + # [Optional] Not declared by the payout or unreferenced refund destination schemas. # @return [String] # @!attribute company_name + # The legal name of the corporate or government account holder. A single character or all + # non-alphabetic characters return a field validation error. + # [Required] for corporate and government payout and unreferenced refund destinations + # max 50 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute tax_id + # The legal account holder's tax number or reference. + # [Optional] Bank payouts only. # @return [String] # @!attribute date_of_birth + # The account holder's date of birth, in the format YYYY-MM-DD. Individual account holders only. + # [Optional] + # 10 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute country_of_birth - # @return [Country] + # The account holder's country of birth, as a two-letter ISO country code. Individual account holders + # only. + # [Optional] + # 2 characters + # @return [String] {Country} # @!attribute residential_status + # The account holder's residential status. + # [Optional] Bank payouts only. + # Enum: "resident" "non_resident" # @return [String] {ResidentialStatusType} # @!attribute billing_address + # The account holder's billing address. For card payouts and unreferenced refunds, it is required if + # your company is incorporated in the United States. + # [Required] for individual bank payout destinations, [Optional] otherwise # @return [Address] # @!attribute phone + # The account holder's phone number. + # [Optional] Not part of the card payout id and token destinations, nor of the corporate and + # government bank payout destinations. # @return [Phone] # @!attribute identification + # The account holder's identification. Providing identification details for the payout recipient + # increases the likelihood of a successful payout. + # [Optional] Not part of the card payout id and token destinations, nor of the corporate and + # government bank payout destinations. # @return [AccountHolderIdentification] # @!attribute account_name_inquiry + # Whether the ANI (account name inquiry) check should be performed with the scheme (Visa or + # Mastercard). Payment sources only. + # [Optional] Not declared by the payout or unreferenced refund destination schemas. # @return [Boolean] class AccountHolder attr_accessor :type, diff --git a/lib/checkout_sdk/common/account_holder_identification.rb b/lib/checkout_sdk/common/account_holder_identification.rb index 17ea747..ebec3ee 100644 --- a/lib/checkout_sdk/common/account_holder_identification.rb +++ b/lib/checkout_sdk/common/account_holder_identification.rb @@ -2,13 +2,34 @@ module CheckoutSdk module Common + # The identification of an account holder or a sender. Shared by the card payout, bank payout, + # unreferenced refund and regular payment positions. + # # @!attribute type + # The type of identification. + # [Required] for card payout and unreferenced refund account holders and card payout senders, + # [Optional] for bank payout and regular payment senders and bank payout account holders + # Enum: "passport" "driving_license" "national_id" "company_registration" "tax_id" (card payouts and + # unreferenced refunds); "passport" "driving_licence" "national_id" (bank payout and regular payment + # senders) # @return [String] {AccountHolderIdentificationType} # @!attribute number + # The identification number. + # [Required] for card payout and unreferenced refund account holders and card payout senders, + # [Optional] for bank payout and regular payment senders and bank payout account holders + # max 25 characters (card payout and unreferenced refund account holders) # @return [String] # @!attribute issuing_country + # 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] + # 2 characters (card payouts and unreferenced refunds) # @return [String] {Country} # @!attribute date_of_expiry + # If applicable, the expiration date of the identification, in the format YYYY-MM-DD. + # Card payouts and unreferenced refunds only. + # [Optional] + # 10 characters # @return [String] class AccountHolderIdentification attr_accessor :type, diff --git a/lib/checkout_sdk/payments/card_payout_processing.rb b/lib/checkout_sdk/payments/card_payout_processing.rb index 6f4fa7f..d613292 100644 --- a/lib/checkout_sdk/payments/card_payout_processing.rb +++ b/lib/checkout_sdk/payments/card_payout_processing.rb @@ -9,6 +9,7 @@ module Payments # 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 # @return [String] # @!attribute affiliate_url diff --git a/lib/checkout_sdk/payments/destination/bank_account_destination.rb b/lib/checkout_sdk/payments/destination/bank_account_destination.rb index 795872f..2e4f6fa 100644 --- a/lib/checkout_sdk/payments/destination/bank_account_destination.rb +++ b/lib/checkout_sdk/payments/destination/bank_account_destination.rb @@ -8,6 +8,7 @@ module Payments # The type of the bank account. Note that some beneficiary banks do not accept payouts to # savings accounts. # [Required] + # Enum: "savings" "current" "cash" # @return [String] {CheckoutSdk::Common::AccountType} # @!attribute account_number # Number (which can contain letters) that identifies the local account. @@ -46,9 +47,9 @@ module Payments # @!attribute bank # The bank details. # [Optional] - # @deprecated Not in the current specification of the bank account payout destination, so the - # API does not define it. Retained for backwards compatibility and will be removed in a - # future version. + # @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 and will be removed in the + # next major version. There is no replacement. # @return [CheckoutSdk::Common::BankDetails] class BankAccountDestination < Destination attr_accessor :account_type, diff --git a/lib/checkout_sdk/payments/destination/network_token_destination.rb b/lib/checkout_sdk/payments/destination/network_token_destination.rb index da4f394..58cb410 100644 --- a/lib/checkout_sdk/payments/destination/network_token_destination.rb +++ b/lib/checkout_sdk/payments/destination/network_token_destination.rb @@ -23,6 +23,7 @@ module Payments # @!attribute token_type # The network token type. # [Required] + # Enum: "vts" "mdes" "applepay" "googlepay" # @return [String] {CheckoutSdk::Payments::NetworkTokenType} # @!attribute cryptogram # The network token's Base64-encoded cryptographic identifier (TAVV). diff --git a/lib/checkout_sdk/payments/hosted/hosted_payment_instruction.rb b/lib/checkout_sdk/payments/hosted/hosted_payment_instruction.rb index eedbfa4..c334453 100644 --- a/lib/checkout_sdk/payments/hosted/hosted_payment_instruction.rb +++ b/lib/checkout_sdk/payments/hosted/hosted_payment_instruction.rb @@ -2,7 +2,16 @@ module CheckoutSdk module Payments + # Details about the payment instruction of a hosted payments session. + # Hosted payments use purpose only. + # # @!attribute purpose + # The purpose of the payment. This field might be required for AFT transactions depending on the + # card's issuer_country. + # [Optional] + # Enum: "donations" "education" "emergency_need" "expatriation" "family_support" "financial_services" + # "gifts" "income" "insurance" "investment" "it_services" "leisure" "loan_payment" "medical_treatment" + # "other" "pension" "royalties" "savings" "travel_and_tourism" # @return [String] {PaymentPurposeType} class HostedPaymentInstruction attr_accessor :purpose diff --git a/lib/checkout_sdk/payments/instruction_scheme.rb b/lib/checkout_sdk/payments/instruction_scheme.rb index 150b13b..a4c97c0 100644 --- a/lib/checkout_sdk/payments/instruction_scheme.rb +++ b/lib/checkout_sdk/payments/instruction_scheme.rb @@ -5,8 +5,9 @@ module Payments # The payment scheme type of a bank payout instruction. # Enum: "local" "instant" class InstructionScheme - # @deprecated Not in the current specification, which only lists local and instant. Retained for - # backwards compatibility and will be removed in a future version. + # @deprecated Not in the request schema, which only lists local and instant; still returned in + # PayoutDetails (instruction.scheme). Retained for backwards compatibility and will be removed in the + # next major version. There is no replacement. SWIFT = 'swift' LOCAL = 'local' INSTANT = 'instant' diff --git a/lib/checkout_sdk/payments/payment_instruction.rb b/lib/checkout_sdk/payments/payment_instruction.rb index cefcc96..006c5e6 100644 --- a/lib/checkout_sdk/payments/payment_instruction.rb +++ b/lib/checkout_sdk/payments/payment_instruction.rb @@ -2,13 +2,15 @@ module CheckoutSdk module Payments - # Additional details about the instruction of a payout or an unreferenced refund. + # Additional details about the instruction of a payment, a payout or an unreferenced refund. # Card payouts and unreferenced refunds use purpose and funds_transfer_type; bank payouts use scheme and - # remittance. + # remittance; regular payments (PaymentRequest) use purpose only. # # @!attribute purpose - # The purpose of the card payout or unreferenced refund. - # Required if the card's issuer_country is one of AR, BD, CL, CO, EG, IN or MX. + # The purpose of the payment, card payout or unreferenced refund. + # For card payouts, required if the card's issuer_country is one of AR, BD, CL, CO, EG, IN, MX or SA. + # For unreferenced refunds, required if the card's issuer_country is one of AR, BD, CL, CO, EG, IN or MX. + # For regular 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" @@ -17,14 +19,16 @@ module Payments # @!attribute charge_bearer # The charge bearer of a bank payout. # [Optional] - # @deprecated Not in the current request specification; it only appears on the payout details - # response. Retained for backwards compatibility and will be removed in a future version. + # @deprecated Not in the request schema; still returned in PayoutDetails (instruction.charge_bearer). + # Retained for backwards compatibility and will be removed in the next major version. There is no + # replacement. # @return [String] {ChargeBearer} # @!attribute repair # Whether the bank payout may be repaired. # [Optional] - # @deprecated Not in the current request specification; it only appears on the payout details - # response. Retained for backwards compatibility and will be removed in a future version. + # @deprecated Not in the request schema; still returned in PayoutDetails (instruction.repair). + # Retained for backwards compatibility and will be removed in the next major version. There is no + # replacement. # @return [TrueClass, FalseClass] # @!attribute scheme # An option to restrict the payment scheme type of a bank payout, when not available payouts will fail. @@ -38,13 +42,14 @@ module Payments # @!attribute quote_id # The FX quote identifier of a bank payout. # [Optional] - # @deprecated Not in the current request specification; it only appears on the payout details - # response. Retained for backwards compatibility and will be removed in a future version. + # @deprecated Not in the request schema; still returned in PayoutDetails (instruction.quote_id). + # Retained for backwards compatibility and will be removed in the next major version. There is no + # replacement. # @return [String] # @!attribute skip_expiry # [Optional] - # @deprecated Not in the current specification of any instruction, so the API does not define it. - # Retained for backwards compatibility and will be removed in a future version. + # @deprecated Not in any schema of the specification. Retained for backwards compatibility and will be + # removed in the next major version. There is no replacement. # @return [TrueClass, FalseClass] # @!attribute funds_transfer_type # The funds transfer type code for the type of card payout you're performing. @@ -56,8 +61,8 @@ module Payments # @return [String] # @!attribute mvv # [Optional] - # @deprecated Not in the current specification of any instruction, so the API does not define it. - # Retained for backwards compatibility and will be removed in a future version. + # @deprecated Not in any schema of the specification. Retained for backwards compatibility and will be + # removed in the next major version. There is no replacement. # @return [String] class PaymentInstruction attr_accessor :purpose, diff --git a/lib/checkout_sdk/payments/payout_billing_descriptor.rb b/lib/checkout_sdk/payments/payout_billing_descriptor.rb index e4283b1..29cf432 100644 --- a/lib/checkout_sdk/payments/payout_billing_descriptor.rb +++ b/lib/checkout_sdk/payments/payout_billing_descriptor.rb @@ -2,7 +2,17 @@ module CheckoutSdk module Payments + # 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. + # # @!attribute reference + # 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 # @return [String] class PayoutBillingDescriptor attr_accessor :reference diff --git a/lib/checkout_sdk/payments/payout_request.rb b/lib/checkout_sdk/payments/payout_request.rb index 56a6460..be246fb 100644 --- a/lib/checkout_sdk/payments/payout_request.rb +++ b/lib/checkout_sdk/payments/payout_request.rb @@ -24,6 +24,7 @@ module Payments # 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, max 3 characters for bank payouts # @return [String] {CheckoutSdk::Common::Currency} # @!attribute reference # A custom reference to identify the payout. For example, an order number. @@ -49,7 +50,7 @@ module Payments # The unique identifier for the processing channel. # [Required] # ^(pc)_(\w{26})$ - # 29 characters + # 29 characters (min 29, max 29) for card payouts only # @return [String] # @!attribute metadata # Stores additional information about the transaction with custom fields. Card payouts only. diff --git a/lib/checkout_sdk/payments/sender/corporate_sender.rb b/lib/checkout_sdk/payments/sender/corporate_sender.rb index 0a1ee60..1d442f1 100644 --- a/lib/checkout_sdk/payments/sender/corporate_sender.rb +++ b/lib/checkout_sdk/payments/sender/corporate_sender.rb @@ -2,15 +2,34 @@ module CheckoutSdk module Payments + # A corporate sender, with type corporate. Used by card payouts, bank payouts and regular payments. + # The reference is inherited from {Sender}. + # # @!attribute company_name + # The sender's company name. For card payouts, this must be a valid legal name: a single character or + # all non-alphabetic characters return a field validation error. + # [Required] + # max 35 characters for card payouts # @return [String] # @!attribute address + # The sender's address. For card payouts, the residential address of an individual sender or the + # registered corporate address of a corporate or government sender. + # [Required] # @return [CheckoutSdk::Common::Address] # @!attribute reference_type + # The type of identifier used as the reference. Card payouts only. + # [Required] for card payouts + # Enum: "other" # @return [String] # @!attribute source_of_funds + # The source of the funds used to fund the card payout. Card payouts only. + # [Required] for card payouts + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" # @return [String] {SourceOfFunds} # @!attribute identification + # The sender's identification. For card payouts, required if the card's issuer_country is one of AR, BR, + # CO or PE. + # [Optional] Card payouts only. # @return [CheckoutSdk::Common::AccountHolderIdentification] class CorporateSender < Sender attr_accessor :company_name, diff --git a/lib/checkout_sdk/payments/sender/government_sender.rb b/lib/checkout_sdk/payments/sender/government_sender.rb index c26ad45..236dc66 100644 --- a/lib/checkout_sdk/payments/sender/government_sender.rb +++ b/lib/checkout_sdk/payments/sender/government_sender.rb @@ -2,17 +2,38 @@ module CheckoutSdk module Payments + # A government sender, with type government. Card payouts only. + # # @!attribute company_name + # The sender's company name. This must be a valid legal name: a single character or all non-alphabetic + # characters return a field validation error. + # [Required] + # max 35 characters # @return [String] # @!attribute address + # The sender's registered corporate address. + # [Required] # @return [CheckoutSdk::Common::Address] # @!attribute reference + # The unique identifier for the sender, for example a customer number. Only alphanumeric characters + # are supported. + # [Required] + # min 5 characters + # max 15 characters # @return [String] # @!attribute reference_type + # The type of identifier used as the reference. + # [Required] + # Enum: "other" # @return [String] # @!attribute source_of_funds + # The source of the funds used to fund the card payout. + # [Required] + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" # @return [String] {SourceOfFunds} # @!attribute identification + # The sender's identification. Required if the card's issuer_country is one of AR, BR, CO or PE. + # [Optional] # @return [CheckoutSdk::Common::AccountHolderIdentification] class GovernmentSender < Sender attr_accessor :company_name, diff --git a/lib/checkout_sdk/payments/sender/individual_sender.rb b/lib/checkout_sdk/payments/sender/individual_sender.rb index 999beab..6c83a38 100644 --- a/lib/checkout_sdk/payments/sender/individual_sender.rb +++ b/lib/checkout_sdk/payments/sender/individual_sender.rb @@ -2,29 +2,75 @@ module CheckoutSdk module Payments + # An individual sender, with type individual. Used by card payouts, bank payouts and regular payments. + # # @!attribute first_name + # The sender's legal first name. For card payouts, the combination of first_name, middle_name and + # last_name should exceed 3 letters in length, and a single character or all non-alphabetic + # characters return a field validation error. + # [Required] + # max 50 characters for card payouts # @return [String] # @!attribute middle_name + # The sender's middle name. Card payouts only. + # [Optional] + # max 50 characters # @return [String] # @!attribute last_name + # The sender's legal last name. For card payouts, the combination of first_name, middle_name and + # last_name must exceed 3 letters in length, and a single character or all non-alphabetic characters + # return a field validation error. + # [Required] + # max 50 characters for card payouts # @return [String] # @!attribute dob + # The sender's date of birth in a YYYY-MM-DD format. Strongly recommended where available. Bank + # payouts only; card payouts and regular payments use date_of_birth. + # [Optional] + # max 10 characters # @return [String] # @!attribute address + # The sender's address. For card payouts, the sender's residential address. + # [Required] # @return [CheckoutSdk::Common::Address] # @!attribute identification + # The sender's identification. For card payouts, required if the card's issuer_country is one of AR, BR, + # CO or PE. + # [Optional] # @return [CheckoutSdk::Common::AccountHolderIdentification] # @!attribute reference + # 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. For regular payments, the unique reference for the + # sender of the payment. + # [Required] for card payouts and bank payouts, [Optional] for regular payments + # min 5 characters, max 15 characters for card payouts # @return [String] # @!attribute reference_type + # The type of identifier used as the reference. Card payouts only. + # [Required] for card payouts + # Enum: "other" # @return [String] # @!attribute date_of_birth + # 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] + # 10 characters for card payouts, max 10 characters for regular payments # @return [String] # @!attribute source_of_funds + # The source of the funds used to fund the card payout. Card payouts only. + # [Required] for card payouts + # Enum: "credit" "debit" "prepaid" "deposit_account" "mobile_money_account" "cash" # @return [String] {SourceOfFunds} # @!attribute country_of_birth + # The sender's country of birth, as a two-letter ISO country code. Card payouts only. + # [Optional] + # 2 characters # @return [String] {CheckoutSdk::Common::Country} # @!attribute nationality + # The sender's nationality, as a two-letter ISO country code. Card payouts only. + # [Optional] + # 2 characters # @return [String] {CheckoutSdk::Common::Country} class IndividualSender < Sender attr_accessor :first_name, diff --git a/lib/checkout_sdk/payments/sender/instrument_sender.rb b/lib/checkout_sdk/payments/sender/instrument_sender.rb index b74dca6..4d9610e 100644 --- a/lib/checkout_sdk/payments/sender/instrument_sender.rb +++ b/lib/checkout_sdk/payments/sender/instrument_sender.rb @@ -2,6 +2,9 @@ module CheckoutSdk module Payments + # An instrument sender, with type instrument: the sender information is retrieved from the payment + # instrument (the destination, for bank payouts). Used by bank payouts and regular payments. + # The reference is inherited from {Sender}: [Required] for bank payouts, [Optional] for regular payments. class InstrumentSender < Sender def initialize super(CheckoutSdk::Payments::SenderType::INSTRUMENT) diff --git a/lib/checkout_sdk/payments/sender/sender.rb b/lib/checkout_sdk/payments/sender/sender.rb index beb5a6b..4a3bc42 100644 --- a/lib/checkout_sdk/payments/sender/sender.rb +++ b/lib/checkout_sdk/payments/sender/sender.rb @@ -2,9 +2,25 @@ module CheckoutSdk module Payments + # The sender of a payment or a payout. Set the subclass that matches the sender type. + # - Card payouts (money transfers): {IndividualSender}, {CorporateSender}, {GovernmentSender}. + # - Bank payouts: {IndividualSender}, {CorporateSender}, {InstrumentSender}. Not supported for Platform or + # Direct Seller Checkout.com clients. + # - Regular payments (PaymentRequest): {IndividualSender}, {CorporateSender}, {InstrumentSender}. + # # @!attribute type + # The sender type. Set by each subclass. + # [Required] + # Enum: "individual" "corporate" "government" (card payouts); "instrument" "individual" "corporate" + # (bank payouts and regular payments) # @return [String] {SenderType} # @!attribute reference + # 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. For regular payments, the unique reference for the + # sender of the payment. + # [Required] for card payouts and bank payouts, [Optional] for regular payments + # min 5 characters, max 15 characters for card payouts # @return [String] class Sender attr_reader :type diff --git a/lib/checkout_sdk/payments/source/payout_source.rb b/lib/checkout_sdk/payments/source/payout_source.rb index 58b7ad8..e794c56 100644 --- a/lib/checkout_sdk/payments/source/payout_source.rb +++ b/lib/checkout_sdk/payments/source/payout_source.rb @@ -15,10 +15,10 @@ module Payments # @!attribute amount # The amount of the source. # [Optional] - # @deprecated Not in the current specification of any payout or unreferenced refund source, so the - # API does not define it. Retained for backwards compatibility and will be removed in a future - # version. - # @return [String] + # @deprecated Not in the request schema of any payout or unreferenced refund source; still returned in + # PayoutDetails (source.amount). Retained for backwards compatibility and will be removed in the next + # major version. There is no replacement. + # @return [Integer] class PayoutSource attr_reader :type attr_accessor :id, diff --git a/lib/checkout_sdk/payments/unreferenced_refund_request.rb b/lib/checkout_sdk/payments/unreferenced_refund_request.rb index 1215bbc..5afb29b 100644 --- a/lib/checkout_sdk/payments/unreferenced_refund_request.rb +++ b/lib/checkout_sdk/payments/unreferenced_refund_request.rb @@ -68,12 +68,12 @@ module Payments class UnreferencedRefundRequest PAYMENT_TYPE = 'UnreferencedRefund' + attr_reader :payment_type attr_accessor :source, :destination, :instruction, :amount, :currency, - :payment_type, :processing_channel_id, :reference, :metadata, diff --git a/spec/checkout_sdk/payments/bank_payout_request_serialization_spec.rb b/spec/checkout_sdk/payments/bank_payout_request_serialization_spec.rb new file mode 100644 index 0000000..672bcb7 --- /dev/null +++ b/spec/checkout_sdk/payments/bank_payout_request_serialization_spec.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +require 'json' + +RSpec.describe CheckoutSdk::Payments::PayoutRequest do + def wire(object) + JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(object))) + end + + let(:address_json) do + { + 'address_line1' => '123 High St.', + 'address_line2' => 'Flat 456', + 'city' => 'London', + 'zip' => 'SW1A 1AA', + 'country' => 'GB' + } + end + + # The schema has no request example; field examples from the schema are used where it declares them. + let(:swagger_example) do + { + 'source' => { 'type' => 'entity', 'id' => 'ent_w4jelhppmfiufdnatam37wrfc4' }, + 'destination' => { + 'type' => 'bank_account', + 'account_type' => 'savings', + 'account_number' => '13654567455', + 'bank_code' => '123-456', + 'branch_code' => '6443', + 'iban' => 'HU93116000060000000012345676', + 'bban' => '3704 0044 0532 0130 00', + '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' => address_json, + 'phone' => { 'country_code' => '+1', 'number' => '415 555 2671' } + } + }, + 'amount' => 6540, + 'currency' => 'USD', + 'reference' => 'Withdrawal', + 'billing_descriptor' => { 'reference' => 'Withdrawal01' }, + 'sender' => { + 'type' => 'individual', + 'reference' => 'CUST12345', + 'first_name' => 'John', + 'last_name' => 'Jones', + 'dob' => '1985-05-15', + 'address' => address_json + }, + 'instruction' => { 'scheme' => 'local', 'remittance' => { 'reference' => 'INV-2024-0001' } }, + 'processing_channel_id' => 'pc_q4dbxom5jbgudnjzjpz7j2z6uq', + 'segment' => { 'brand' => 'Acme Corporation', 'business_category' => 'Inward Payments', 'market' => 'UK' } + } + end + + let(:address) do + address = CheckoutSdk::Common::Address.new + address.address_line1 = '123 High St.' + address.address_line2 = 'Flat 456' + address.city = 'London' + address.zip = 'SW1A 1AA' + address.country = CheckoutSdk::Common::Country::GB + address + end + + let(:account_holder) do + phone = CheckoutSdk::Common::Phone.new + phone.country_code = '+1' + phone.number = '415 555 2671' + + holder = CheckoutSdk::Common::AccountHolder.new + holder.type = CheckoutSdk::Common::AccountHolderType::INDIVIDUAL + holder.first_name = 'John' + holder.last_name = 'Smith' + holder.tax_id = '123456' + holder.date_of_birth = '1986-01-01' + holder.country_of_birth = CheckoutSdk::Common::Country::GB + holder.residential_status = 'resident' + holder.billing_address = address + holder.phone = phone + holder + end + + let(:destination) do + destination = CheckoutSdk::Payments::BankAccountDestination.new + destination.account_type = CheckoutSdk::Common::AccountType::SAVINGS + destination.account_number = '13654567455' + destination.bank_code = '123-456' + destination.branch_code = '6443' + destination.iban = 'HU93116000060000000012345676' + destination.bban = '3704 0044 0532 0130 00' + destination.swift_bic = '37040044' + destination.country = CheckoutSdk::Common::Country::GB + destination.account_holder = account_holder + destination + end + + let(:sender) do + sender = CheckoutSdk::Payments::IndividualSender.new + sender.reference = 'CUST12345' + sender.first_name = 'John' + sender.last_name = 'Jones' + sender.dob = '1985-05-15' + sender.address = address + sender + end + + let(:request) do + source = CheckoutSdk::Payments::EntitySource.new + source.id = 'ent_w4jelhppmfiufdnatam37wrfc4' + + billing_descriptor = CheckoutSdk::Payments::PayoutBillingDescriptor.new + billing_descriptor.reference = 'Withdrawal01' + + remittance = CheckoutSdk::Payments::InstructionRemittance.new + remittance.reference = 'INV-2024-0001' + instruction = CheckoutSdk::Payments::PaymentInstruction.new + instruction.scheme = CheckoutSdk::Payments::InstructionScheme::LOCAL + instruction.remittance = remittance + + segment = CheckoutSdk::Payments::PaymentSegment.new + segment.brand = 'Acme Corporation' + segment.business_category = 'Inward Payments' + segment.market = 'UK' + + request = described_class.new + request.source = source + request.destination = destination + request.amount = 6540 + request.currency = CheckoutSdk::Common::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 + request + end + + it 'serializes exactly the ten snake case keys the bank payout schema declares' do + expect(wire(request).keys.sort).to eq(%w[amount billing_descriptor currency destination instruction + processing_channel_id reference segment sender source]) + end + + it 'serializes the whole body to the swagger example' do + json = wire(request) + + expect(json).to eq(swagger_example) + expect(json['amount']).to be_an(Integer) + end + + it 'round trips every property through JSON' do + json = wire(request) + + %i[amount currency reference processing_channel_id].each do |attr| + expect(json[attr.to_s]).to eq(request.public_send(attr)) + end + expect(json['source']).to eq('type' => request.source.type, 'id' => request.source.id) + expect(json['billing_descriptor']).to eq('reference' => request.billing_descriptor.reference) + %i[type account_type account_number bank_code branch_code iban bban swift_bic country].each do |attr| + expect(json['destination'][attr.to_s]).to eq(destination.public_send(attr)) + end + %i[type first_name last_name tax_id date_of_birth country_of_birth residential_status].each do |attr| + expect(json['destination']['account_holder'][attr.to_s]).to eq(account_holder.public_send(attr)) + end + %i[address_line1 address_line2 city zip country].each do |attr| + expect(json['destination']['account_holder']['billing_address'][attr.to_s]).to eq(address.public_send(attr)) + expect(json['sender']['address'][attr.to_s]).to eq(address.public_send(attr)) + end + %i[type reference first_name last_name dob].each do |attr| + expect(json['sender'][attr.to_s]).to eq(sender.public_send(attr)) + end + expect(json['instruction']).to eq('scheme' => request.instruction.scheme, + 'remittance' => { 'reference' => request.instruction.remittance.reference }) + %i[brand business_category market].each do |attr| + expect(json['segment'][attr.to_s]).to eq(request.segment.public_send(attr)) + end + end + + it 'nests an id destination without an account holder' do + id_destination = CheckoutSdk::Payments::IdDestination.new + id_destination.id = 'src_wmlfc3zyhqzehihu7giusaaawu' + request.destination = id_destination + + expect(wire(request)['destination']).to eq('type' => 'id', 'id' => 'src_wmlfc3zyhqzehihu7giusaaawu') + end + + it 'serializes an instrument sender with its reference only' do + instrument_sender = CheckoutSdk::Payments::InstrumentSender.new + instrument_sender.reference = 'CUST12345' + request.sender = instrument_sender + + expect(wire(request)['sender']).to eq('type' => 'instrument', 'reference' => 'CUST12345') + end +end diff --git a/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb b/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb new file mode 100644 index 0000000..ab60d4f --- /dev/null +++ b/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'json' + +# Deprecated payout members are retained for backwards compatibility until the next major version, so +# they must keep serializing under their wire keys. +RSpec.describe CheckoutSdk::Payments::PayoutRequest do + def wire(object) + JSON.parse(JSON.generate(CheckoutSdk::JsonSerializer.to_custom_hash(object))) + end + + it 'still serializes the deprecated instruction members' do + instruction = CheckoutSdk::Payments::PaymentInstruction.new + instruction.charge_bearer = CheckoutSdk::Payments::ChargeBearer::OUR + instruction.repair = true + instruction.quote_id = 'qte_mbabizu24mvu3mela5njyhpit4' + instruction.scheme = CheckoutSdk::Payments::InstructionScheme::SWIFT + instruction.skip_expiry = true + instruction.mvv = '0123456789' + request = described_class.new + request.instruction = instruction + + expect(wire(request)['instruction']).to eq('charge_bearer' => 'OUR', + 'repair' => true, + 'quote_id' => 'qte_mbabizu24mvu3mela5njyhpit4', + 'scheme' => 'swift', + 'skip_expiry' => true, + 'mvv' => '0123456789') + end + + it 'still serializes the deprecated source amount' do + source = CheckoutSdk::Payments::CurrencyAccountSource.new + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + source.amount = 1000 + request = described_class.new + request.source = source + + expect(wire(request)['source']).to eq('type' => 'currency_account', + 'id' => 'ca_y3oqhf46pyzuxjbcn2giaqnb44', + 'amount' => 1000) + end + + it 'still serializes the deprecated bank account destination bank' do + bank = CheckoutSdk::Common::BankDetails.new + bank.name = 'Lloyds TSB' + bank.branch = 'Bournemouth' + destination = CheckoutSdk::Payments::BankAccountDestination.new + destination.bank = bank + request = described_class.new + request.destination = destination + + expect(wire(request)['destination']).to eq('type' => 'bank_account', + 'bank' => { 'name' => 'Lloyds TSB', 'branch' => 'Bournemouth' }) + end +end diff --git a/spec/checkout_sdk/payments/payout_request_fields_spec.rb b/spec/checkout_sdk/payments/payout_request_fields_spec.rb index de097ed..217ea41 100644 --- a/spec/checkout_sdk/payments/payout_request_fields_spec.rb +++ b/spec/checkout_sdk/payments/payout_request_fields_spec.rb @@ -61,7 +61,9 @@ def wire(object) json = wire(request) expect(json['items'].first).to eq('type' => item.type, 'sub_type' => item.sub_type) - %i[brand business_category market].each { |attr| expect(json['segment'][attr.to_s]).to eq(segment.public_send(attr)) } + %i[brand business_category market].each do |attr| + expect(json['segment'][attr.to_s]).to eq(segment.public_send(attr)) + end expect(json['previous_payment_id']).to eq(request.previous_payment_id) %i[affiliate_id affiliate_url processing_speed purchase_country].each do |attr| expect(json['processing'][attr.to_s]).to eq(processing.public_send(attr)) @@ -71,4 +73,177 @@ def wire(object) it 'omits the new properties when they are not set' do expect(wire(described_class.new)).to eq({}) end + + describe 'a full card payout' do + # The schema has no request example; field examples from the schema are used where it declares them. + let(:swagger_example) do + { + 'source' => { 'type' => 'currency_account', 'id' => 'ca_y3oqhf46pyzuxjbcn2giaqnb44' }, + 'destination' => { + 'type' => 'card', + 'number' => '4242424242424242', + 'expiry_month' => 6, + 'expiry_year' => 2030, + 'account_holder' => { 'type' => 'individual', 'first_name' => 'John', 'last_name' => 'Smith' } + }, + 'amount' => 1000, + 'currency' => 'GBP', + 'instruction' => { 'purpose' => 'family_support', 'funds_transfer_type' => 'AA' }, + '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' => 'CUST12345', + 'reference_type' => 'other', + 'source_of_funds' => 'debit' + }, + 'reference' => 'ORD-5023-4E89', + 'previous_payment_id' => 'pay_fun26akvvjjerahhctaq2uzhu4', + 'metadata' => { 'udf1' => 'payout-1' }, + 'segment' => { 'brand' => 'Acme Corporation', 'business_category' => 'Inward Payments', 'market' => 'UK' }, + 'processing' => { + 'affiliate_id' => 'AFF123', + 'affiliate_url' => 'www.mycrypto.com', + 'processing_speed' => 'fast', + 'purchase_country' => 'GB' + } + } + end + + let(:individual_holder) do + holder = CheckoutSdk::Common::AccountHolder.new + holder.type = CheckoutSdk::Common::AccountHolderType::INDIVIDUAL + holder.first_name = 'John' + holder.last_name = 'Smith' + holder + end + + let(:card_destination) do + destination = CheckoutSdk::Payments::CardDestination.new + destination.number = '4242424242424242' + destination.expiry_month = 6 + destination.expiry_year = 2030 + destination.account_holder = individual_holder + destination + end + + let(:sender) do + address = CheckoutSdk::Common::Address.new + address.address_line1 = '123 High St.' + address.city = 'London' + address.country = CheckoutSdk::Common::Country::GB + + sender = CheckoutSdk::Payments::IndividualSender.new + sender.first_name = 'John' + sender.last_name = 'Jones' + sender.address = address + sender.reference = 'CUST12345' + sender.reference_type = 'other' + sender.source_of_funds = CheckoutSdk::Payments::SourceOfFunds::DEBIT + sender + end + + let(:request) do + source = CheckoutSdk::Payments::CurrencyAccountSource.new + source.id = 'ca_y3oqhf46pyzuxjbcn2giaqnb44' + + instruction = CheckoutSdk::Payments::PaymentInstruction.new + instruction.purpose = CheckoutSdk::Payments::PaymentPurposeType::FAMILY_SUPPORT + instruction.funds_transfer_type = 'AA' + + request = described_class.new + request.source = source + request.destination = card_destination + request.amount = 1000 + request.currency = CheckoutSdk::Common::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 = { 'udf1' => 'payout-1' } + request.segment = segment + request.processing = processing + request + end + + it 'serializes exactly the thirteen snake case keys the card payout schema declares' do + expect(wire(request).keys.sort).to eq(%w[amount currency destination instruction items metadata + previous_payment_id processing processing_channel_id reference + segment sender source]) + end + + it 'serializes the whole body to the swagger example' do + json = wire(request) + + expect(json).to eq(swagger_example) + expect(json['amount']).to be_an(Integer) + expect(json['destination']['expiry_month']).to be_an(Integer) + end + + it 'round trips every property through JSON' do + json = wire(request) + + %i[amount currency processing_channel_id reference previous_payment_id metadata].each do |attr| + expect(json[attr.to_s]).to eq(request.public_send(attr)) + end + expect(json['source']).to eq('type' => request.source.type, 'id' => request.source.id) + %i[type number expiry_month expiry_year].each do |attr| + expect(json['destination'][attr.to_s]).to eq(card_destination.public_send(attr)) + end + %i[purpose funds_transfer_type].each do |attr| + expect(json['instruction'][attr.to_s]).to eq(request.instruction.public_send(attr)) + end + %i[type first_name last_name reference reference_type source_of_funds].each do |attr| + expect(json['sender'][attr.to_s]).to eq(sender.public_send(attr)) + end + %i[address_line1 city country].each do |attr| + expect(json['sender']['address'][attr.to_s]).to eq(sender.address.public_send(attr)) + end + expect(json['items']).to eq([{ 'type' => item.type, 'sub_type' => item.sub_type }]) + %i[brand business_category market].each do |attr| + expect(json['segment'][attr.to_s]).to eq(segment.public_send(attr)) + end + %i[affiliate_id affiliate_url processing_speed purchase_country].each do |attr| + expect(json['processing'][attr.to_s]).to eq(processing.public_send(attr)) + end + end + + it 'nests an id destination with a name only account holder' do + destination = CheckoutSdk::Payments::IdDestination.new + destination.id = 'src_wmlfc3zyhqzehihu7giusaaawu' + destination.account_holder = individual_holder + request.destination = destination + + expect(wire(request)['destination']).to eq( + 'type' => 'id', + 'id' => 'src_wmlfc3zyhqzehihu7giusaaawu', + 'account_holder' => { 'type' => 'individual', 'first_name' => 'John', 'last_name' => 'Smith' } + ) + end + + it 'nests a token destination with a corporate account holder' do + holder = CheckoutSdk::Common::AccountHolder.new + holder.type = CheckoutSdk::Common::AccountHolderType::CORPORATE + holder.company_name = 'Acme Corporation' + destination = CheckoutSdk::Payments::TokenDestination.new + destination.token = 'tok_ubfj2q76miwundwlk72vxt2i7q' + destination.account_holder = holder + request.destination = destination + + expect(wire(request)['destination']).to eq( + 'type' => 'token', + 'token' => 'tok_ubfj2q76miwundwlk72vxt2i7q', + 'account_holder' => { 'type' => 'corporate', 'company_name' => 'Acme Corporation' } + ) + end + + it 'nests a card destination with its four required keys' do + expect(wire(request)['destination'].keys.sort).to eq(%w[account_holder expiry_month expiry_year number type]) + end + end end From bc9b48043fbdea65f1199a2fe555cec23f7dc5ab Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 28 Oct 2026 22:54:29 +0100 Subject: [PATCH 8/9] Deprecated fields mark up + tests --- lib/checkout_sdk/common/account_holder.rb | 14 ++++++++++++-- lib/checkout_sdk/common/common.rb | 1 + lib/checkout_sdk/common/residential_status_type.rb | 11 +++++++++++ .../payments/deprecated_payout_members_spec.rb | 13 +++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 lib/checkout_sdk/common/residential_status_type.rb diff --git a/lib/checkout_sdk/common/account_holder.rb b/lib/checkout_sdk/common/account_holder.rb index 5b985fb..06c7cb3 100644 --- a/lib/checkout_sdk/common/account_holder.rb +++ b/lib/checkout_sdk/common/account_holder.rb @@ -19,7 +19,12 @@ module Common # Enum: "individual" "corporate" "government" # @return [String] {AccountHolderType} # @!attribute full_name - # [Optional] Not declared by the payout or unreferenced refund destination schemas. + # The account holder's full name. + # [Optional] + # @deprecated Not in the request schema of any account holder; the Klarna payment source account_holder + # had it until the API removed it. Still returned in the PayPal payment details source account_holder. + # Retained for backwards compatibility and will be removed in the next major version. There is no + # replacement. # @return [String] # @!attribute first_name # The account holder's legal first name. The combination of first_name, middle_name and last_name @@ -50,7 +55,12 @@ module Common # max 255 characters # @return [String] # @!attribute gender - # [Optional] Not declared by the payout or unreferenced refund destination schemas. + # The account holder's gender. + # [Optional] + # @deprecated Not in the request schema of any account holder; the Klarna payment source account_holder + # had it until the API removed it. Still returned in the Klarna payment details source account_holder. + # Retained for backwards compatibility and will be removed in the next major version. There is no + # replacement. # @return [String] # @!attribute company_name # The legal name of the corporate or government account holder. A single character or all diff --git a/lib/checkout_sdk/common/common.rb b/lib/checkout_sdk/common/common.rb index 739e199..6e04c98 100644 --- a/lib/checkout_sdk/common/common.rb +++ b/lib/checkout_sdk/common/common.rb @@ -9,6 +9,7 @@ require 'checkout_sdk/common/country' require 'checkout_sdk/common/account_holder_identification_type' require 'checkout_sdk/common/account_holder_type' +require 'checkout_sdk/common/residential_status_type' require 'checkout_sdk/common/instrument_type' require 'checkout_sdk/common/bank_details' require 'checkout_sdk/common/payment_source_type' diff --git a/lib/checkout_sdk/common/residential_status_type.rb b/lib/checkout_sdk/common/residential_status_type.rb new file mode 100644 index 0000000..0dd7c88 --- /dev/null +++ b/lib/checkout_sdk/common/residential_status_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Common + # The account holder's residential status (bank payout and refund destination account holders). + module ResidentialStatusType + RESIDENT = 'resident' + NON_RESIDENT = 'non_resident' + end + end +end diff --git a/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb b/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb index ab60d4f..8cb1b19 100644 --- a/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb +++ b/spec/checkout_sdk/payments/deprecated_payout_members_spec.rb @@ -52,4 +52,17 @@ def wire(object) expect(wire(request)['destination']).to eq('type' => 'bank_account', 'bank' => { 'name' => 'Lloyds TSB', 'branch' => 'Bournemouth' }) end + + it 'still serializes the deprecated account holder full_name and gender' do + account_holder = CheckoutSdk::Common::AccountHolder.new + account_holder.full_name = 'John Smith' + account_holder.gender = 'male' + + expect(wire(account_holder)).to eq('full_name' => 'John Smith', 'gender' => 'male') + end + + it 'exposes the residential status values of the spec' do + expect(CheckoutSdk::Common::ResidentialStatusType::RESIDENT).to eq('resident') + expect(CheckoutSdk::Common::ResidentialStatusType::NON_RESIDENT).to eq('non_resident') + end end From 880646fe0ff2d35ceca4d1ffe99e1bb750b292e1 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Fri, 30 Oct 2026 07:25:43 +0100 Subject: [PATCH 9/9] Comments update --- lib/checkout_sdk/common/account_holder.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/checkout_sdk/common/account_holder.rb b/lib/checkout_sdk/common/account_holder.rb index 06c7cb3..3e63704 100644 --- a/lib/checkout_sdk/common/account_holder.rb +++ b/lib/checkout_sdk/common/account_holder.rb @@ -21,8 +21,8 @@ module Common # @!attribute full_name # The account holder's full name. # [Optional] - # @deprecated Not in the request schema of any account holder; the Klarna payment source account_holder - # had it until the API removed it. Still returned in the PayPal payment details source account_holder. + # @deprecated Not in the request schema of any account holder. Still returned in the PayPal payment + # details source account_holder. # Retained for backwards compatibility and will be removed in the next major version. There is no # replacement. # @return [String] @@ -30,9 +30,10 @@ module Common # The account holder's legal first name. The combination of first_name, middle_name and last_name # should exceed 3 letters in length for card payouts. A single character or all non-alphabetic # characters return a field validation error. - # [Required] for individual card payout and bank payout destinations; not required for card payout id - # and token destinations when the name is already stored with the token or instrument; [Optional] for - # unreferenced refunds + # [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. In the card payout id and token destinations it is not required if the first name is + # already stored with the token or instrument. # max 50 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute middle_name @@ -44,9 +45,10 @@ module Common # The account holder's legal last name. The combination of first_name, middle_name and last_name # should exceed 3 letters in length for card payouts. A single character or all non-alphabetic # characters return a field validation error. - # [Required] for individual card payout and bank payout destinations; not required for card payout id - # and token destinations when the name is already stored with the token or instrument; [Optional] for - # unreferenced refunds + # [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. In the card payout id and token destinations it is not required if the last name is + # already stored with the token or instrument. # max 50 characters (card payouts and unreferenced refunds) # @return [String] # @!attribute email @@ -57,8 +59,8 @@ module Common # @!attribute gender # The account holder's gender. # [Optional] - # @deprecated Not in the request schema of any account holder; the Klarna payment source account_holder - # had it until the API removed it. Still returned in the Klarna payment details source account_holder. + # @deprecated Not in the request schema of any account holder. Still returned in the Klarna payment + # details source account_holder. # Retained for backwards compatibility and will be removed in the next major version. There is no # replacement. # @return [String]