From 97cf2ce0f143ec54f1bee1a4624307ecfca98c5b Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 5 Aug 2026 08:55:51 +0200 Subject: [PATCH 1/2] ChallengeIndicator/SessionChallengeIndicator enum alignment --- lib/checkout_sdk/sessions/category.rb | 2 +- .../channel/three_ds_method_completion.rb | 6 +- lib/checkout_sdk/sessions/experience.rb | 18 ++ lib/checkout_sdk/sessions/google_spa.rb | 15 ++ .../sessions/session_challenge_indicator.rb | 31 ++++ lib/checkout_sdk/sessions/session_request.rb | 19 ++- lib/checkout_sdk/sessions/sessions.rb | 3 + .../sessions/shipping_indicator.rb | 14 +- .../sessions/source/session_scheme.rb | 2 + lib/checkout_sdk/sessions/transaction_type.rb | 2 +- .../sessions/sessions_values_spec.rb | 161 ++++++++++++++++++ 11 files changed, 260 insertions(+), 13 deletions(-) create mode 100644 lib/checkout_sdk/sessions/experience.rb create mode 100644 lib/checkout_sdk/sessions/google_spa.rb create mode 100644 lib/checkout_sdk/sessions/session_challenge_indicator.rb create mode 100644 spec/checkout_sdk/sessions/sessions_values_spec.rb diff --git a/lib/checkout_sdk/sessions/category.rb b/lib/checkout_sdk/sessions/category.rb index dc3378d..c3f70a7 100644 --- a/lib/checkout_sdk/sessions/category.rb +++ b/lib/checkout_sdk/sessions/category.rb @@ -4,7 +4,7 @@ module CheckoutSdk module Sessions module Category PAYMENT = 'payment' - NON_PAYMENT = 'nonPayment' + NON_PAYMENT = 'non_payment' end end end diff --git a/lib/checkout_sdk/sessions/channel/three_ds_method_completion.rb b/lib/checkout_sdk/sessions/channel/three_ds_method_completion.rb index 1c002a3..4a65bf2 100644 --- a/lib/checkout_sdk/sessions/channel/three_ds_method_completion.rb +++ b/lib/checkout_sdk/sessions/channel/three_ds_method_completion.rb @@ -3,9 +3,9 @@ module CheckoutSdk module Sessions module ThreeDsMethodCompletion - Y = 'y' - N = 'n' - U = 'u' + Y = 'Y' + N = 'N' + U = 'U' end end end diff --git a/lib/checkout_sdk/sessions/experience.rb b/lib/checkout_sdk/sessions/experience.rb new file mode 100644 index 0000000..7d2dc0a --- /dev/null +++ b/lib/checkout_sdk/sessions/experience.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Sessions + # The authentication experience chosen for a session. + # + # Used by {SessionRequest#preferred_experiences}, which takes an array of these values. + # + # [Optional] + module Experience + # 3D Secure authentication. The constant is named THREE_DS because a Ruby constant cannot + # begin with a digit; the wire value is '3ds'. + THREE_DS = '3ds' + # Google Secure Payment Authentication. + GOOGLE_SPA = 'google_spa' + end + end +end diff --git a/lib/checkout_sdk/sessions/google_spa.rb b/lib/checkout_sdk/sessions/google_spa.rb new file mode 100644 index 0000000..257bf4f --- /dev/null +++ b/lib/checkout_sdk/sessions/google_spa.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Sessions + # This object contains the Google SPA properties (non-hosted only). + # + # @!attribute continue_url + # @return [String] Fully qualified URL for redirecting the user's browser session after + # authentication. For example, this field may be the merchant's website for purchase + # confirmation once payment is complete. Required if in full redirect (not iframe) mode. + class GoogleSpa + attr_accessor :continue_url + end + end +end diff --git a/lib/checkout_sdk/sessions/session_challenge_indicator.rb b/lib/checkout_sdk/sessions/session_challenge_indicator.rb new file mode 100644 index 0000000..1ee96a2 --- /dev/null +++ b/lib/checkout_sdk/sessions/session_challenge_indicator.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Sessions + # Indicates whether a challenge is requested for this session. + # + # Used by {SessionRequest#challenge_indicator} for POST /sessions. This is the only field in the + # API that accepts the exemption values below; the 3ds.challenge_indicator field on payments, + # hosted payments, payment links and payment sessions accepts only the first four values and is + # modelled by {CheckoutSdk::Common::ChallengeIndicator}. + # + # The following are requests for exemption: {LOW_VALUE}, {TRUSTED_LISTING}, + # {TRUSTED_LISTING_PROMPT} and {TRANSACTION_RISK_ASSESSMENT}. If an exemption cannot be applied, + # then the value {NO_CHALLENGE_REQUESTED} will be used instead. + # + # [Optional] + # Default: {NO_PREFERENCE} + # max 50 characters + module SessionChallengeIndicator + NO_PREFERENCE = 'no_preference' + NO_CHALLENGE_REQUESTED = 'no_challenge_requested' + CHALLENGE_REQUESTED = 'challenge_requested' + CHALLENGE_REQUESTED_MANDATE = 'challenge_requested_mandate' + LOW_VALUE = 'low_value' + TRUSTED_LISTING = 'trusted_listing' + TRUSTED_LISTING_PROMPT = 'trusted_listing_prompt' + TRANSACTION_RISK_ASSESSMENT = 'transaction_risk_assessment' + DATA_SHARE = 'data_share' + end + end +end diff --git a/lib/checkout_sdk/sessions/session_request.rb b/lib/checkout_sdk/sessions/session_request.rb index f64f53b..e93c976 100644 --- a/lib/checkout_sdk/sessions/session_request.rb +++ b/lib/checkout_sdk/sessions/session_request.rb @@ -3,7 +3,7 @@ require 'checkout_sdk/sessions/authentication_type' require 'checkout_sdk/sessions/category' require 'checkout_sdk/sessions/transaction_type' -require 'checkout_sdk/common/challenge_indicator' +require 'checkout_sdk/sessions/session_challenge_indicator' module CheckoutSdk module Sessions @@ -24,15 +24,13 @@ module Sessions # @!attribute account_info # @return [CardHolderAccountInfo] # @!attribute challenge_indicator - # @return [String] {CheckoutSdk::Common::ChallengeIndicator} + # @return [String] {SessionChallengeIndicator} # @!attribute billing_descriptor # @return [SessionsBillingDescriptor] # @!attribute reference # @return [String] # @!attribute merchant_risk_info # @return [MerchantRiskInfo] - # @!attribute prior_transaction_reference - # @return [String] # @!attribute transaction_type # @return [String] {TransactionType} # @!attribute shipping_address @@ -51,6 +49,12 @@ module Sessions # @return [Optimization] # @!attribute initial_transaction # @return [InitialTransaction] + # @!attribute device_information + # @return [DeviceInformation] + # @!attribute google_spa + # @return [GoogleSpa] Google SPA properties (non-hosted only). + # @!attribute preferred_experiences + # @return [Array] The chosen experience(s) for this session. {Experience} class SessionRequest attr_accessor :source, :amount, @@ -64,7 +68,6 @@ class SessionRequest :billing_descriptor, :reference, :merchant_risk_info, - :prior_transaction_reference, :transaction_type, :shipping_address, :shipping_address_matches_billing, @@ -74,12 +77,14 @@ class SessionRequest :installment, :optimization, :initial_transaction, - :device_information + :device_information, + :google_spa, + :preferred_experiences def initialize(source: CardSource.new, authentication_type: CheckoutSdk::Sessions::AuthenticationType::REGULAR, authentication_category: CheckoutSdk::Sessions::Category::PAYMENT, - challenge_indicator: CheckoutSdk::Common::ChallengeIndicator::NO_PREFERENCE, + challenge_indicator: CheckoutSdk::Sessions::SessionChallengeIndicator::NO_PREFERENCE, transaction_type: CheckoutSdk::Sessions::TransactionType::GOODS_SERVICE) @source = source @authentication_type = authentication_type diff --git a/lib/checkout_sdk/sessions/sessions.rb b/lib/checkout_sdk/sessions/sessions.rb index 6a37d90..e8ae701 100644 --- a/lib/checkout_sdk/sessions/sessions.rb +++ b/lib/checkout_sdk/sessions/sessions.rb @@ -6,10 +6,13 @@ require 'checkout_sdk/sessions/category' require 'checkout_sdk/sessions/delivery_timeframe' require 'checkout_sdk/sessions/device_information' +require 'checkout_sdk/sessions/experience' +require 'checkout_sdk/sessions/google_spa' require 'checkout_sdk/sessions/installment' require 'checkout_sdk/sessions/merchant_risk_info' require 'checkout_sdk/sessions/recurring' require 'checkout_sdk/sessions/session_address' +require 'checkout_sdk/sessions/session_challenge_indicator' require 'checkout_sdk/sessions/session_marketplace_data' require 'checkout_sdk/sessions/session_request' require 'checkout_sdk/sessions/session_source_type' diff --git a/lib/checkout_sdk/sessions/shipping_indicator.rb b/lib/checkout_sdk/sessions/shipping_indicator.rb index acc65eb..e58b935 100644 --- a/lib/checkout_sdk/sessions/shipping_indicator.rb +++ b/lib/checkout_sdk/sessions/shipping_indicator.rb @@ -2,8 +2,20 @@ module CheckoutSdk module Sessions + # Indicates the shipping method chosen for the transaction. + # + # Used by {MerchantRiskInfo#shipping_indicator}. Choose the option that accurately describes the + # cardholder's specific transaction. + # + # [Optional] module ShippingIndicator - VISA = 'visa' + BILLING_ADDRESS = 'billing_address' + ANOTHER_ADDRESS_ON_FILE = 'another_address_on_file' + NOT_ON_FILE = 'not_on_file' + STORE_PICK_UP = 'store_pick_up' + DIGITAL_GOODS = 'digital_goods' + TRAVEL_AND_EVENT_NO_SHIPPING = 'travel_and_event_no_shipping' + OTHER = 'other' end end end diff --git a/lib/checkout_sdk/sessions/source/session_scheme.rb b/lib/checkout_sdk/sessions/source/session_scheme.rb index 6c5fcce..15cc040 100644 --- a/lib/checkout_sdk/sessions/source/session_scheme.rb +++ b/lib/checkout_sdk/sessions/source/session_scheme.rb @@ -9,6 +9,8 @@ module SessionScheme AMEX = 'amex' DINERS = 'diners' CARTES_BANCAIRES = 'cartes_bancaires' + DISCOVER = 'discover' + UPI = 'upi' end end end diff --git a/lib/checkout_sdk/sessions/transaction_type.rb b/lib/checkout_sdk/sessions/transaction_type.rb index 0a1a621..ed8788c 100644 --- a/lib/checkout_sdk/sessions/transaction_type.rb +++ b/lib/checkout_sdk/sessions/transaction_type.rb @@ -7,7 +7,7 @@ module TransactionType CHECK_ACCEPTANCE = 'check_acceptance' GOODS_SERVICE = 'goods_service' PREPAID_ACTIVATION_AND_LOAD = 'prepaid_activation_and_load' - QUASHI_CARD_TRANSACTION = 'quashi_card_transaction' + QUASI_CARD_TRANSACTION = 'quasi_card_transaction' end end end diff --git a/spec/checkout_sdk/sessions/sessions_values_spec.rb b/spec/checkout_sdk/sessions/sessions_values_spec.rb new file mode 100644 index 0000000..b5b85d7 --- /dev/null +++ b/spec/checkout_sdk/sessions/sessions_values_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +# Spec-conformance guards for the sessions value modules. +# +# These modules hold the raw wire values sent to and returned by the API, so a typo is invisible at +# development time and only fails against the live API. Each module below is asserted against the +# exact value set defined by the Checkout.com API Reference, plus a structural guard that catches +# casing mistakes across every value module in the Sessions namespace. +RSpec.describe 'Sessions value modules' do + # An API value is snake_case, or a single uppercase letter for the Y/N/U style codes. + def valid_api_value + /\A([a-z0-9_]+|[A-Z])\z/ + end + + def values_of(mod) + mod.constants.map { |name| mod.const_get(name) }.grep(String) + end + + context 'challenge indicator split' do + # Module#constants does not preserve declaration order, so the constants are named explicitly. + # This asserts both the spec order and that each constant maps to the right wire value. + it 'exposes all nine values on the sessions module, in spec order' do + mod = CheckoutSdk::Sessions::SessionChallengeIndicator + + expect( + [mod::NO_PREFERENCE, mod::NO_CHALLENGE_REQUESTED, mod::CHALLENGE_REQUESTED, + mod::CHALLENGE_REQUESTED_MANDATE, mod::LOW_VALUE, mod::TRUSTED_LISTING, + mod::TRUSTED_LISTING_PROMPT, mod::TRANSACTION_RISK_ASSESSMENT, mod::DATA_SHARE] + ).to eq( + %w[no_preference no_challenge_requested challenge_requested challenge_requested_mandate + low_value trusted_listing trusted_listing_prompt transaction_risk_assessment data_share] + ) + expect(values_of(mod).size).to eq(9) + end + + it 'keeps only the four base values on the shared payments module' do + expect(values_of(CheckoutSdk::Common::ChallengeIndicator)).to contain_exactly( + 'no_preference', 'no_challenge_requested', 'challenge_requested', 'challenge_requested_mandate' + ) + end + + it 'keeps the exemption values off the shared payments module' do + exemptions = %w[low_value trusted_listing trusted_listing_prompt transaction_risk_assessment + data_share] + shared = values_of(CheckoutSdk::Common::ChallengeIndicator) + sessions = values_of(CheckoutSdk::Sessions::SessionChallengeIndicator) + + expect(shared & exemptions).to be_empty + expect(sessions).to include(*exemptions) + end + + it 'defaults SessionRequest to no_preference' do + expect(CheckoutSdk::Sessions::SessionRequest.new.challenge_indicator).to eq('no_preference') + end + end + + context 'value modules match the spec' do + it 'Category uses snake_case non_payment, not the camelCase the API rejects' do + expect(values_of(CheckoutSdk::Sessions::Category)).to contain_exactly('payment', 'non_payment') + end + + it 'TransactionType spells quasi_card_transaction correctly' do + expect(CheckoutSdk::Sessions::TransactionType::QUASI_CARD_TRANSACTION) + .to eq('quasi_card_transaction') + expect(values_of(CheckoutSdk::Sessions::TransactionType).uniq).to contain_exactly( + 'account_funding', 'check_acceptance', 'goods_service', 'prepaid_activation_and_load', + 'quasi_card_transaction' + ) + end + + it 'ShippingIndicator covers all seven spec values and no longer holds visa' do + values = values_of(CheckoutSdk::Sessions::ShippingIndicator) + + expect(values).to contain_exactly( + 'billing_address', 'another_address_on_file', 'not_on_file', 'store_pick_up', + 'digital_goods', 'travel_and_event_no_shipping', 'other' + ) + expect(values).not_to include('visa') + end + + it 'ThreeDsMethodCompletion uses the uppercase Y/N/U the spec defines' do + expect(values_of(CheckoutSdk::Sessions::ThreeDsMethodCompletion)) + .to contain_exactly('Y', 'N', 'U') + end + + it 'SessionScheme covers all eight schemes' do + expect(values_of(CheckoutSdk::Sessions::SessionScheme)).to contain_exactly( + 'visa', 'mastercard', 'jcb', 'amex', 'diners', 'cartes_bancaires', 'discover', 'upi' + ) + end + end + + context 'SessionRequest field set' do + # The 24 properties of the SessionRequest schema in the API Reference. Guards both directions: a + # spec property missing from the SDK, and an attribute the SDK declares that the spec does not. + it 'declares exactly the spec property set' do + expected = %w[source amount currency processing_channel_id marketplace authentication_type + authentication_category account_info challenge_indicator billing_descriptor + reference merchant_risk_info transaction_type shipping_address + shipping_address_matches_billing completion channel_data recurring installment + optimization initial_transaction device_information google_spa + preferred_experiences] + + declared = CheckoutSdk::Sessions::SessionRequest.public_instance_methods(false) + .map(&:to_s) + .reject { |name| name.end_with?('=') } + + expect(declared).to match_array(expected) + expect(declared.size).to eq(24) + end + + it 'no longer declares prior_transaction_reference, which the spec does not define' do + expect(CheckoutSdk::Sessions::SessionRequest.public_instance_methods(false)) + .not_to include(:prior_transaction_reference) + end + + it 'accepts google_spa and preferred_experiences' do + google_spa = CheckoutSdk::Sessions::GoogleSpa.new + google_spa.continue_url = 'https://merchant.com/continue' + + request = CheckoutSdk::Sessions::SessionRequest.new + request.google_spa = google_spa + request.preferred_experiences = [CheckoutSdk::Sessions::Experience::THREE_DS, + CheckoutSdk::Sessions::Experience::GOOGLE_SPA] + + expect(request.google_spa.continue_url).to eq('https://merchant.com/continue') + expect(request.preferred_experiences).to eq(%w[3ds google_spa]) + end + + it 'Experience matches the spec value set' do + expect(values_of(CheckoutSdk::Sessions::Experience)).to contain_exactly('3ds', 'google_spa') + end + end + + context 'structural guard' do + # Catches camelCase or wrong casing leaking into a wire value, which is how nonPayment survived. + it 'every sessions value is snake_case or a single uppercase code' do + modules = [ + CheckoutSdk::Sessions::SessionChallengeIndicator, + CheckoutSdk::Sessions::Category, + CheckoutSdk::Sessions::TransactionType, + CheckoutSdk::Sessions::AuthenticationType, + CheckoutSdk::Sessions::ShippingIndicator, + CheckoutSdk::Sessions::SessionScheme, + CheckoutSdk::Sessions::ThreeDsMethodCompletion, + CheckoutSdk::Sessions::AuthenticationMethod, + CheckoutSdk::Sessions::DeliveryTimeframe, + CheckoutSdk::Sessions::ThreeDsReqAuthMethod, + CheckoutSdk::Sessions::SessionSourceType, + CheckoutSdk::Sessions::Experience, + CheckoutSdk::Common::ChallengeIndicator + ] + checked = modules.flat_map { |mod| values_of(mod) } + + expect(checked.size).to be > 50 + checked.each do |value| + expect(value).to match(valid_api_value), "#{value.inspect} is not a valid API value" + end + end + end +end From 9096ec30339db22da3bc0ab991eaf4697ad4e8c2 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 5 Aug 2026 09:57:43 +0200 Subject: [PATCH 2/2] Some fixes and tests --- .../sessions/channel/browser_session.rb | 9 ++++- .../sessions/source/card_source.rb | 5 +-- .../sessions/sessions_values_spec.rb | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/checkout_sdk/sessions/channel/browser_session.rb b/lib/checkout_sdk/sessions/channel/browser_session.rb index 92bcc86..8f0ba90 100644 --- a/lib/checkout_sdk/sessions/channel/browser_session.rb +++ b/lib/checkout_sdk/sessions/channel/browser_session.rb @@ -26,6 +26,11 @@ module Sessions # @return [String] # @!attribute ip_address # @return [String] + # @!attribute iframe_payment_allowed + # @return [TrueClass, FalseClass] Whether the Payment API is enabled for all parent frames. + # Required for Google SPA support in hosted sessions. + # @!attribute user_agent_client_hint + # @return [String] The raw Sec-CH-UA header value. This can improve Google SPA support. class BrowserSession < ChannelData attr_accessor :three_ds_method_completion, :accept_header, @@ -37,7 +42,9 @@ class BrowserSession < ChannelData :screen_width, :timezone, :user_agent, - :ip_address + :ip_address, + :iframe_payment_allowed, + :user_agent_client_hint def initialize(three_ds_method_completion: CheckoutSdk::Sessions::ThreeDsMethodCompletion::U) super(ChannelDataType::BROWSER) diff --git a/lib/checkout_sdk/sessions/source/card_source.rb b/lib/checkout_sdk/sessions/source/card_source.rb index e668866..dcd6429 100644 --- a/lib/checkout_sdk/sessions/source/card_source.rb +++ b/lib/checkout_sdk/sessions/source/card_source.rb @@ -12,15 +12,12 @@ module Sessions # @return [String] # @!attribute stored # @return [TrueClass, FalseClass] - # @!attribute store_for_future_use - # @return [TrueClass, FalseClass] class CardSource < SessionSource attr_accessor :number, :expiry_month, :expiry_year, :name, - :stored, - :store_for_future_use + :stored def initialize(stored: false) super(SessionSourceType::CARD) diff --git a/spec/checkout_sdk/sessions/sessions_values_spec.rb b/spec/checkout_sdk/sessions/sessions_values_spec.rb index b5b85d7..55b4e9a 100644 --- a/spec/checkout_sdk/sessions/sessions_values_spec.rb +++ b/spec/checkout_sdk/sessions/sessions_values_spec.rb @@ -132,6 +132,45 @@ def values_of(mod) end end + context 'session model field sets' do + def readers(klass) + klass.public_instance_methods(false).map(&:to_s).reject { |name| name.end_with?('=') } + end + + # The spec Browser schema has 14 properties. iframe_payment_allowed and user_agent_client_hint + # were previously missing, so Google SPA support could not be signalled. + it 'BrowserSession covers every spec field' do + declared = readers(CheckoutSdk::Sessions::BrowserSession) + + readers(CheckoutSdk::Sessions::ChannelData) + + expect(declared).to include('iframe_payment_allowed', 'user_agent_client_hint') + expect(declared).to match_array( + %w[channel three_ds_method_completion accept_header java_enabled javascript_enabled language + color_depth screen_height screen_width timezone user_agent ip_address + iframe_payment_allowed user_agent_client_hint] + ) + end + + # The sessions CardSource schema has no store_for_future_use; that field belongs to the payments + # sources, which create an instrument. + it 'CardSource does not expose store_for_future_use' do + declared = readers(CheckoutSdk::Sessions::CardSource) + + readers(CheckoutSdk::Sessions::SessionSource) + + expect(declared).not_to include('store_for_future_use') + expect(declared).to match_array( + %w[type scheme billing_address home_phone mobile_phone work_phone email + number expiry_month expiry_year name stored] + ) + end + + # All 20 properties of the spec CardholderAccountInfo schema must be accessible, not merely + # documented. + it 'CardholderAccountInfo exposes all twenty spec properties' do + expect(readers(CheckoutSdk::Sessions::CardholderAccountInfo).size).to eq(20) + end + end + context 'structural guard' do # Catches camelCase or wrong casing leaking into a wire value, which is how nonPayment survived. it 'every sessions value is snake_case or a single uppercase code' do