From a7d226d4d84a578bcaa6ab01fcfa4d9d82f1fc3a Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 21 Sep 2026 17:18:49 +0200 Subject: [PATCH 1/2] Add verification attempt-assets endpoints and card scheduled_activation_date --- lib/checkout_sdk/api_client.rb | 47 +++- .../address_document_verification_client.rb | 35 ++- .../address_document_verification_request.rb | 16 +- .../face_authentication_attempt_request.rb | 15 +- .../face_authentication_client.rb | 16 +- .../face_authentication_request.rb | 19 ++ .../id_document_verification_client.rb | 36 ++- .../id_document_verification_request.rb | 30 ++ lib/checkout_sdk/identities/identities.rb | 8 + .../identity_verification_attempt_request.rb | 16 +- .../identity_verification_client.rb | 16 +- .../identity_verification_request.rb | 29 +- .../idv_client_information.rb | 20 +- .../idv_declared_data.rb | 19 +- .../idv_identity_client_information.rb | 30 ++ .../idv_identity_declared_data.rb | 34 +++ lib/checkout_sdk/identities/idv_address.rb | 48 ++++ .../idv_attempt_assets_query_filter.rb | 24 ++ .../identities/idv_attempts_query_filter.rb | 24 ++ .../identities/idv_phone_number.rb | 27 ++ .../issuing/card_update_headers.rb | 32 +++ lib/checkout_sdk/issuing/issuing.rb | 1 + lib/checkout_sdk/issuing/issuing_client.rb | 12 +- .../issuing/update_card_request.rb | 39 ++- spec/checkout_sdk/api_client_spec.rb | 73 ++++- ...dress_document_verification_client_spec.rb | 2 +- ..._document_verification_integration_spec.rb | 27 ++ .../face_authentication_client_spec.rb | 2 +- .../id_document_verification_client_spec.rb | 2 +- ..._document_verification_integration_spec.rb | 27 ++ .../identities_assets_pagination_spec.rb | 164 +++++++++++ .../identities_serialization_spec.rb | 263 ++++++++++++++++++ .../identity_verification_client_spec.rb | 2 +- .../issuing_cards_serialization_spec.rb | 87 ++++++ .../issuing_disputes_serialization_spec.rb | 15 +- .../issuing/issuing_extras_spec.rb | 2 +- 36 files changed, 1188 insertions(+), 71 deletions(-) create mode 100644 lib/checkout_sdk/identities/identity_verification/idv_identity_client_information.rb create mode 100644 lib/checkout_sdk/identities/identity_verification/idv_identity_declared_data.rb create mode 100644 lib/checkout_sdk/identities/idv_address.rb create mode 100644 lib/checkout_sdk/identities/idv_attempt_assets_query_filter.rb create mode 100644 lib/checkout_sdk/identities/idv_attempts_query_filter.rb create mode 100644 lib/checkout_sdk/identities/idv_phone_number.rb create mode 100644 lib/checkout_sdk/issuing/card_update_headers.rb create mode 100644 spec/checkout_sdk/identities/identities_assets_pagination_spec.rb create mode 100644 spec/checkout_sdk/identities/identities_serialization_spec.rb create mode 100644 spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb diff --git a/lib/checkout_sdk/api_client.rb b/lib/checkout_sdk/api_client.rb index 84eb630..bc513fc 100644 --- a/lib/checkout_sdk/api_client.rb +++ b/lib/checkout_sdk/api_client.rb @@ -26,8 +26,8 @@ def invoke_put(path, authorization, request, headers = nil) invoke(:put, path, authorization, request, nil, params: nil, extra_headers: headers) end - def invoke_patch(path, authorization, request = nil) - invoke(:patch, path, authorization, request, nil, params: nil) + def invoke_patch(path, authorization, request = nil, headers = nil) + invoke(:patch, path, authorization, request, nil, params: nil, extra_headers: headers) end def invoke_delete(path, authorization) @@ -66,18 +66,45 @@ def default_headers(authorization) Authorization: authorization.authorization_header } end - # Map a typed headers container (e.g. {CheckoutSdk::Common::Headers}) onto the underlying - # HTTP header hash. Each attribute that's set is emitted as the corresponding - # canonical HTTP header. + # Attribute name to HTTP header name, for every typed headers container the SDK supports. + # + # The header names are case sensitive in the spec: return-encrypted-cvv is lower case where a + # naive conversion would produce Return-Encrypted-Cvv. + # + # The values are Strings, not Symbols, and that is load bearing. Faraday::Utils::Headers + # preserves a String key verbatim but rewrites a Symbol key as + # `key.to_s.split('_').map(&:capitalize).join('-')`, so :'return-encrypted-cvv' would reach + # the wire as Return-encrypted-cvv and :'Encryption-Key' as Encryption-key. Strings are the + # only way to send the spelling the spec asks for. As a side effect this also stops If-Match + # being sent as If-match, which it was before. + EXTRA_HEADER_NAMES = { + if_match: 'If-Match', + accept: 'Accept', + return_encrypted_cvv: 'return-encrypted-cvv', + encryption_key: 'Encryption-Key' + }.freeze + private_constant :EXTRA_HEADER_NAMES + + # Map a typed headers container (e.g. {CheckoutSdk::Common::Headers}, + # {CheckoutSdk::Issuing::CardUpdateHeaders}) onto the underlying HTTP header hash. Each + # attribute that is set is emitted as the corresponding HTTP header. + # + # Previously a hand-written if/return chain. That shape had a latent trap: it ended with + # `return unless extra_headers.respond_to?(:accept) && extra_headers.accept`, so any header + # handled after that point was dead code for every container without an `accept` attribute. + # Iterating an explicit map removes the trap and keeps the existing If-Match and Accept + # behaviour byte for byte. def apply_extra_headers(http_headers, extra_headers) return if extra_headers.nil? - if extra_headers.respond_to?(:if_match) && extra_headers.if_match - http_headers[:'If-Match'] = extra_headers.if_match - end - return unless extra_headers.respond_to?(:accept) && extra_headers.accept + EXTRA_HEADER_NAMES.each do |attribute, header_name| + next unless extra_headers.respond_to?(attribute) + + value = extra_headers.public_send(attribute) + next if value.nil? || value.to_s.empty? - http_headers[:Accept] = extra_headers.accept + http_headers[header_name] = value + end end def append_params(path, input_params) diff --git a/lib/checkout_sdk/identities/address_document_verification/address_document_verification_client.rb b/lib/checkout_sdk/identities/address_document_verification/address_document_verification_client.rb index 671c373..b9710ce 100644 --- a/lib/checkout_sdk/identities/address_document_verification/address_document_verification_client.rb +++ b/lib/checkout_sdk/identities/address_document_verification/address_document_verification_client.rb @@ -9,7 +9,8 @@ class AddressDocumentVerificationClient < Client ANONYMIZE = 'anonymize' ATTEMPTS = 'attempts' PDF_REPORT = 'pdf-report' - private_constant :ADDRESS_DOCUMENT_VERIFICATIONS, :ANONYMIZE, :ATTEMPTS, :PDF_REPORT + ASSETS = 'assets' + private_constant :ADDRESS_DOCUMENT_VERIFICATIONS, :ANONYMIZE, :ATTEMPTS, :PDF_REPORT, :ASSETS # @param [ApiClient] api_client # @param [CheckoutConfiguration] configuration @@ -49,11 +50,19 @@ def create_address_document_verification_attempt(address_document_verification_i ) end + # Get the details of all attempts for a specific address document verification. + # + # Results are paginated. Beta. + # # @param [String] address_document_verification_id - def get_address_document_verification_attempts(address_document_verification_id) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated attempt list + def get_address_document_verification_attempts(address_document_verification_id, query = nil) api_client.invoke_get( build_path(ADDRESS_DOCUMENT_VERIFICATIONS, address_document_verification_id, ATTEMPTS), - sdk_authorization + sdk_authorization, + query ) end @@ -73,6 +82,26 @@ def get_address_document_verification_pdf_report(address_document_verification_i sdk_authorization ) end + + # Get the assets (the document image) uploaded for a address document verification attempt. + # + # Results are paginated. Beta. + # + # @param [String] address_document_verification_id + # @param [String] attempt_id + # @param [Hash, CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated asset list + def get_address_document_verification_attempt_assets(address_document_verification_id, + attempt_id, + query = nil) + api_client.invoke_get( + build_path(ADDRESS_DOCUMENT_VERIFICATIONS, address_document_verification_id, + ATTEMPTS, attempt_id, ASSETS), + sdk_authorization, + query + ) + end end end end diff --git a/lib/checkout_sdk/identities/address_document_verification/address_document_verification_request.rb b/lib/checkout_sdk/identities/address_document_verification/address_document_verification_request.rb index 67fb474..7fec96b 100644 --- a/lib/checkout_sdk/identities/address_document_verification/address_document_verification_request.rb +++ b/lib/checkout_sdk/identities/address_document_verification/address_document_verification_request.rb @@ -6,11 +6,21 @@ module AddressDocumentVerification # Request body for POST /address-document-verifications. # # @!attribute applicant_id - # @return [String] The applicant's unique identifier. [Required] Pattern: ^aplt_\w+$ + # The applicant's unique identifier. + # [Required] + # ^aplt_\w+$ + # @return [String] # @!attribute user_journey_id - # @return [String] Your configuration ID. [Required] Pattern: ^usj_[a-z2-7]{26}$ + # Your configuration ID. + # [Required] + # ^usj_[a-z2-7]{26}$ + # @return [String] # @!attribute declared_data - # @return [IdvDeclaredData, Hash] The personal details provided by the applicant. [Optional] + # The personal details provided by the applicant. The address document verification + # request takes the narrower two field IdvDeclaredData shape, not the identity + # verification one. + # [Optional] + # @return [CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData] class AddressDocumentVerificationRequest attr_accessor :applicant_id, :user_journey_id, diff --git a/lib/checkout_sdk/identities/face_authentication/face_authentication_attempt_request.rb b/lib/checkout_sdk/identities/face_authentication/face_authentication_attempt_request.rb index d738b90..a6dc692 100644 --- a/lib/checkout_sdk/identities/face_authentication/face_authentication_attempt_request.rb +++ b/lib/checkout_sdk/identities/face_authentication/face_authentication_attempt_request.rb @@ -6,11 +6,22 @@ module FaceAuthentication # Request body for POST /face-authentications/{id}/attempts. # # @!attribute redirect_url - # @return [String] URL to redirect the applicant after the attempt. + # The URL to redirect the applicant to after the attempt. + # [Required] + # Format: uri + # @return [String] + # @!attribute phone_number + # The applicant's mobile phone number, if sharing the attempt URL via SMS. + # [Optional] + # @return [CheckoutSdk::Identities::IdvPhoneNumber] # @!attribute client_information + # The applicant's details. The face authentication attempt takes the narrower + # FavClientInformation shape, which declares neither document field, so this is + # deliberately the parent class and not IdvIdentityClientInformation. + # [Optional] # @return [CheckoutSdk::Identities::IdentityVerification::IdvClientInformation] class FaceAuthenticationAttemptRequest - attr_accessor :redirect_url, :client_information + attr_accessor :redirect_url, :phone_number, :client_information end end end diff --git a/lib/checkout_sdk/identities/face_authentication/face_authentication_client.rb b/lib/checkout_sdk/identities/face_authentication/face_authentication_client.rb index 3fc8612..2a1ab5e 100644 --- a/lib/checkout_sdk/identities/face_authentication/face_authentication_client.rb +++ b/lib/checkout_sdk/identities/face_authentication/face_authentication_client.rb @@ -47,11 +47,19 @@ def create_face_authentication_attempt(face_authentication_id, attempt_request) ) end + # Get the details of all attempts for a specific face authentication. + # + # Results are paginated. Beta. + # # @param [String] face_authentication_id - def get_face_authentication_attempts(face_authentication_id) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated attempt list + def get_face_authentication_attempts(face_authentication_id, query = nil) api_client.invoke_get( build_path(FACE_AUTHENTICATIONS, face_authentication_id, ATTEMPTS), - sdk_authorization + sdk_authorization, + query ) end @@ -67,8 +75,8 @@ def get_face_authentication_attempt(face_authentication_id, attempt_id) # Retrieve the assets (face images and videos) captured during a face authentication attempt. # @param [String] face_authentication_id # @param [String] attempt_id - # @param [Hash, nil] query pagination query parameters; supports :skip (Integer, default: 0) - # and :limit (Integer, default: 10) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) def get_face_authentication_attempt_assets(face_authentication_id, attempt_id, query = nil) api_client.invoke_get( build_path(FACE_AUTHENTICATIONS, face_authentication_id, ATTEMPTS, attempt_id, ASSETS), diff --git a/lib/checkout_sdk/identities/face_authentication/face_authentication_request.rb b/lib/checkout_sdk/identities/face_authentication/face_authentication_request.rb index fecea9a..68ce606 100644 --- a/lib/checkout_sdk/identities/face_authentication/face_authentication_request.rb +++ b/lib/checkout_sdk/identities/face_authentication/face_authentication_request.rb @@ -5,14 +5,33 @@ module Identities module FaceAuthentication # Request body for POST /face-authentications. # + # Realigned with the spec on 2026-09-21. This class omitted user_journey_id, which + # FavFaceAuthentication declares and requires, and declared external_reference and + # redirect_url, which it does not. The two invented attributes are retained rather than + # removed, so no caller breaks, and are scheduled for removal in a future major; the API + # ignores them. + # # @!attribute applicant_id + # The applicant's unique identifier. + # [Required] + # ^aplt_\w+$ + # @return [String] + # @!attribute user_journey_id + # Your configuration ID. + # [Required] + # ^usj_[a-z2-7]{26}$ # @return [String] # @!attribute external_reference + # @deprecated Not declared by FavFaceAuthentication. Never sent by the API and ignored if + # supplied. Scheduled for removal in a future major. # @return [String] # @!attribute redirect_url + # @deprecated Not declared by FavFaceAuthentication. The redirect URL belongs on the + # attempt request, not the face authentication. Scheduled for removal in a future major. # @return [String] class FaceAuthenticationRequest attr_accessor :applicant_id, + :user_journey_id, :external_reference, :redirect_url end diff --git a/lib/checkout_sdk/identities/id_document_verification/id_document_verification_client.rb b/lib/checkout_sdk/identities/id_document_verification/id_document_verification_client.rb index 1780514..83a2cce 100644 --- a/lib/checkout_sdk/identities/id_document_verification/id_document_verification_client.rb +++ b/lib/checkout_sdk/identities/id_document_verification/id_document_verification_client.rb @@ -9,7 +9,8 @@ class IdDocumentVerificationClient < Client ANONYMIZE = 'anonymize' ATTEMPTS = 'attempts' PDF_REPORT = 'pdf-report' - private_constant :ID_DOCUMENT_VERIFICATIONS, :ANONYMIZE, :ATTEMPTS, :PDF_REPORT + ASSETS = 'assets' + private_constant :ID_DOCUMENT_VERIFICATIONS, :ANONYMIZE, :ATTEMPTS, :PDF_REPORT, :ASSETS # @param [ApiClient] api_client # @param [CheckoutConfiguration] configuration @@ -47,11 +48,19 @@ def create_id_document_verification_attempt(id_document_verification_id, attempt ) end + # Get the details of all attempts for a specific ID document verification. + # + # Results are paginated. Beta. + # # @param [String] id_document_verification_id - def get_id_document_verification_attempts(id_document_verification_id) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated attempt list + def get_id_document_verification_attempts(id_document_verification_id, query = nil) api_client.invoke_get( build_path(ID_DOCUMENT_VERIFICATIONS, id_document_verification_id, ATTEMPTS), - sdk_authorization + sdk_authorization, + query ) end @@ -71,6 +80,27 @@ def get_id_document_verification_pdf_report(id_document_verification_id) sdk_authorization ) end + + # Get the assets (the front and back images of the document) uploaded for an ID document + # verification attempt. + # + # Results are paginated. Beta. + # + # @param [String] id_document_verification_id + # @param [String] attempt_id + # @param [Hash, CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated asset list + def get_id_document_verification_attempt_assets(id_document_verification_id, + attempt_id, + query = nil) + api_client.invoke_get( + build_path(ID_DOCUMENT_VERIFICATIONS, id_document_verification_id, + ATTEMPTS, attempt_id, ASSETS), + sdk_authorization, + query + ) + end end end end diff --git a/lib/checkout_sdk/identities/id_document_verification/id_document_verification_request.rb b/lib/checkout_sdk/identities/id_document_verification/id_document_verification_request.rb index 45e0a34..29e536e 100644 --- a/lib/checkout_sdk/identities/id_document_verification/id_document_verification_request.rb +++ b/lib/checkout_sdk/identities/id_document_verification/id_document_verification_request.rb @@ -5,18 +5,48 @@ module Identities module IdDocumentVerification # Request body for POST /id-document-verifications. # + # Realigned with the spec on 2026-09-21. This class previously declared + # external_reference, document_type, country and redirect_url, none of which + # IddvIdDocumentVerification declares, and omitted user_journey_id and declared_data, which + # it does. The four invented attributes are retained rather than removed, so no caller + # breaks, and are scheduled for removal in a future major; the API ignores them. + # # @!attribute applicant_id + # The applicant's unique identifier. + # [Required] + # ^aplt_\w+$ + # @return [String] + # @!attribute user_journey_id + # Your configuration ID. + # [Optional] + # ^usj_[a-z2-7]{26}$ # @return [String] + # @!attribute declared_data + # The personal details provided by the applicant. The ID document verification request + # takes the narrower two field IdvDeclaredData shape. + # [Optional] + # @return [CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData] # @!attribute external_reference + # @deprecated Not declared by IddvIdDocumentVerification. Never sent by the API and + # ignored if supplied. Scheduled for removal in a future major. # @return [String] # @!attribute document_type + # @deprecated Not declared by IddvIdDocumentVerification. The document type is chosen by + # the applicant during the attempt and returned on the response, not sent on the + # request. Scheduled for removal in a future major. # @return [String] # @!attribute country + # @deprecated Not declared by IddvIdDocumentVerification. Scheduled for removal in a + # future major. # @return [String] # @!attribute redirect_url + # @deprecated Not declared by IddvIdDocumentVerification. The redirect URL belongs on the + # attempt request, not the verification. Scheduled for removal in a future major. # @return [String] class IdDocumentVerificationRequest attr_accessor :applicant_id, + :user_journey_id, + :declared_data, :external_reference, :document_type, :country, diff --git a/lib/checkout_sdk/identities/identities.rb b/lib/checkout_sdk/identities/identities.rb index d4250ae..bcdc7cc 100644 --- a/lib/checkout_sdk/identities/identities.rb +++ b/lib/checkout_sdk/identities/identities.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +# Shared identities types +require 'checkout_sdk/identities/idv_phone_number' +require 'checkout_sdk/identities/idv_address' +require 'checkout_sdk/identities/idv_attempts_query_filter' +require 'checkout_sdk/identities/idv_attempt_assets_query_filter' + # Applicants require 'checkout_sdk/identities/applicants/create_applicant_request' require 'checkout_sdk/identities/applicants/update_applicant_request' @@ -21,7 +27,9 @@ # Identity Verification require 'checkout_sdk/identities/identity_verification/idv_declared_data' +require 'checkout_sdk/identities/identity_verification/idv_identity_declared_data' require 'checkout_sdk/identities/identity_verification/idv_client_information' +require 'checkout_sdk/identities/identity_verification/idv_identity_client_information' require 'checkout_sdk/identities/identity_verification/identity_verification_attempt_request' require 'checkout_sdk/identities/identity_verification/identity_verification_request' require 'checkout_sdk/identities/identity_verification/identity_verification_client' diff --git a/lib/checkout_sdk/identities/identity_verification/identity_verification_attempt_request.rb b/lib/checkout_sdk/identities/identity_verification/identity_verification_attempt_request.rb index 71a285d..0e29a9e 100644 --- a/lib/checkout_sdk/identities/identity_verification/identity_verification_attempt_request.rb +++ b/lib/checkout_sdk/identities/identity_verification/identity_verification_attempt_request.rb @@ -6,11 +6,21 @@ module IdentityVerification # Request body for POST /identity-verifications/{id}/attempts. # # @!attribute redirect_url - # @return [String] URL to redirect the applicant after the attempt. + # The URL to redirect the applicant to after the attempt. + # [Required] + # Format: uri + # @return [String] + # @!attribute phone_number + # The applicant's mobile phone number, if sharing the attempt URL via SMS. + # [Optional] + # @return [CheckoutSdk::Identities::IdvPhoneNumber] # @!attribute client_information - # @return [IdvClientInformation] + # The applicant's details. The identity verification attempt accepts the wider + # IdvClientInformation shape, unlike the face authentication attempt. + # [Optional] + # @return [IdvIdentityClientInformation] class IdentityVerificationAttemptRequest - attr_accessor :redirect_url, :client_information + attr_accessor :redirect_url, :phone_number, :client_information end end end diff --git a/lib/checkout_sdk/identities/identity_verification/identity_verification_client.rb b/lib/checkout_sdk/identities/identity_verification/identity_verification_client.rb index 6adf66e..e7080d3 100644 --- a/lib/checkout_sdk/identities/identity_verification/identity_verification_client.rb +++ b/lib/checkout_sdk/identities/identity_verification/identity_verification_client.rb @@ -55,11 +55,19 @@ def create_identity_verification_attempt(identity_verification_id, attempt_reque ) end + # Get the details of all attempts for a specific identity verification. + # + # Results are paginated. Beta. + # # @param [String] identity_verification_id - def get_identity_verification_attempts(identity_verification_id) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) + # @return [OpenStruct] the paginated attempt list + def get_identity_verification_attempts(identity_verification_id, query = nil) api_client.invoke_get( build_path(IDENTITY_VERIFICATIONS, identity_verification_id, ATTEMPTS), - sdk_authorization + sdk_authorization, + query ) end @@ -84,8 +92,8 @@ def get_identity_verification_pdf_report(identity_verification_id) # identity verification attempt. # @param [String] identity_verification_id # @param [String] attempt_id - # @param [Hash, nil] query pagination query parameters; supports :skip (Integer, default: 0) - # and :limit (Integer, default: 10) + # @param [Hash, CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter, nil] query pagination + # query parameters; supports :skip (Integer, default: 0) and :limit (Integer, default: 10) def get_identity_verification_attempt_assets(identity_verification_id, attempt_id, query = nil) api_client.invoke_get( build_path(IDENTITY_VERIFICATIONS, identity_verification_id, ATTEMPTS, attempt_id, ASSETS), diff --git a/lib/checkout_sdk/identities/identity_verification/identity_verification_request.rb b/lib/checkout_sdk/identities/identity_verification/identity_verification_request.rb index 0097605..5efeec8 100644 --- a/lib/checkout_sdk/identities/identity_verification/identity_verification_request.rb +++ b/lib/checkout_sdk/identities/identity_verification/identity_verification_request.rb @@ -6,15 +6,36 @@ module IdentityVerification # Request body for POST /identity-verifications and POST /create-and-open-idv. # # @!attribute applicant_id - # @return [String] The applicant's unique identifier (e.g. "aplt_..."). + # The applicant's unique identifier. + # [Required] + # ^aplt_\w+$ + # @return [String] # @!attribute user_journey_id - # @return [String] Your configuration ID (e.g. "usj_..."). + # Your configuration ID. + # [Optional] + # ^usj_[a-z2-7]{26}$ + # @return [String] # @!attribute declared_data - # @return [IdvDeclaredData] + # The personal details provided by the applicant. The identity verification requests take + # the five field IdvIdentityDeclaredData shape. + # [Required] + # @return [IdvIdentityDeclaredData] # @!attribute risk_labels + # One or more codes that provide more information about risks associated with the + # verification. + # + # The request schema declares this readOnly field, so carrying it here is spec faithful. + # It cannot be sent and will be ignored by the API; read it from the response instead. + # [Optional] # @return [Array] + # @!attribute redirect_url + # The URL to redirect the applicant to after the attempt. Accepted by + # POST /create-and-open-idv only, which opens the first attempt in the same call. + # [Optional] + # Format: uri + # @return [String] class IdentityVerificationRequest - attr_accessor :applicant_id, :user_journey_id, :declared_data, :risk_labels + attr_accessor :applicant_id, :user_journey_id, :declared_data, :risk_labels, :redirect_url end end end diff --git a/lib/checkout_sdk/identities/identity_verification/idv_client_information.rb b/lib/checkout_sdk/identities/identity_verification/idv_client_information.rb index 0dbd385..8e1dc4c 100644 --- a/lib/checkout_sdk/identities/identity_verification/idv_client_information.rb +++ b/lib/checkout_sdk/identities/identity_verification/idv_client_information.rb @@ -3,12 +3,26 @@ module CheckoutSdk module Identities module IdentityVerification - # Pre-populated client information for an identity-verification attempt. + # The applicant's details for a face authentication attempt. + # + # Maps FavClientInformation. Deliberately smaller than + # {IdvIdentityClientInformation}, which adds the two fields only the identity verification + # attempt accepts. Shared with the face authentication attempt, so do not add IDV only + # fields here. # # @!attribute pre_selected_residence_country - # @return [String] ISO alpha-2 country code. + # The applicant's residence country. + # [Optional] + # Standard: ISO 3166-1 alpha-2 country code + # ^[A-Z]{2} + # Example: FR + # @return [String] a value of {CheckoutSdk::Common::Country} # @!attribute pre_selected_language - # @return [String] IETF BCP 47 language tag. + # The language you want to use for the user interface. + # [Optional] + # Format: IETF BCP 47 language tag + # Example: en-US + # @return [String] class IdvClientInformation attr_accessor :pre_selected_residence_country, :pre_selected_language end diff --git a/lib/checkout_sdk/identities/identity_verification/idv_declared_data.rb b/lib/checkout_sdk/identities/identity_verification/idv_declared_data.rb index 236325c..11551b2 100644 --- a/lib/checkout_sdk/identities/identity_verification/idv_declared_data.rb +++ b/lib/checkout_sdk/identities/identity_verification/idv_declared_data.rb @@ -3,12 +3,25 @@ module CheckoutSdk module Identities module IdentityVerification - # The personal details provided by the applicant for an identity verification. + # The personal details provided by the applicant. + # + # Maps IdvDeclaredData, the shape the address document and ID document verification requests + # accept. The identity verification requests take the larger {IdvIdentityDeclaredData}. # # @!attribute name - # @return [String] The applicant's name (max 255 characters). + # The applicant's name. + # [Required] + # min 2 characters, max 255 characters + # Example: Hannah Bret + # @return [String] + # @!attribute birth_date + # The applicant's birth date. + # [Optional] + # Format: yyyy-MM-dd + # Example: 1994-10-15 + # @return [String] class IdvDeclaredData - attr_accessor :name + attr_accessor :name, :birth_date end end end diff --git a/lib/checkout_sdk/identities/identity_verification/idv_identity_client_information.rb b/lib/checkout_sdk/identities/identity_verification/idv_identity_client_information.rb new file mode 100644 index 0000000..48ca6c7 --- /dev/null +++ b/lib/checkout_sdk/identities/identity_verification/idv_identity_client_information.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + module IdentityVerification + # The applicant's details for an identity verification attempt. + # + # Maps IdvClientInformation, a strict superset of FavClientInformation, so it inherits from + # {IdvClientInformation}. The face authentication attempt must keep using the parent: its + # schema declares neither document field, so sending them there would be a request the API + # rejects. + # + # @!attribute pre_selected_document_issuing_country + # The country that issued the applicant's identity document. + # [Optional] + # Standard: ISO 3166-1 alpha-2 country code + # ^[A-Z]{2} + # Example: FR + # @return [String] a value of {CheckoutSdk::Common::Country} + # @!attribute pre_selected_document_type + # The type of identity document the applicant uses for the attempt. + # [Optional] + # Enum: "Driving licence" "ID" "Other" "Passport" "Residence Permit" "Travel Document" "Visa" + # @return [String] + class IdvIdentityClientInformation < IdvClientInformation + attr_accessor :pre_selected_document_issuing_country, :pre_selected_document_type + end + end + end +end diff --git a/lib/checkout_sdk/identities/identity_verification/idv_identity_declared_data.rb b/lib/checkout_sdk/identities/identity_verification/idv_identity_declared_data.rb new file mode 100644 index 0000000..4aedede --- /dev/null +++ b/lib/checkout_sdk/identities/identity_verification/idv_identity_declared_data.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + module IdentityVerification + # The personal details provided by the applicant for an identity verification. + # + # Maps IdvIdentityDeclaredData, a strict superset of IdvDeclaredData, so it inherits from it. + # The address document and ID document verification requests take the smaller + # {IdvDeclaredData} shape instead: sending these three extra fields there would be a request + # the API rejects. + # + # @!attribute phone_number + # The applicant's mobile phone number, if sharing the attempt URL via SMS. + # [Optional] + # @return [CheckoutSdk::Identities::IdvPhoneNumber] + # @!attribute email + # The applicant's email address. Explicitly nullable in the spec, so the API may return + # null for it rather than omitting it. + # [Optional] + # Format: email + # Nullable: true + # Example: hannah.bret@example.com + # @return [String] + # @!attribute address + # The applicant's address. + # [Optional] + # @return [CheckoutSdk::Identities::IdvAddress] + class IdvIdentityDeclaredData < IdvDeclaredData + attr_accessor :phone_number, :email, :address + end + end + end +end diff --git a/lib/checkout_sdk/identities/idv_address.rb b/lib/checkout_sdk/identities/idv_address.rb new file mode 100644 index 0000000..af883d1 --- /dev/null +++ b/lib/checkout_sdk/identities/idv_address.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + # The applicant's address. + # + # @!attribute address_line1 + # The first line of the address. + # [Optional] + # max 250 characters + # Example: 123 Main Street + # @return [String] + # @!attribute address_line2 + # The second line of the address. + # [Optional] + # max 250 characters + # Example: Apt 4B + # @return [String] + # @!attribute city + # The city or town. + # [Optional] + # max 50 characters + # Example: London + # @return [String] + # @!attribute state + # The state, county, or province. + # [Optional] + # max 50 characters + # Example: Greater London + # @return [String] + # @!attribute zip + # The postal or ZIP code. + # [Optional] + # max 50 characters + # Example: SW1A 1AA + # @return [String] + # @!attribute country + # The two-letter ISO country code of the address. + # [Optional] + # Standard: ISO 3166-1 alpha-2 country code + # max 2 characters + # Example: GB + # @return [String] a value of {CheckoutSdk::Common::Country} + class IdvAddress + attr_accessor :address_line1, :address_line2, :city, :state, :zip, :country + end + end +end diff --git a/lib/checkout_sdk/identities/idv_attempt_assets_query_filter.rb b/lib/checkout_sdk/identities/idv_attempt_assets_query_filter.rb new file mode 100644 index 0000000..fbb9ee4 --- /dev/null +++ b/lib/checkout_sdk/identities/idv_attempt_assets_query_filter.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + # Pagination for the attempt-assets endpoints. + # + # Separate from {IdvAttemptsQueryFilter} because the spec documents the two sets of parameters + # against different resources: these count assets, those count attempts. + # + # @!attribute skip + # The number of assets to skip. + # [Optional] + # Default: 0 + # @return [Integer] + # @!attribute limit + # The maximum number of assets to return. + # [Optional] + # Default: 10 + # @return [Integer] + class IdvAttemptAssetsQueryFilter + attr_accessor :skip, :limit + end + end +end diff --git a/lib/checkout_sdk/identities/idv_attempts_query_filter.rb b/lib/checkout_sdk/identities/idv_attempts_query_filter.rb new file mode 100644 index 0000000..b49024a --- /dev/null +++ b/lib/checkout_sdk/identities/idv_attempts_query_filter.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + # Pagination for the list-attempts endpoints. + # + # Separate from {IdvAttemptAssetsQueryFilter} because the spec documents the two sets of + # parameters against different resources: these count attempts, those count assets. + # + # @!attribute skip + # The number of attempts to skip. + # [Optional] + # Default: 0 + # @return [Integer] + # @!attribute limit + # The maximum number of attempts to return. + # [Optional] + # Default: 10 + # @return [Integer] + class IdvAttemptsQueryFilter + attr_accessor :skip, :limit + end + end +end diff --git a/lib/checkout_sdk/identities/idv_phone_number.rb b/lib/checkout_sdk/identities/idv_phone_number.rb new file mode 100644 index 0000000..d93435c --- /dev/null +++ b/lib/checkout_sdk/identities/idv_phone_number.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Identities + # The applicant's mobile phone number, if sharing the attempt URL via SMS. + # + # Named IdvPhoneNumber rather than PhoneNumber because + # {CheckoutSdk::Issuing::IssuingPhoneNumber} already exists for a different shape, and the two + # must not be confused. + # + # @!attribute country_code + # The international phone country code. This is a dialling prefix, not an ISO country code. + # [Required] + # ^\+(\d+)$ + # Example: +33 + # @return [String] + # @!attribute number + # The applicant's mobile number, without the country code. + # [Required] + # ^\d{1,14}$ + # Example: 5555550102 + # @return [String] + class IdvPhoneNumber + attr_accessor :country_code, :number + end + end +end diff --git a/lib/checkout_sdk/issuing/card_update_headers.rb b/lib/checkout_sdk/issuing/card_update_headers.rb new file mode 100644 index 0000000..4e32316 --- /dev/null +++ b/lib/checkout_sdk/issuing/card_update_headers.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module CheckoutSdk + module Issuing + # The optional HTTP headers accepted when updating a card's details. + # + # Modelled as its own class rather than added to {CheckoutSdk::Common::Headers} because these + # two headers belong to one endpoint, and Common::Headers is shared by every client. + # + # @!attribute return_encrypted_cvv + # Set to "true" to retrieve the card's encrypted credentials in the response. Requires an + # RSA public key in the Encryption-Key header, otherwise the API answers 422 with error code + # encryption_key_required. + # + # A String rather than a boolean: the value is emitted verbatim as an HTTP header, and "true" + # is what the spec shows. + # [Optional] + # Maps to HTTP header return-encrypted-cvv. + # Example: "true" + # @return [String] + # @!attribute encryption_key + # The RSA public key used to encrypt returned credentials. Required when + # return_encrypted_cvv is set. Provide the public key with the BEGIN PUBLIC KEY and END + # PUBLIC KEY headers and any newline characters removed, encoded as Base64. + # [Optional] + # Maps to HTTP header Encryption-Key. + # @return [String] + class CardUpdateHeaders + attr_accessor :return_encrypted_cvv, :encryption_key + end + end +end diff --git a/lib/checkout_sdk/issuing/issuing.rb b/lib/checkout_sdk/issuing/issuing.rb index e59f99d..7368b37 100644 --- a/lib/checkout_sdk/issuing/issuing.rb +++ b/lib/checkout_sdk/issuing/issuing.rb @@ -9,6 +9,7 @@ require 'checkout_sdk/issuing/issuing_phone_number' require 'checkout_sdk/issuing/issuing_address' require 'checkout_sdk/issuing/update_cardholder_request' +require 'checkout_sdk/issuing/card_update_headers' require 'checkout_sdk/issuing/update_card_request' # Simulations diff --git a/lib/checkout_sdk/issuing/issuing_client.rb b/lib/checkout_sdk/issuing/issuing_client.rb index 4c35735..a68b3e5 100644 --- a/lib/checkout_sdk/issuing/issuing_client.rb +++ b/lib/checkout_sdk/issuing/issuing_client.rb @@ -85,13 +85,21 @@ def get_card_details(card_id) api_client.invoke_get(build_path(ISSUING, CARDS, card_id), sdk_authorization) end + # Update the details of an issued card. Only the fields for which you provide values are + # updated. + # # @param [String] card_id # @param [Hash, UpdateCardRequest] update_card_request - def update_card(card_id, update_card_request) + # @param [CardUpdateHeaders, nil] headers the optional return-encrypted-cvv and + # Encryption-Key HTTP headers. Setting return_encrypted_cvv without encryption_key returns + # a 422 with error code encryption_key_required. + # @return [OpenStruct] the update response, carrying encrypted_cvv when requested + def update_card(card_id, update_card_request, headers = nil) api_client.invoke_patch( build_path(ISSUING, CARDS, card_id), sdk_authorization, - update_card_request + update_card_request, + headers ) end diff --git a/lib/checkout_sdk/issuing/update_card_request.rb b/lib/checkout_sdk/issuing/update_card_request.rb index 571aaa7..ee31345 100644 --- a/lib/checkout_sdk/issuing/update_card_request.rb +++ b/lib/checkout_sdk/issuing/update_card_request.rb @@ -5,23 +5,48 @@ module Issuing # Request body for PATCH /issuing/cards/{cardId}. # # @!attribute reference + # Your reference. + # [Optional] + # max 256 characters + # Example: X-123456-N11 # @return [String] # @!attribute metadata + # The user's metadata. + # [Optional] # @return [Hash{String => String}] # @!attribute expiry_month + # The card's expiration month. + # [Optional] + # min 1, max 12 + # Example: 5 # @return [Integer] # @!attribute expiry_year + # The card's expiration year. + # [Optional] + # min 4 characters, max 4 characters + # Example: 2025 # @return [Integer] - # @!attribute activation_date - # @return [String] ISO-8601 date scheduling the card's activation. Supported formats: - # date only `yyyy-mm-dd` (treated as midnight UTC) or date with round hour - # `yyyy-mm-ddTHH:mmZ` / `yyyy-mm-ddTHH:mm±HH:mm`. Maps swagger `IssuingActivationDate`. + # @!attribute scheduled_activation_date + # Date scheduling the card's first activation. Only applies to the initial activation of a + # card. Two formats are supported: date only (yyyy-MM-dd, treated as midnight UTC), or date + # with round hour (yyyy-MM-ddTHH:mmZ in UTC, or yyyy-MM-ddTHH:mm+HH:mm with offset). Only + # round hours are allowed when a time is provided (HH:00). The value must be at least the + # next round hour after the request time. + # + # Renamed from activation_date by the 2026-09-02 spec, which also removed the + # IssuingActivationDate schema this attribute used to cite. + # [Optional] + # Example: 2026-06-01T10:00Z + # @return [String] # @!attribute revocation_date - # @return [String] Date for the card to be automatically revoked, in the form `yyyy-mm-dd`. - # Must be after the current date. Maps swagger `IssuingRevocationDate`. + # Date scheduling the card's automatic revocation. + # [Optional] + # Format: yyyy-MM-dd (time is midnight UTC) + # Example: 2027-03-12 + # @return [String] class UpdateCardRequest attr_accessor :reference, :metadata, :expiry_month, :expiry_year, - :activation_date, :revocation_date + :scheduled_activation_date, :revocation_date end end end diff --git a/spec/checkout_sdk/api_client_spec.rb b/spec/checkout_sdk/api_client_spec.rb index b8f2457..c508dd8 100644 --- a/spec/checkout_sdk/api_client_spec.rb +++ b/spec/checkout_sdk/api_client_spec.rb @@ -180,7 +180,78 @@ api_client.invoke_put('accounts/entities/ent_1/reserve-rules/rsv_1', auth, {}, extra) - expect(captured[:'If-Match']).to eq('W/"etag-123"') + # The key is now a String, not a Symbol. Faraday rewrites Symbol keys via + # split('_').map(&:capitalize).join('-'), which turned :'If-Match' into If-match on the + # wire; a String reaches the wire verbatim. + expect(captured['If-Match']).to eq('W/"etag-123"') + expect(captured).not_to have_key(:'If-Match') + end + + it 'emits both card update headers on PATCH with their exact swagger spelling' do + captured = nil + allow(http_client).to receive(:run_request) do |_method, _path, _body, headers| + captured = headers + response + end + extra = CheckoutSdk::Issuing::CardUpdateHeaders.new + extra.return_encrypted_cvv = 'true' + extra.encryption_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A' + + api_client.invoke_patch('issuing/cards/crd_1', auth, {}, extra) + + # return-encrypted-cvv is lower case in the spec and Encryption-Key is title case. String + # keys are what preserves both; Symbol keys would have become Return-encrypted-cvv and + # Encryption-key by the time Faraday wrote the request. + expect(captured['return-encrypted-cvv']).to eq('true') + expect(captured['Encryption-Key']).to eq('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A') + expect(captured).not_to have_key('Return-Encrypted-Cvv') + end + + it 'emits no card update headers on PATCH when none are supplied' do + captured = nil + allow(http_client).to receive(:run_request) do |_method, _path, _body, headers| + captured = headers + response + end + + api_client.invoke_patch('issuing/cards/crd_1', auth, {}) + + expect(captured).not_to have_key('return-encrypted-cvv') + expect(captured).not_to have_key('Encryption-Key') + end + + it 'emits the encryption key on its own' do + captured = nil + allow(http_client).to receive(:run_request) do |_method, _path, _body, headers| + captured = headers + response + end + extra = CheckoutSdk::Issuing::CardUpdateHeaders.new + extra.encryption_key = 'MIIBIjAN' + + api_client.invoke_patch('issuing/cards/crd_1', auth, {}, extra) + + expect(captured['Encryption-Key']).to eq('MIIBIjAN') + expect(captured).not_to have_key('return-encrypted-cvv') + end + + # The previous apply_extra_headers ended with + # `return unless extra_headers.respond_to?(:accept) && extra_headers.accept`, so a container + # without an accept attribute short circuited and any header handled after that point was + # dead code. CardUpdateHeaders has no accept, which is exactly the shape that used to trip it. + it 'does not short circuit for a headers container that has no accept attribute' do + captured = nil + allow(http_client).to receive(:run_request) do |_method, _path, _body, headers| + captured = headers + response + end + extra = CheckoutSdk::Issuing::CardUpdateHeaders.new + extra.return_encrypted_cvv = 'true' + + expect(extra).not_to respond_to(:accept) + api_client.invoke_patch('issuing/cards/crd_1', auth, {}, extra) + + expect(captured['return-encrypted-cvv']).to eq('true') end it 'omits the If-Match header when no headers container is passed' do diff --git a/spec/checkout_sdk/identities/address_document_verification/address_document_verification_client_spec.rb b/spec/checkout_sdk/identities/address_document_verification/address_document_verification_client_spec.rb index 958fc72..debf7f5 100644 --- a/spec/checkout_sdk/identities/address_document_verification/address_document_verification_client_spec.rb +++ b/spec/checkout_sdk/identities/address_document_verification/address_document_verification_client_spec.rb @@ -58,7 +58,7 @@ describe '#get_address_document_verification_attempts' do it 'GETs address-document-verifications/{id}/attempts' do expect(api_client_mock).to receive(:invoke_get) - .with('address-document-verifications/adv_x/attempts', 'secret_key').and_return('response') + .with('address-document-verifications/adv_x/attempts', 'secret_key', nil).and_return('response') expect(client.get_address_document_verification_attempts('adv_x')).to eq('response') end end diff --git a/spec/checkout_sdk/identities/address_document_verification/address_document_verification_integration_spec.rb b/spec/checkout_sdk/identities/address_document_verification/address_document_verification_integration_spec.rb index 179af40..be003ab 100644 --- a/spec/checkout_sdk/identities/address_document_verification/address_document_verification_integration_spec.rb +++ b/spec/checkout_sdk/identities/address_document_verification/address_document_verification_integration_spec.rb @@ -58,5 +58,32 @@ expect(response).not_to be_nil end end + + describe '#get_address_document_verification_attempts with pagination' do + it 'honours skip and limit' do + query = CheckoutSdk::Identities::IdvAttemptsQueryFilter.new + query.limit = 1 + response = client.get_address_document_verification_attempts(ENV.fetch('CHECKOUT_ADV_ID', nil), query) + expect(response).not_to be_nil + expect(response.limit).to eq(1) + end + end + + describe '#get_address_document_verification_attempt_assets' do + it 'retrieves the uploaded document image' do + query = CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter.new + query.limit = 10 + response = client.get_address_document_verification_attempt_assets( + ENV.fetch('CHECKOUT_ADV_ID', nil), + ENV.fetch('CHECKOUT_ADV_ATTEMPT_ID', nil), + query + ) + expect(response).not_to be_nil + response.data.each do |asset| + expect(asset.type).to eq('document') + expect(asset._links.asset_url.href).not_to be_nil + end + end + end end end diff --git a/spec/checkout_sdk/identities/face_authentication/face_authentication_client_spec.rb b/spec/checkout_sdk/identities/face_authentication/face_authentication_client_spec.rb index f2c75d4..2b78e1f 100644 --- a/spec/checkout_sdk/identities/face_authentication/face_authentication_client_spec.rb +++ b/spec/checkout_sdk/identities/face_authentication/face_authentication_client_spec.rb @@ -44,7 +44,7 @@ describe '#get_face_authentication_attempts' do it 'GETs face-authentications/{id}/attempts' do expect(api_client_mock).to receive(:invoke_get) - .with('face-authentications/fa_x/attempts', 'secret_key').and_return('response') + .with('face-authentications/fa_x/attempts', 'secret_key', nil).and_return('response') expect(client.get_face_authentication_attempts('fa_x')).to eq('response') end end diff --git a/spec/checkout_sdk/identities/id_document_verification/id_document_verification_client_spec.rb b/spec/checkout_sdk/identities/id_document_verification/id_document_verification_client_spec.rb index b86b3e1..e438e3c 100644 --- a/spec/checkout_sdk/identities/id_document_verification/id_document_verification_client_spec.rb +++ b/spec/checkout_sdk/identities/id_document_verification/id_document_verification_client_spec.rb @@ -44,7 +44,7 @@ describe '#get_id_document_verification_attempts' do it 'GETs id-document-verifications/{id}/attempts' do expect(api_client_mock).to receive(:invoke_get) - .with('id-document-verifications/idv_x/attempts', 'secret_key').and_return('response') + .with('id-document-verifications/idv_x/attempts', 'secret_key', nil).and_return('response') expect(client.get_id_document_verification_attempts('idv_x')).to eq('response') end end diff --git a/spec/checkout_sdk/identities/id_document_verification/id_document_verification_integration_spec.rb b/spec/checkout_sdk/identities/id_document_verification/id_document_verification_integration_spec.rb index 104179c..ea1e948 100644 --- a/spec/checkout_sdk/identities/id_document_verification/id_document_verification_integration_spec.rb +++ b/spec/checkout_sdk/identities/id_document_verification/id_document_verification_integration_spec.rb @@ -44,5 +44,32 @@ expect(response).not_to be_nil end end + + describe '#get_id_document_verification_attempts with pagination' do + it 'honours skip and limit' do + query = CheckoutSdk::Identities::IdvAttemptsQueryFilter.new + query.limit = 1 + response = client.get_id_document_verification_attempts(ENV['CHECKOUT_IDV_DOC_ID'], query) + expect(response).not_to be_nil + expect(response.limit).to eq(1) + end + end + + describe '#get_id_document_verification_attempt_assets' do + it 'retrieves the uploaded document images' do + query = CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter.new + query.limit = 10 + response = client.get_id_document_verification_attempt_assets( + ENV['CHECKOUT_IDV_DOC_ID'], + ENV['CHECKOUT_IDV_DOC_ATTEMPT_ID'], + query + ) + expect(response).not_to be_nil + response.data.each do |asset| + expect(%w[document_front_image document_back_image]).to include(asset.type) + expect(asset._links.asset_url.href).not_to be_nil + end + end + end end end diff --git a/spec/checkout_sdk/identities/identities_assets_pagination_spec.rb b/spec/checkout_sdk/identities/identities_assets_pagination_spec.rb new file mode 100644 index 0000000..483b841 --- /dev/null +++ b/spec/checkout_sdk/identities/identities_assets_pagination_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +# Part A (the two new attempt-assets endpoints) and part F M1 (pagination on all four +# list-attempts endpoints) of the 2026-09-02 swagger delta. +RSpec.describe 'Identities attempt assets and pagination' do + let(:credentials_mock) { double('credentials') } + let(:api_client_mock) { double('api_client') } + let(:configuration_mock) { double('configuration') } + + before do + allow(configuration_mock).to receive(:credentials).and_return(credentials_mock) + allow(credentials_mock).to receive(:get_authorization).and_return('secret_key') + end + + def pagination + query = CheckoutSdk::Identities::IdvAttemptsQueryFilter.new + query.skip = 5 + query.limit = 25 + query + end + + def asset_pagination + query = CheckoutSdk::Identities::IdvAttemptAssetsQueryFilter.new + query.skip = 2 + query.limit = 50 + query + end + + describe 'AddressDocumentVerificationClient' do + let(:client) do + CheckoutSdk::Identities::AddressDocumentVerification::AddressDocumentVerificationClient + .new(api_client_mock, configuration_mock) + end + + it 'GETs the new attempts/{attempt_id}/assets path' do + query = asset_pagination + expect(api_client_mock).to receive(:invoke_get) + .with('address-document-verifications/adv_x/attempts/adva_x/assets', 'secret_key', query) + .and_return('response') + + expect(client.get_address_document_verification_attempt_assets('adv_x', 'adva_x', query)) + .to eq('response') + end + + it 'sends no query when the assets filter is omitted' do + expect(api_client_mock).to receive(:invoke_get) + .with('address-document-verifications/adv_x/attempts/adva_x/assets', 'secret_key', nil) + .and_return('response') + + expect(client.get_address_document_verification_attempt_assets('adv_x', 'adva_x')) + .to eq('response') + end + + it 'forwards the pagination filter on the attempts list' do + query = pagination + expect(api_client_mock).to receive(:invoke_get) + .with('address-document-verifications/adv_x/attempts', 'secret_key', query) + .and_return('response') + + expect(client.get_address_document_verification_attempts('adv_x', query)).to eq('response') + end + end + + describe 'IdDocumentVerificationClient' do + let(:client) do + CheckoutSdk::Identities::IdDocumentVerification::IdDocumentVerificationClient + .new(api_client_mock, configuration_mock) + end + + it 'GETs the new attempts/{attempt_id}/assets path' do + query = asset_pagination + expect(api_client_mock).to receive(:invoke_get) + .with('id-document-verifications/iddv_x/attempts/datp_x/assets', 'secret_key', query) + .and_return('response') + + expect(client.get_id_document_verification_attempt_assets('iddv_x', 'datp_x', query)) + .to eq('response') + end + + it 'sends no query when the assets filter is omitted' do + expect(api_client_mock).to receive(:invoke_get) + .with('id-document-verifications/iddv_x/attempts/datp_x/assets', 'secret_key', nil) + .and_return('response') + + expect(client.get_id_document_verification_attempt_assets('iddv_x', 'datp_x')) + .to eq('response') + end + + it 'forwards the pagination filter on the attempts list' do + query = pagination + expect(api_client_mock).to receive(:invoke_get) + .with('id-document-verifications/iddv_x/attempts', 'secret_key', query).and_return('response') + + expect(client.get_id_document_verification_attempts('iddv_x', query)).to eq('response') + end + end + + describe 'IdentityVerificationClient' do + let(:client) do + CheckoutSdk::Identities::IdentityVerification::IdentityVerificationClient + .new(api_client_mock, configuration_mock) + end + + it 'forwards the pagination filter on the attempts list' do + query = pagination + expect(api_client_mock).to receive(:invoke_get) + .with('identity-verifications/idv_x/attempts', 'secret_key', query).and_return('response') + + expect(client.get_identity_verification_attempts('idv_x', query)).to eq('response') + end + + it 'accepts the typed assets filter on the pre-existing assets method' do + query = asset_pagination + expect(api_client_mock).to receive(:invoke_get) + .with('identity-verifications/idv_x/attempts/att_x/assets', 'secret_key', query) + .and_return('response') + + expect(client.get_identity_verification_attempt_assets('idv_x', 'att_x', query)) + .to eq('response') + end + end + + describe 'FaceAuthenticationClient' do + let(:client) do + CheckoutSdk::Identities::FaceAuthentication::FaceAuthenticationClient + .new(api_client_mock, configuration_mock) + end + + it 'forwards the pagination filter on the attempts list' do + query = pagination + expect(api_client_mock).to receive(:invoke_get) + .with('face-authentications/fa_x/attempts', 'secret_key', query).and_return('response') + + expect(client.get_face_authentication_attempts('fa_x', query)).to eq('response') + end + end + + describe 'the query filters' do + it 'serializes skip and limit' do + expect(CheckoutSdk::JsonSerializer.to_custom_hash(pagination)) + .to eq('skip' => 5, 'limit' => 25) + end + + it 'keeps an explicit zero skip, unlike the PHP and Go filters' do + query = CheckoutSdk::Identities::IdvAttemptsQueryFilter.new + query.skip = 0 + query.limit = 10 + + expect(CheckoutSdk::JsonSerializer.to_custom_hash(query)).to eq('skip' => 0, 'limit' => 10) + end + + it 'omits unset members' do + query = CheckoutSdk::Identities::IdvAttemptsQueryFilter.new + query.limit = 25 + + expect(CheckoutSdk::JsonSerializer.to_custom_hash(query)).to eq('limit' => 25) + end + + it 'encodes to a query string through append_params' do + client = CheckoutSdk::ApiClient.allocate + expect(client.send(:append_params, 'attempts', pagination)).to eq('attempts?skip=5&limit=25') + end + end +end diff --git a/spec/checkout_sdk/identities/identities_serialization_spec.rb b/spec/checkout_sdk/identities/identities_serialization_spec.rb new file mode 100644 index 0000000..6538a8d --- /dev/null +++ b/spec/checkout_sdk/identities/identities_serialization_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +# Part E of the 2026-09-02 swagger delta, plus the D1 split between the shared and the identity +# verification specific shapes. +RSpec.describe 'Identities serialization' do + IDV = CheckoutSdk::Identities::IdentityVerification + IDENTITIES = CheckoutSdk::Identities + + def serialize(object) + CheckoutSdk::JsonSerializer.to_custom_hash(object) + end + + def phone_number + phone = IDENTITIES::IdvPhoneNumber.new + phone.country_code = '+33' + phone.number = '5555550102' + phone + end + + def address + addr = IDENTITIES::IdvAddress.new + addr.address_line1 = '123 Main Street' + addr.city = 'London' + addr.zip = 'SW1A 1AA' + addr.country = 'GB' + addr + end + + def identity_declared_data + declared = IDV::IdvIdentityDeclaredData.new + declared.name = 'Hannah Bret' + declared.birth_date = '1994-10-15' + declared.email = 'hannah.bret@example.com' + declared.phone_number = phone_number + declared.address = address + declared + end + + describe CheckoutSdk::Identities::IdvPhoneNumber do + it 'serializes both properties' do + expect(serialize(phone_number)).to eq('country_code' => '+33', 'number' => '5555550102') + end + + # T17: the phone country code is a dialling prefix, not an ISO code, so it stays a string. + it 'keeps the dialling prefix verbatim' do + expect(serialize(phone_number)['country_code']).to eq('+33') + end + end + + describe CheckoutSdk::Identities::IdvAddress do + it 'serializes every property' do + addr = address + addr.address_line2 = 'Apt 4B' + addr.state = 'Greater London' + + expect(serialize(addr)).to eq( + 'address_line1' => '123 Main Street', + 'address_line2' => 'Apt 4B', + 'city' => 'London', + 'state' => 'Greater London', + 'zip' => 'SW1A 1AA', + 'country' => 'GB' + ) + end + end + + describe 'the declared data split (D1)' do + it 'keeps the shared shape at two attributes' do + declared = IDV::IdvDeclaredData.new + declared.name = 'Hannah Bret' + declared.birth_date = '1994-10-15' + + expect(serialize(declared)).to eq('name' => 'Hannah Bret', 'birth_date' => '1994-10-15') + end + + # The shared class is used by the address document and ID document verification requests. The + # three identity verification only fields must not be reachable on it. + it 'does not expose the identity verification only fields on the shared class' do + declared = IDV::IdvDeclaredData.new + + expect(declared).not_to respond_to(:phone_number=) + expect(declared).not_to respond_to(:email=) + expect(declared).not_to respond_to(:address=) + end + + it 'inherits the shared attributes on the identity verification shape' do + expect(IDV::IdvIdentityDeclaredData.superclass).to eq(IDV::IdvDeclaredData) + expect(serialize(identity_declared_data)).to eq( + 'name' => 'Hannah Bret', + 'birth_date' => '1994-10-15', + 'email' => 'hannah.bret@example.com', + 'phone_number' => { 'country_code' => '+33', 'number' => '5555550102' }, + 'address' => { + 'address_line1' => '123 Main Street', + 'city' => 'London', + 'zip' => 'SW1A 1AA', + 'country' => 'GB' + } + ) + end + + it 'omits unset optional attributes' do + declared = IDV::IdvIdentityDeclaredData.new + declared.name = 'Hannah Bret' + + expect(serialize(declared)).to eq('name' => 'Hannah Bret') + end + end + + describe 'the client information split (D1)' do + it 'keeps the face authentication shape at two attributes' do + info = IDV::IdvClientInformation.new + info.pre_selected_residence_country = 'FR' + info.pre_selected_language = 'en-US' + + expect(serialize(info)).to eq( + 'pre_selected_residence_country' => 'FR', + 'pre_selected_language' => 'en-US' + ) + end + + # The face authentication attempt schema declares neither document field, so sending them + # there would be a request the API rejects. The narrow class is what prevents it. + it 'does not expose the document fields on the face authentication shape' do + info = IDV::IdvClientInformation.new + + expect(info).not_to respond_to(:pre_selected_document_issuing_country=) + expect(info).not_to respond_to(:pre_selected_document_type=) + end + + it 'adds the two identity verification only fields on the wider shape' do + info = IDV::IdvIdentityClientInformation.new + info.pre_selected_residence_country = 'FR' + info.pre_selected_language = 'en-US' + info.pre_selected_document_issuing_country = 'GB' + info.pre_selected_document_type = 'Travel Document' + + expect(IDV::IdvIdentityClientInformation.superclass).to eq(IDV::IdvClientInformation) + expect(serialize(info)).to eq( + 'pre_selected_residence_country' => 'FR', + 'pre_selected_language' => 'en-US', + 'pre_selected_document_issuing_country' => 'GB', + 'pre_selected_document_type' => 'Travel Document' + ) + end + end + + describe IDV::IdentityVerificationAttemptRequest do + it 'serializes phone_number and the wider client information' do + info = IDV::IdvIdentityClientInformation.new + info.pre_selected_document_type = 'Passport' + + req = described_class.new + req.redirect_url = 'https://example.com/success' + req.phone_number = phone_number + req.client_information = info + + hash = serialize(req) + expect(hash['redirect_url']).to eq('https://example.com/success') + expect(hash['phone_number']).to eq('country_code' => '+33', 'number' => '5555550102') + expect(hash['client_information']).to eq('pre_selected_document_type' => 'Passport') + end + end + + describe CheckoutSdk::Identities::FaceAuthentication::FaceAuthenticationAttemptRequest do + it 'serializes phone_number and keeps the narrow client information' do + info = IDV::IdvClientInformation.new + info.pre_selected_residence_country = 'FR' + + req = described_class.new + req.redirect_url = 'https://example.com/success' + req.phone_number = phone_number + req.client_information = info + + hash = serialize(req) + expect(hash['phone_number']['country_code']).to eq('+33') + expect(hash['client_information']).to eq('pre_selected_residence_country' => 'FR') + expect(hash['client_information']).not_to have_key('pre_selected_document_type') + end + end + + describe IDV::IdentityVerificationRequest do + it 'carries the five field identity declared data' do + req = described_class.new + req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' + req.user_journey_id = 'usj_tkoi5db4hryu5cei5vwoabr7we' + req.declared_data = identity_declared_data + + hash = serialize(req) + expect(hash['declared_data']['email']).to eq('hannah.bret@example.com') + expect(hash['declared_data']['phone_number']['country_code']).to eq('+33') + expect(hash['declared_data']['address']['country']).to eq('GB') + end + end + + describe CheckoutSdk::Identities::IdDocumentVerification::IdDocumentVerificationRequest do + # The class previously declared neither of these, and declared four attributes the spec has + # no equivalent for. The four are retained as deprecated so no caller breaks. + it 'now declares the two attributes the spec requires' do + req = described_class.new + req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' + req.user_journey_id = 'usj_tkoi5db4hryu5cei5vwoabr7we' + + declared = IDV::IdvDeclaredData.new + declared.name = 'Hannah Bret' + declared.birth_date = '1994-10-15' + req.declared_data = declared + + hash = serialize(req) + expect(hash['applicant_id']).to eq('aplt_tkoi5db4hryu5cei5vwoabr7we') + expect(hash['user_journey_id']).to eq('usj_tkoi5db4hryu5cei5vwoabr7we') + expect(hash['declared_data']).to eq('name' => 'Hannah Bret', 'birth_date' => '1994-10-15') + end + + it 'still responds to the four attributes the spec does not declare' do + req = described_class.new + + expect(req).to respond_to(:external_reference=) + expect(req).to respond_to(:document_type=) + expect(req).to respond_to(:country=) + expect(req).to respond_to(:redirect_url=) + end + end + + describe CheckoutSdk::Identities::FaceAuthentication::FaceAuthenticationRequest do + # The class omitted user_journey_id, which FavFaceAuthentication requires, and declared two + # attributes the spec has no equivalent for. The two are retained as deprecated. + it 'now declares the two attributes the spec requires' do + req = described_class.new + req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' + req.user_journey_id = 'usj_tkoi5db4hryu5cei5vwoabr7we' + + expect(serialize(req)).to eq( + 'applicant_id' => 'aplt_tkoi5db4hryu5cei5vwoabr7we', + 'user_journey_id' => 'usj_tkoi5db4hryu5cei5vwoabr7we' + ) + end + + it 'still responds to the two attributes the spec does not declare' do + req = described_class.new + + expect(req).to respond_to(:external_reference=) + expect(req).to respond_to(:redirect_url=) + end + end + + describe CheckoutSdk::Identities::AddressDocumentVerification::AddressDocumentVerificationRequest do + it 'takes the narrow declared data shape' do + declared = IDV::IdvDeclaredData.new + declared.name = 'Hannah Bret' + declared.birth_date = '1994-10-15' + + req = described_class.new + req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' + req.user_journey_id = 'usj_tkoi5db4hryu5cei5vwoabr7we' + req.declared_data = declared + + expect(serialize(req)['declared_data']).to eq( + 'name' => 'Hannah Bret', 'birth_date' => '1994-10-15' + ) + end + end +end diff --git a/spec/checkout_sdk/identities/identity_verification/identity_verification_client_spec.rb b/spec/checkout_sdk/identities/identity_verification/identity_verification_client_spec.rb index c4cd7f5..c0c9d55 100644 --- a/spec/checkout_sdk/identities/identity_verification/identity_verification_client_spec.rb +++ b/spec/checkout_sdk/identities/identity_verification/identity_verification_client_spec.rb @@ -60,7 +60,7 @@ describe '#get_identity_verification_attempts' do it 'GETs identity-verifications/{id}/attempts' do expect(api_client_mock).to receive(:invoke_get) - .with('identity-verifications/idv_x/attempts', 'secret_key').and_return('response') + .with('identity-verifications/idv_x/attempts', 'secret_key', nil).and_return('response') expect(client.get_identity_verification_attempts('idv_x')).to eq('response') end end diff --git a/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb b/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb new file mode 100644 index 0000000..7730582 --- /dev/null +++ b/spec/checkout_sdk/issuing/issuing_cards_serialization_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# The card scheduling fields aligned with the 2026-09-02 swagger delta. These used to live in +# issuing_disputes_serialization_spec.rb, which had nothing to do with cards. +RSpec.describe 'Issuing cards serialization' do + def serialize(request) + CheckoutSdk::JsonSerializer.to_custom_hash(request) + end + + describe CheckoutSdk::Issuing::UpdateCardRequest do + it 'serializes scheduled_activation_date and revocation_date' do + req = described_class.new + req.scheduled_activation_date = '2026-06-01T10:00Z' + req.revocation_date = '2027-03-12' + + hash = serialize(req) + + expect(hash['scheduled_activation_date']).to eq('2026-06-01T10:00Z') + expect(hash['revocation_date']).to eq('2027-03-12') + expect(hash).not_to have_key('activation_date') + end + + it 'no longer declares activation_date' do + # Ruby attr_accessor is defined per class, so a caller still calling + # req.activation_date = ... raises NoMethodError rather than silently serializing a key the + # API rejects. That is a better failure mode than php or python, where the assignment would + # succeed, and this guard records it. + expect(described_class.new).not_to respond_to(:activation_date) + expect(described_class.new).not_to respond_to(:activation_date=) + expect(described_class.new).to respond_to(:scheduled_activation_date=) + end + + it 'serializes every declared attribute' do + req = described_class.new + req.reference = 'X-123456-N11' + req.metadata = { 'udf1' => 'metadata1' } + req.expiry_month = 6 + req.expiry_year = 2030 + req.scheduled_activation_date = '2026-06-01T10:00Z' + req.revocation_date = '2027-03-12' + + expect(serialize(req)).to eq( + 'reference' => 'X-123456-N11', + 'metadata' => { 'udf1' => 'metadata1' }, + 'expiry_month' => 6, + 'expiry_year' => 2030, + 'scheduled_activation_date' => '2026-06-01T10:00Z', + 'revocation_date' => '2027-03-12' + ) + end + + it 'omits unset attributes' do + expect(serialize(described_class.new)).to eq({}) + end + + # The swagger example for update-card-request, verbatim. + it 'round trips the swagger example' do + payload = JSON.parse( + '{"reference":"X-123456-N11","expiry_month":6,"expiry_year":2030,' \ + '"revocation_date":"2027-03-12","scheduled_activation_date":"2026-06-01T10:00Z"}' + ) + + req = described_class.new + req.reference = payload['reference'] + req.expiry_month = payload['expiry_month'] + req.expiry_year = payload['expiry_year'] + req.revocation_date = payload['revocation_date'] + req.scheduled_activation_date = payload['scheduled_activation_date'] + + expect(serialize(req)).to eq(payload) + end + end + + describe CheckoutSdk::Issuing::CardUpdateHeaders do + it 'exposes the two headers the endpoint accepts' do + headers = described_class.new + expect(headers).to respond_to(:return_encrypted_cvv=) + expect(headers).to respond_to(:encryption_key=) + end + + it 'declares return_encrypted_cvv as a string value, not a boolean' do + headers = described_class.new + headers.return_encrypted_cvv = 'true' + expect(headers.return_encrypted_cvv).to eq('true') + end + end +end diff --git a/spec/checkout_sdk/issuing/issuing_disputes_serialization_spec.rb b/spec/checkout_sdk/issuing/issuing_disputes_serialization_spec.rb index ebd9128..2e9a223 100644 --- a/spec/checkout_sdk/issuing/issuing_disputes_serialization_spec.rb +++ b/spec/checkout_sdk/issuing/issuing_disputes_serialization_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'Issuing disputes/cards serialization' do +RSpec.describe 'Issuing disputes serialization' do describe CheckoutSdk::Issuing::IssuingDisputeFraudType do it 'maps every value to its exact swagger string' do expected = { @@ -108,17 +108,4 @@ expect(hash['amount']).to eq(100) end end - - describe CheckoutSdk::Issuing::UpdateCardRequest do - it 'serializes activation_date and revocation_date' do - req = described_class.new - req.activation_date = '2026-06-01T10:00Z' - req.revocation_date = '2027-03-12' - - hash = CheckoutSdk::JsonSerializer.to_custom_hash(req) - - expect(hash['activation_date']).to eq('2026-06-01T10:00Z') - expect(hash['revocation_date']).to eq('2027-03-12') - end - end end diff --git a/spec/checkout_sdk/issuing/issuing_extras_spec.rb b/spec/checkout_sdk/issuing/issuing_extras_spec.rb index 4748e5a..8212e7a 100644 --- a/spec/checkout_sdk/issuing/issuing_extras_spec.rb +++ b/spec/checkout_sdk/issuing/issuing_extras_spec.rb @@ -22,7 +22,7 @@ it 'PATCHes issuing/cards/{id}' do req = CheckoutSdk::Issuing::UpdateCardRequest.new expect(api_client_mock).to receive(:invoke_patch) - .with('issuing/cards/crd_1', 'secret_key', req).and_return('r') + .with('issuing/cards/crd_1', 'secret_key', req, nil).and_return('r') expect(client.update_card('crd_1', req)).to eq('r') end end From 28e66c8e8c2695ec649cda840bfcfcdbcc0bf2fd Mon Sep 17 00:00:00 2001 From: david ruiz Date: Mon, 21 Sep 2026 17:47:42 +0200 Subject: [PATCH 2/2] Test extended --- .../identities_response_shape_spec.rb | 216 ++++++++++++++++++ .../identities_serialization_spec.rb | 40 ++-- 2 files changed, 236 insertions(+), 20 deletions(-) create mode 100644 spec/checkout_sdk/identities/identities_response_shape_spec.rb diff --git a/spec/checkout_sdk/identities/identities_response_shape_spec.rb b/spec/checkout_sdk/identities/identities_response_shape_spec.rb new file mode 100644 index 0000000..42b1175 --- /dev/null +++ b/spec/checkout_sdk/identities/identities_response_shape_spec.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: true + +# Part A of the 2026-09-02 swagger delta, at the response side. +# +# The client specs mock the ApiClient and only prove the path, and the ADV and IDDV integration +# specs are wrapped in `skip`, so before this file nothing runnable ever parsed an assets payload. +# These drive the real parse path (parse_response -> parse_body -> deep_convert_to_ostruct), which +# is what actually hands a caller their result. +# +# The payloads are the swagger examples verbatim, from components.examples, apart from shortened +# hrefs. asset_url is the only link AdvAttemptAsset and IddvAttemptAsset declare, and it is +# required, so using the spec's own example rather than a hand-written fixture is the point. +RSpec.describe 'Identities response shapes' do + let(:configuration) do + double( + 'CheckoutConfiguration', + http_client: Faraday.new, + multipart_http_client: Faraday.new, + logger: Logger.new(File::NULL) + ) + end + + let(:api_client) { CheckoutSdk::ApiClient.new(configuration, 'https://api.sandbox.checkout.com') } + + def parse(body, status: 200) + response = double('Response', status: status, body: body, + headers: { 'Content-Type' => 'application/json' }) + allow(CheckoutSdk::CheckoutUtils).to receive(:map_to_http_metadata).with(response).and_return( + OpenStruct.new(status_code: status, body: body) + ) + api_client.send(:parse_response, response) + end + + let(:adv_attempt_assets) do + <<~JSON + { + "total_count": 1, + "skip": 0, + "limit": 10, + "data": [ + { + "type": "document", + "_links": { + "asset_url": { "href": "https://storage-b.env.ubble.ai/ubble-ai/NDY/address_document.png" } + } + } + ], + "_links": { + "self": { "href": "https://idv.checkout.com/address-document-verifications/adv_1/attempts/adva_1/assets" }, + "next": { "href": "https://idv.checkout.com/address-document-verifications/adv_1/attempts/adva_1/assets?skip=10" }, + "previous": { "href": "https://idv.checkout.com/address-document-verifications/adv_1/attempts/adva_1/assets?skip=0" } + } + } + JSON + end + + let(:iddv_attempt_assets) do + <<~JSON + { + "total_count": 2, + "skip": 0, + "limit": 10, + "data": [ + { + "type": "document_front_image", + "_links": { "asset_url": { "href": "https://storage-b.env.ubble.ai/ubble-ai/NDY/document_front.png" } } + }, + { + "type": "document_back_image", + "_links": { "asset_url": { "href": "https://storage-b.env.ubble.ai/ubble-ai/NDY/document_back.png" } } + } + ], + "_links": { + "self": { "href": "https://idv.checkout.com/id-document-verifications/iddv_1/attempts/datp_1/assets" } + } + } + JSON + end + + describe 'the address document verification attempt assets response' do + it 'parses the swagger example' do + response = parse(adv_attempt_assets) + + expect(response.total_count).to eq(1) + expect(response.skip).to eq(0) + expect(response.limit).to eq(10) + expect(response.data.length).to eq(1) + expect(response.data.first.type).to eq('document') + expect(response.data.first._links.asset_url.href).to include('address_document.png') + expect(response.http_metadata.status_code).to eq(200) + end + + it 'exposes the pagination links, without which skip and limit cannot be walked' do + response = parse(adv_attempt_assets) + + expect(response._links.self.href).to end_with('/assets') + expect(response._links.next.href).to include('skip=10') + expect(response._links.previous.href).to include('skip=0') + end + + it 'names the asset link asset_url and not download' do + asset = parse(adv_attempt_assets).data.first + + expect(asset._links.asset_url).not_to be_nil + expect(asset._links.download).to be_nil + end + end + + describe 'the ID document verification attempt assets response' do + it 'parses the swagger example, including both asset types' do + response = parse(iddv_attempt_assets) + + expect(response.total_count).to eq(2) + expect(response.data.length).to eq(2) + expect(response.data.map(&:type)).to eq(%w[document_front_image document_back_image]) + expect(response.data[0]._links.asset_url.href).to include('document_front.png') + expect(response.data[1]._links.asset_url.href).to include('document_back.png') + end + + it 'names the asset link asset_url and not download' do + parse(iddv_attempt_assets).data.each do |asset| + expect(asset._links.asset_url).not_to be_nil + expect(asset._links.download).to be_nil + end + end + end + + # T7: data declares minItems 0, so an attempt with no assets yet is a legal page. + describe 'an empty assets page' do + it 'parses with an empty data array' do + response = parse( + '{"total_count":0,"skip":0,"limit":10,"data":[],' \ + '"_links":{"self":{"href":"https://idv.checkout.com/a"}}}' + ) + + expect(response.total_count).to eq(0) + expect(response.data).to eq([]) + expect(response._links.self.href).not_to be_nil + end + end + + # Part F M1: the list-attempts responses gained nothing structurally, but the pagination members + # are what the new skip and limit parameters page through, so they are worth pinning. + describe 'a paginated attempts list response' do + it 'exposes total_count, skip, limit and the links' do + response = parse( + '{"total_count":25,"skip":10,"limit":10,"data":[{"id":"adva_1","status":"completed"}],' \ + '"_links":{"self":{"href":"https://idv.checkout.com/a?skip=10"},' \ + '"next":{"href":"https://idv.checkout.com/a?skip=20"},' \ + '"previous":{"href":"https://idv.checkout.com/a?skip=0"}}}' + ) + + expect(response.total_count).to eq(25) + expect(response.skip).to eq(10) + expect(response.limit).to eq(10) + expect(response.data.first.status).to eq('completed') + expect(response._links.next.href).to include('skip=20') + end + end + + # Part D: the API answers 422 with error code encryption_key_required when return-encrypted-cvv + # is set to "true" without an Encryption-Key header. parse_response raises for status >= 400, so + # the error body has to survive into the exception for a caller to act on it. + describe 'the card update 422' do + it 'raises CheckoutApiException carrying the encryption_key_required code' do + body = '{"request_id":"0HLHPN8802NUF:00000003",' \ + '"error_type":"request_invalid",' \ + '"error_codes":["encryption_key_required"]}' + response = double('Response', status: 422, body: body, + headers: { 'Content-Type' => 'application/json' }) + allow(CheckoutSdk::CheckoutUtils).to receive(:map_to_http_metadata).with(response).and_return( + OpenStruct.new(status_code: 422, body: body) + ) + + expect { api_client.send(:parse_response, response) } + .to raise_error(CheckoutSdk::CheckoutApiException) { |error| + expect(error.http_metadata.status_code).to eq(422) + expect(error.error_details.error_type).to eq('request_invalid') + expect(error.error_details.error_codes).to include('encryption_key_required') + } + end + end + + # The success counterpart: encrypted_cvv is returned only when the headers ask for it, so it is + # absent on an ordinary update. + describe 'the card update response' do + it 'exposes encrypted_cvv when the headers requested it' do + response = parse('{"last_modified_date":"2026-06-01T10:00:00Z","encrypted_cvv":"oJMoNMEEUiQKYOsQ4Zd"}') + + expect(response.encrypted_cvv).to eq('oJMoNMEEUiQKYOsQ4Zd') + expect(response.last_modified_date).to eq('2026-06-01T10:00:00Z') + end + + it 'leaves encrypted_cvv nil on an ordinary update' do + response = parse('{"last_modified_date":"2026-06-01T10:00:00Z"}') + + expect(response.encrypted_cvv).to be_nil + end + end + + # Part C is a no-op in ruby because responses are OpenStruct, but the key still changed, so the + # report response is worth one assertion in each direction. + describe 'the PDF report response' do + it 'exposes pdf_report' do + response = parse('{"pdf_report":"https://www.example.com/reports/idv_1.pdf"}') + + expect(response.pdf_report).to end_with('.pdf') + end + + it 'leaves signed_url nil, since IdvPdf no longer declares it' do + response = parse('{"pdf_report":"https://www.example.com/reports/idv_1.pdf"}') + + expect(response.signed_url).to be_nil + end + end +end diff --git a/spec/checkout_sdk/identities/identities_serialization_spec.rb b/spec/checkout_sdk/identities/identities_serialization_spec.rb index 6538a8d..b9e07c2 100644 --- a/spec/checkout_sdk/identities/identities_serialization_spec.rb +++ b/spec/checkout_sdk/identities/identities_serialization_spec.rb @@ -3,22 +3,22 @@ # Part E of the 2026-09-02 swagger delta, plus the D1 split between the shared and the identity # verification specific shapes. RSpec.describe 'Identities serialization' do - IDV = CheckoutSdk::Identities::IdentityVerification - IDENTITIES = CheckoutSdk::Identities - + # Classes are referenced by their full names rather than through local constants: assigning + # IDV = ... inside a describe block defines the constant on Object, leaking a generic name into + # the global namespace. def serialize(object) CheckoutSdk::JsonSerializer.to_custom_hash(object) end def phone_number - phone = IDENTITIES::IdvPhoneNumber.new + phone = CheckoutSdk::Identities::IdvPhoneNumber.new phone.country_code = '+33' phone.number = '5555550102' phone end def address - addr = IDENTITIES::IdvAddress.new + addr = CheckoutSdk::Identities::IdvAddress.new addr.address_line1 = '123 Main Street' addr.city = 'London' addr.zip = 'SW1A 1AA' @@ -27,7 +27,7 @@ def address end def identity_declared_data - declared = IDV::IdvIdentityDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvIdentityDeclaredData.new declared.name = 'Hannah Bret' declared.birth_date = '1994-10-15' declared.email = 'hannah.bret@example.com' @@ -66,7 +66,7 @@ def identity_declared_data describe 'the declared data split (D1)' do it 'keeps the shared shape at two attributes' do - declared = IDV::IdvDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData.new declared.name = 'Hannah Bret' declared.birth_date = '1994-10-15' @@ -76,7 +76,7 @@ def identity_declared_data # The shared class is used by the address document and ID document verification requests. The # three identity verification only fields must not be reachable on it. it 'does not expose the identity verification only fields on the shared class' do - declared = IDV::IdvDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData.new expect(declared).not_to respond_to(:phone_number=) expect(declared).not_to respond_to(:email=) @@ -84,7 +84,7 @@ def identity_declared_data end it 'inherits the shared attributes on the identity verification shape' do - expect(IDV::IdvIdentityDeclaredData.superclass).to eq(IDV::IdvDeclaredData) + expect(CheckoutSdk::Identities::IdentityVerification::IdvIdentityDeclaredData.superclass).to eq(CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData) expect(serialize(identity_declared_data)).to eq( 'name' => 'Hannah Bret', 'birth_date' => '1994-10-15', @@ -100,7 +100,7 @@ def identity_declared_data end it 'omits unset optional attributes' do - declared = IDV::IdvIdentityDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvIdentityDeclaredData.new declared.name = 'Hannah Bret' expect(serialize(declared)).to eq('name' => 'Hannah Bret') @@ -109,7 +109,7 @@ def identity_declared_data describe 'the client information split (D1)' do it 'keeps the face authentication shape at two attributes' do - info = IDV::IdvClientInformation.new + info = CheckoutSdk::Identities::IdentityVerification::IdvClientInformation.new info.pre_selected_residence_country = 'FR' info.pre_selected_language = 'en-US' @@ -122,20 +122,20 @@ def identity_declared_data # The face authentication attempt schema declares neither document field, so sending them # there would be a request the API rejects. The narrow class is what prevents it. it 'does not expose the document fields on the face authentication shape' do - info = IDV::IdvClientInformation.new + info = CheckoutSdk::Identities::IdentityVerification::IdvClientInformation.new expect(info).not_to respond_to(:pre_selected_document_issuing_country=) expect(info).not_to respond_to(:pre_selected_document_type=) end it 'adds the two identity verification only fields on the wider shape' do - info = IDV::IdvIdentityClientInformation.new + info = CheckoutSdk::Identities::IdentityVerification::IdvIdentityClientInformation.new info.pre_selected_residence_country = 'FR' info.pre_selected_language = 'en-US' info.pre_selected_document_issuing_country = 'GB' info.pre_selected_document_type = 'Travel Document' - expect(IDV::IdvIdentityClientInformation.superclass).to eq(IDV::IdvClientInformation) + expect(CheckoutSdk::Identities::IdentityVerification::IdvIdentityClientInformation.superclass).to eq(CheckoutSdk::Identities::IdentityVerification::IdvClientInformation) expect(serialize(info)).to eq( 'pre_selected_residence_country' => 'FR', 'pre_selected_language' => 'en-US', @@ -145,9 +145,9 @@ def identity_declared_data end end - describe IDV::IdentityVerificationAttemptRequest do + describe CheckoutSdk::Identities::IdentityVerification::IdentityVerificationAttemptRequest do it 'serializes phone_number and the wider client information' do - info = IDV::IdvIdentityClientInformation.new + info = CheckoutSdk::Identities::IdentityVerification::IdvIdentityClientInformation.new info.pre_selected_document_type = 'Passport' req = described_class.new @@ -164,7 +164,7 @@ def identity_declared_data describe CheckoutSdk::Identities::FaceAuthentication::FaceAuthenticationAttemptRequest do it 'serializes phone_number and keeps the narrow client information' do - info = IDV::IdvClientInformation.new + info = CheckoutSdk::Identities::IdentityVerification::IdvClientInformation.new info.pre_selected_residence_country = 'FR' req = described_class.new @@ -179,7 +179,7 @@ def identity_declared_data end end - describe IDV::IdentityVerificationRequest do + describe CheckoutSdk::Identities::IdentityVerification::IdentityVerificationRequest do it 'carries the five field identity declared data' do req = described_class.new req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' @@ -201,7 +201,7 @@ def identity_declared_data req.applicant_id = 'aplt_tkoi5db4hryu5cei5vwoabr7we' req.user_journey_id = 'usj_tkoi5db4hryu5cei5vwoabr7we' - declared = IDV::IdvDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData.new declared.name = 'Hannah Bret' declared.birth_date = '1994-10-15' req.declared_data = declared @@ -246,7 +246,7 @@ def identity_declared_data describe CheckoutSdk::Identities::AddressDocumentVerification::AddressDocumentVerificationRequest do it 'takes the narrow declared data shape' do - declared = IDV::IdvDeclaredData.new + declared = CheckoutSdk::Identities::IdentityVerification::IdvDeclaredData.new declared.name = 'Hannah Bret' declared.birth_date = '1994-10-15'