diff --git a/CHANGELOG b/CHANGELOG index 32c4e68ea39..7e0fbd635c1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,75 @@ = ActiveMerchant CHANGELOG == HEAD +* Mercury, TransFirst: Repair gateways following updates to `rexml` [aenand] #5206 +* NMI: Fix Decrypted indicator for Google/Apple pay [javierpedrozaing] #5196 +* FlexCharge: add more descriptives error messages [gasb150] #5199 +* Braintree: Updates to Paypal Integration [almalee24] #5190 +* Stripe and Stripe PI: Add metadata and order_id for refund and void [yunnydang] #5204 +* CommerceHub: Update test url [DustinHaefele] #5211 +* Adyen: Fix billing address empty string error [yunnydang] #5208 +* Elavon: Update sending CVV for MIT transactions [almalee24] #5210 +* Adyen: Fix NT integration [jherreraa] #5155 +* HPS: Update NetworkTokenizationCreditCard flow [almalee24] #5178 +* Braintree: Support override_application_id [aenand] #5194 +* Decidir: Pass CVV for NT [almalee24] #5205 +* NMI: Add customer vault fields [yunnydang] #5215 +* CheckoutV2: Add inquire method [almalee24] #5209 +* Decidir & Braintree: Scrub cryptogram and card number [almalee24] #5220 +* Naranja: Update valid number check to include luhn10 [DustinHaefele] #5217 +* Cybersource: Add apple_pay with discover. [DustinHaefele] #5213 +* MercadoPago: Add idempotency key field [yunnydang] #5229 +* Adyen: Update split refund method [yunnydang] #5218 +* Adyen: Remove raw_error_message [almalee24] #5202 +* Elavon: Remove old Stored Credential method [almalee24] #5219 +* PayTrace: Update MultiResponse for Capture [almalee24] #5203 +* Adyen: Add support for Pan Only GooglePay [almalee24] #5221 +* Decidir: Send extra fields for tokenized NT transactions [sinourain] #5224 +* StripePI: Skip add_network_token_cryptogram_and_eci method to accept ApplePay recurring payments [sinourain] #5212 +* Decidir: Fix scrub method after update NT fields [sinourain] #5241 +* Cybersource and Cybersource Rest: Update card type code for Carnet cards [rachelkirk] #5235 +* Stripe PI: Add challenge as valid value for request_three_d_secure [jcreiff] #5238 +* StripePI: Add metadata for GooglePay FPAN [almalee24] #5242 +* Paypal: Add inquire method [almalee24] #5231 +* Adyen: Enable multiple legs within airline data [jcreiff] #5249 +* SafeCharge: Add card holder verification fields [yunnydang] #5252 +* Iveri: Add AuthorisationReversal for Auth Void [almalee24] #5233 +* Stripe PI: Update Stored Credentials [almalee24] #5236 +* Checkout V2: Update stored credential options function [jherreraa] #5239 +* Ebanx: Add support for Stored Credentials [almalee24] #5243 + +== Version 1.137.0 (August 2, 2024) +* Unlock dependency on `rexml` to allow fixing a CVE (#5181). +* Bump Ruby version to 3.1 [dustinhaefele] #5104 +* FlexCharge: Update inquire method to use the new orders end-point +* Worldpay: Prefer options for network_transaction_id [aenand] #5129 +* Braintree: Prefer options for network_transaction_id [aenand] #5129 +* Cybersource Rest: Update support for stored credentials [aenand] #5083 +* Plexo: Add support to NetworkToken payments [euribe09] #5130 +* Braintree: Update card verfification payload if billing address fields are not present [yunnydang] #5142 +* DLocal: Update the phone and ip fields [yunnydang] #5143 +* CheckoutV2: Add support for risk data fields [yunnydang] #5147 +* Pin Payments: Add new 3DS params mentioned in Pin Payments docs [hudakh] #4720 +* RedsysRest: Add support for stored credentials & 3DS exemptions [jherreraa] #5132 +* CheckoutV2: Truncate the reference id for amex transactions [yunnydang] #5151 +* CommerceHub: Add billing address name override [yunnydang] #5157 +* StripePI: Add optional ability for 3DS exemption on verify calls [yunnydang] #5160 +* CyberSource: Update stored credentials [sinourain] #5136 +* Orbital: Update to accept UCAF Indicator GSF [almalee24] #5150 +* CyberSource: Add addtional invoiceHeader fields [yunnydang] #5161 +* MerchantWarrior: Update phone, email, ip and store ID [almalee24] #5158 +* Credorax: Update 3DS version mapping [almalee24] #5159 +* Add Maestro card bins [yunnydang] #5172 +* Braintree: Remove stored credential v1 [almalee24] #5175 +* Braintree Blue: Pass overridden mid into client token for GS 3DS [sinourain] #5166 +* Moneris: Update crypt_type for 3DS [almalee24] #5162 +* CheckoutV2: Update 3DS message & error code [almalee24] #5177 +* DecicirPlus: Update error_message to add safety navigator [almalee24] #5187 +* Elavon: Add updated stored credential version [almalee24] #5170 +* Adyen: Add header fields to response body [yunnydang] #5184 +* Stripe and Stripe PI: Add header fields to response body [yunnydang] #5185 + +== Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 * TNS: Use the specified order_id in request if available [yunnydang] #4880 * Cybersource: Support recurring apple pay [aenand] #4874 @@ -174,6 +243,10 @@ * Worldpay: Add support for deafult ECI value [aenand] #5126 * DLocal: Update stored credentials [sinourain] #5112 * NMI: Add NTID override [yunnydang] #5134 +* Cybersource Rest: Support L2/L3 data [aenand] #5117 +* Worldpay: Support L2/L3 data [aenand] #5117 +* Support UATP cardtype [javierpedrozaing] #5137 +* Litle: Add 141 and 142 as successful responses [almalee24] #5135 == Version 1.135.0 (August 24, 2023) * PaymentExpress: Correct endpoints [steveh] #4827 diff --git a/Gemfile b/Gemfile index e6d1bf0f65c..110928c8299 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem 'jruby-openssl', platforms: :jruby group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec gem 'braintree', '>= 4.14.0' - gem 'jose', '~> 1.1.3' + gem 'jose', '~> 1.2.0' gem 'jwe' gem 'mechanize' gem 'timecop' diff --git a/activemerchant.gemspec b/activemerchant.gemspec index fe9ae0bfc33..67777d0699e 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -13,7 +13,7 @@ Gem::Specification.new do |s| s.email = 'tobi@leetsoft.com' s.homepage = 'http://activemerchant.org/' - s.required_ruby_version = '>= 2.7' + s.required_ruby_version = '>= 3.1' s.files = Dir['CHANGELOG', 'README.md', 'MIT-LICENSE', 'CONTRIBUTORS', 'lib/**/*', 'vendor/**/*'] s.require_path = 'lib' @@ -26,7 +26,7 @@ Gem::Specification.new do |s| s.add_dependency('builder', '>= 2.1.2', '< 4.0.0') s.add_dependency('i18n', '>= 0.6.9') s.add_dependency('nokogiri', '~> 1.4') - s.add_dependency('rexml', '~> 3.2.5') + s.add_dependency('rexml', '~> 3.3', '>= 3.3.4') s.add_development_dependency('mocha', '~> 1') s.add_development_dependency('pry') diff --git a/circle.yml b/circle.yml index 949fa18bb15..d9438f7d281 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: ruby: - version: '2.7.0' + version: '3.1.0' dependencies: cache_directories: diff --git a/lib/active_merchant/billing/credit_card.rb b/lib/active_merchant/billing/credit_card.rb index 70ed215d170..95e7ae5ce38 100644 --- a/lib/active_merchant/billing/credit_card.rb +++ b/lib/active_merchant/billing/credit_card.rb @@ -41,6 +41,7 @@ module Billing #:nodoc: # * Panal # * Verve # * Tuya + # * UATP # # For testing purposes, use the 'bogus' credit card brand. This skips the vast majority of # validations, allowing you to focus on your core concerns until you're ready to be more concerned @@ -136,6 +137,7 @@ def number=(value) # * +'panal'+ # * +'verve'+ # * +'tuya'+ + # * +'uatp'+ # # Or, if you wish to test your implementation, +'bogus'+. # diff --git a/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index 0366a499065..dd524d3f814 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -53,7 +53,8 @@ module CreditCardMethods 'hipercard' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), HIPERCARD_RANGES) }, 'panal' => ->(num) { num&.size == 16 && in_bin_range?(num.slice(0, 6), PANAL_RANGES) }, 'verve' => ->(num) { (16..19).cover?(num&.size) && in_bin_range?(num.slice(0, 6), VERVE_RANGES) }, - 'tuya' => ->(num) { num =~ /^588800\d{10}$/ } + 'tuya' => ->(num) { num =~ /^588800\d{10}$/ }, + 'uatp' => ->(num) { num =~ /^(1175|1290)\d{11}$/ } } SODEXO_NO_LUHN = ->(num) { num =~ /^(505864|505865)\d{10}$/ } @@ -127,6 +128,7 @@ module CreditCardMethods 501879 502113 502120 502121 502301 503175 503337 503645 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 + 505616 507001 507002 507004 507082 507090 560014 560565 561033 572402 572610 572626 @@ -174,7 +176,7 @@ module CreditCardMethods (501104..501105), (501107..501108), (501104..501105), - (501107..501108), + (501107..501109), (501800..501899), (502000..502099), (503800..503899), @@ -462,7 +464,7 @@ def sodexo_no_luhn?(numbers) def valid_by_algorithm?(brand, numbers) #:nodoc: case brand when 'naranja' - valid_naranja_algo?(numbers) + valid_naranja_algo?(numbers) || valid_luhn?(numbers) when 'creditel' valid_creditel_algo?(numbers) when 'alia', 'confiable', 'maestro_no_luhn', 'anda', 'tarjeta-d', 'hipercard' diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index 465be06170b..b6977a7c5c1 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -63,7 +63,7 @@ def authorize(money, payment, options = {}) add_3ds(post, options) add_3ds_authenticated_data(post, options) add_splits(post, options) - add_recurring_contract(post, options) + add_recurring_contract(post, options, payment) add_network_transaction_reference(post, options) add_application_info(post, options) add_level_2_data(post, options) @@ -247,8 +247,7 @@ def scrub(transcript) def add_extra_data(post, payment, options) post[:telephoneNumber] = (options[:billing_address][:phone_number] if options.dig(:billing_address, :phone_number)) || (options[:billing_address][:phone] if options.dig(:billing_address, :phone)) || '' - post[:selectedBrand] = options[:selected_brand] if options[:selected_brand] - post[:selectedBrand] ||= NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) + post[:selectedBrand] = options[:selected_brand] if options[:selected_brand] && !post[:selectedBrand] post[:deliveryDate] = options[:delivery_date] if options[:delivery_date] post[:merchantOrderReference] = options[:merchant_order_reference] if options[:merchant_order_reference] post[:captureDelayHours] = options[:capture_delay_hours] if options[:capture_delay_hours] @@ -274,7 +273,6 @@ def add_additional_data(post, payment, options) post[:additionalData] ||= {} post[:additionalData][:overwriteBrand] = normalize(options[:overwrite_brand]) if options[:overwrite_brand] post[:additionalData][:customRoutingFlag] = options[:custom_routing_flag] if options[:custom_routing_flag] - post[:additionalData]['paymentdatasource.type'] = NETWORK_TOKENIZATION_CARD_SOURCE[payment.source.to_s] if payment.is_a?(NetworkTokenizationCreditCard) post[:additionalData][:authorisationType] = options[:authorisation_type] if options[:authorisation_type] post[:additionalData][:adjustAuthorisationData] = options[:adjust_authorisation_data] if options[:adjust_authorisation_data] post[:additionalData][:industryUsage] = options[:industry_usage] if options[:industry_usage] @@ -362,6 +360,26 @@ def add_data_airline(post, options) post[:additionalData].merge!(extract_and_transform(leg_data, options[:additional_data_airline][:leg])) end + # temporary duplication with minor modification (:legs array with nested hashes instead of a single :leg hash) + # this should preserve backward-compatibility with :leg logic above until it is deprecated/removed + if options[:additional_data_airline][:legs].present? + options[:additional_data_airline][:legs].each_with_index do |leg, number| + leg_data = %w[ + carrier_code + class_of_travel + date_of_travel + depart_airport + depart_tax + destination_code + fare_base_code + flight_number + stop_over_code + ].each_with_object({}) { |value, hash| hash["airline.leg#{number + 1}.#{value}"] = value } + + post[:additionalData].merge!(extract_and_transform(leg_data, leg)) + end + end + if options[:additional_data_airline][:passenger].present? passenger_data = %w[ date_of_birth @@ -435,16 +453,17 @@ def add_splits(post, options) splits = [] split_data.each do |split| - amount = { - value: split['amount']['value'] - } - amount[:currency] = split['amount']['currency'] if split['amount']['currency'] + if split['amount'] + amount = {} + amount[:value] = split['amount']['value'] if split['amount']['value'] + amount[:currency] = split['amount']['currency'] if split['amount']['currency'] + end split_hash = { - amount: amount, type: split['type'], reference: split['reference'] } + split_hash[:amount] = amount unless amount.nil? split_hash['account'] = split['account'] if split['account'] splits.push(split_hash) end @@ -465,9 +484,7 @@ def add_shopper_reference(post, options) end def add_shopper_interaction(post, payment, options = {}) - if (options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) == 'cardholder') || - (payment.respond_to?(:verification_value) && payment.verification_value && options.dig(:stored_credential, :initial_transaction).nil?) || - payment.is_a?(NetworkTokenizationCreditCard) + if ecommerce_shopper_interaction?(payment, options) shopper_interaction = 'Ecommerce' else shopper_interaction = 'ContAuth' @@ -510,9 +527,12 @@ def add_address(post, options) end def add_billing_address(post, options, address) + address[:address1] = 'NA' if address[:address1].blank? + address[:address2] = 'NA' if address[:address2].blank? + post[:billingAddress] = {} - post[:billingAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] || 'NA' - post[:billingAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] || 'NA' + post[:billingAddress][:street] = options[:address_override] == true ? address[:address2] : address[:address1] + post[:billingAddress][:houseNumberOrName] = options[:address_override] == true ? address[:address1] : address[:address2] post[:billingAddress][:postalCode] = address[:zip] if address[:zip] post[:billingAddress][:city] = address[:city] || 'NA' post[:billingAddress][:stateOrProvince] = get_state(address) @@ -555,7 +575,7 @@ def add_payment(post, payment, options, action = nil) elsif payment.is_a?(Check) add_bank_account(post, payment, options, action) else - add_mpi_data_for_network_tokenization_card(post, payment, options) if payment.is_a?(NetworkTokenizationCreditCard) + add_network_tokenization_card(post, payment, options) if payment.is_a?(NetworkTokenizationCreditCard) || options[:wallet_type] == :google_pay add_card(post, payment) end end @@ -617,25 +637,46 @@ def add_reference(post, authorization, options = {}) post[:originalReference] = original_reference end - def add_mpi_data_for_network_tokenization_card(post, payment, options) - return if options[:skip_mpi_data] == 'Y' + def add_network_tokenization_card(post, payment, options) + selected_brand = NETWORK_TOKENIZATION_CARD_SOURCE[options[:wallet_type]&.to_s || payment.source.to_s] + if selected_brand + post[:selectedBrand] = selected_brand + post[:additionalData] = {} unless post[:additionalData] + post[:additionalData]['paymentdatasource.type'] = selected_brand + post[:additionalData]['paymentdatasource.tokenized'] = options[:wallet_type] ? 'false' : 'true' if selected_brand == 'googlepay' + end + + return if skip_mpi_data?(options) - post[:mpiData] = {} - post[:mpiData][:authenticationResponse] = 'Y' - post[:mpiData][:cavv] = payment.payment_cryptogram - post[:mpiData][:directoryResponse] = 'Y' - post[:mpiData][:eci] = payment.eci || '07' + post[:mpiData] = { + authenticationResponse: 'Y', + directoryResponse: 'Y', + eci: payment.eci || '07' + } + if payment.try(:network_token?) && options[:switch_cryptogram_mapping_nt] + post[:mpiData][:tokenAuthenticationVerificationValue] = payment.payment_cryptogram + else + post[:mpiData][:cavv] = payment.payment_cryptogram + end end - def add_recurring_contract(post, options = {}) - return unless options[:recurring_contract_type] + def add_recurring_contract(post, options = {}, payment = nil) + return unless options[:recurring_contract_type] || (payment.try(:network_token?) && options[:switch_cryptogram_mapping_nt]) - post[:recurring] = {} - post[:recurring][:contract] = options[:recurring_contract_type] + post[:recurring] ||= {} + post[:recurring][:contract] = options[:recurring_contract_type] if options[:recurring_contract_type] post[:recurring][:recurringDetailName] = options[:recurring_detail_name] if options[:recurring_detail_name] post[:recurring][:recurringExpiry] = options[:recurring_expiry] if options[:recurring_expiry] post[:recurring][:recurringFrequency] = options[:recurring_frequency] if options[:recurring_frequency] post[:recurring][:tokenService] = options[:token_service] if options[:token_service] + + if payment.try(:network_token?) && options[:switch_cryptogram_mapping_nt] + post[:recurring][:contract] = 'EXTERNAL' + post[:recurring][:tokenService] = case payment.brand + when 'visa' then 'VISATOKENSERVICE' + else 'MCTOKENSERVICE' + end + end end def add_application_info(post, options) @@ -755,10 +796,34 @@ def add_metadata(post, options = {}) post[:metadata].merge!(options[:metadata]) if options[:metadata] end + def add_header_fields(response) + return unless @response_headers.present? + + headers = {} + headers['response_headers'] = {} + headers['response_headers']['transient_error'] = @response_headers['transient-error'] if @response_headers['transient-error'] + + response.merge!(headers) + end + def parse(body) return {} if body.blank? - JSON.parse(body) + response = JSON.parse(body) + add_header_fields(response) + response + end + + # Override the regular handle response so we can access the headers + # set header fields and values so we can add them to the response body + def handle_response(response) + @response_headers = response.each_header.to_h if response.respond_to?(:header) + case response.code.to_i + when 200...300 + response.body + else + raise ResponseError.new(response) + end end def commit(action, parameters, options) @@ -865,8 +930,6 @@ def message_from(action, response, options = {}) end def authorize_message_from(response, options = {}) - return raw_authorize_error_message(response) if options[:raw_error_message] - if response['refusalReason'] && response['additionalData'] && (response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']) "#{response['refusalReason']} | #{response['additionalData']['merchantAdviceCode'] || response['additionalData']['refusalReasonRaw']}" else @@ -874,14 +937,6 @@ def authorize_message_from(response, options = {}) end end - def raw_authorize_error_message(response) - if response['refusalReason'] && response['additionalData'] && response['additionalData']['refusalReasonRaw'] - "#{response['refusalReason']} | #{response['additionalData']['refusalReasonRaw']}" - else - response['refusalReason'] || response['resultCode'] || response['message'] || response['result'] - end - end - def authorization_from(action, parameters, response) return nil if response['pspReference'].nil? @@ -903,7 +958,10 @@ def post_data(action, parameters = {}) end def error_code_from(response) - response.dig('additionalData', 'refusalReasonRaw').try(:scan, /^\d+/).try(:first) || STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || response['errorCode'] || response['refusalReason'] + response.dig('additionalData', 'refusalReasonRaw').try(:match, /^([a-zA-Z0-9 ]{1,5})(?=:)/).try(:[], 1).try(:strip) || + STANDARD_ERROR_CODE_MAPPING[response['errorCode']] || + response['errorCode'] || + response['refusalReason'] end def network_transaction_id_from(response) @@ -941,6 +999,17 @@ def unsupported_failure_response(initial_response) def card_not_stored?(response) response.authorization ? response.authorization.split('#')[2].nil? : true end + + def skip_mpi_data?(options = {}) + # Skips adding the NT mpi data if it is explicitly skipped in options, or if it is MIT and not the initial transaction. + options[:skip_mpi_data] == 'Y' || options[:wallet_type] || (!options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) == 'merchant' && options[:switch_cryptogram_mapping_nt]) + end + + def ecommerce_shopper_interaction?(payment, options) + (options.dig(:stored_credential, :initial_transaction) && options.dig(:stored_credential, :initiator) == 'cardholder') || + (payment.respond_to?(:verification_value) && payment.verification_value && options.dig(:stored_credential, :initial_transaction)) || + (payment.is_a?(NetworkTokenizationCreditCard) && !options[:switch_cryptogram_mapping_nt]) + end end end end diff --git a/lib/active_merchant/billing/gateways/braintree/braintree_common.rb b/lib/active_merchant/billing/gateways/braintree/braintree_common.rb index 165d8faaa90..1d9f1df0890 100644 --- a/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +++ b/lib/active_merchant/billing/gateways/braintree/braintree_common.rb @@ -23,6 +23,8 @@ def scrub(transcript) gsub(%r(()[^<]+()), '\1[FILTERED]\2'). gsub(%r(()[^<]+()), '\1[FILTERED]\2'). gsub(%r(()[^<]{100,}()), '\1[FILTERED]\2'). - gsub(%r(()[^<]+()), '\1[FILTERED]\2') + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()[^<]+()), '\1[FILTERED]\2') end end diff --git a/lib/active_merchant/billing/gateways/braintree/token_nonce.rb b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb index dc9a3e0bc90..67cfbc5b7a0 100644 --- a/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +++ b/lib/active_merchant/billing/gateways/braintree/token_nonce.rb @@ -18,10 +18,10 @@ def url "https://payments#{'.sandbox' if sandbox}.braintree-api.com/graphql" end - def create_token_nonce_for_payment_method(payment_method) + def create_token_nonce_for_payment_method(payment_method, options = {}) headers = { 'Accept' => 'application/json', - 'Authorization' => "Bearer #{client_token}", + 'Authorization' => "Bearer #{client_token(options)['authorizationFingerprint']}", 'Content-Type' => 'application/json', 'Braintree-Version' => '2018-05-10' } @@ -34,9 +34,9 @@ def create_token_nonce_for_payment_method(payment_method) return token, message end - def client_token - base64_token = @braintree_gateway.client_token.generate - JSON.parse(Base64.decode64(base64_token))['authorizationFingerprint'] + def client_token(options = {}) + base64_token = @braintree_gateway.client_token.generate({ merchant_account_id: options[:merchant_account_id] || @options[:merchant_account_id] }.compact) + JSON.parse(Base64.decode64(base64_token)) end private diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 8d5a98bb1a8..b5d38cebcbd 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -144,16 +144,21 @@ def verify(creditcard, options = {}) exp_month = creditcard.month.to_s exp_year = creditcard.year.to_s expiration = "#{exp_month}/#{exp_year}" + zip = options[:billing_address].try(:[], :zip) + address1 = options[:billing_address].try(:[], :address1) payload = { credit_card: { number: creditcard.number, expiration_date: expiration, - cvv: creditcard.verification_value, - billing_address: { - postal_code: options[:billing_address][:zip] - } + cvv: creditcard.verification_value } } + if zip || address1 + payload[:credit_card][:billing_address] = {} + payload[:credit_card][:billing_address][:postal_code] = zip if zip + payload[:credit_card][:billing_address][:street_address] = address1 if address1 + end + if merchant_account_id = (options[:merchant_account_id] || @merchant_account_id) payload[:options] = { merchant_account_id: merchant_account_id } end @@ -565,14 +570,17 @@ def create_transaction(transaction_type, money, credit_card_or_vault_id, options transaction_params = create_transaction_parameters(money, credit_card_or_vault_id, options) commit do result = @braintree_gateway.transaction.send(transaction_type, transaction_params) - make_default_payment_method_token(result) if options.dig(:paypal, :paypal_flow_type) == 'checkout_with_vault' && result.success? + make_default_payment_method_token(result, options) response = Response.new(result.success?, message_from_transaction_result(result), response_params(result), response_options(result)) response.cvv_result['message'] = '' response end end - def make_default_payment_method_token(result) + def make_default_payment_method_token(result, options) + return if options[:prevent_default_payment_method] + return unless options.dig(:paypal, :paypal_flow_type) == 'checkout_with_vault' && result.success? + @braintree_gateway.customer.update( result.transaction.customer_details.id, default_payment_method_token: result.transaction.paypal_details.implicitly_vaulted_payment_method_token @@ -680,7 +688,8 @@ def transaction_hash(result) paypal_details = { 'payer_id' => transaction.paypal_details.payer_id, - 'payer_email' => transaction.paypal_details.payer_email + 'payer_email' => transaction.paypal_details.payer_email, + 'paypal_payment_token' => transaction.paypal_details.implicitly_vaulted_payment_method_token || transaction.paypal_details.token } if transaction.risk_data @@ -813,7 +822,7 @@ def add_addresses(parameters, options) end def add_channel(parameters, options) - channel = @options[:channel] || application_id + channel = options[:override_application_id] || @options[:channel] || application_id parameters[:channel] = channel if channel end @@ -913,19 +922,11 @@ def add_stored_credential_data(parameters, credit_card_or_vault_id, options) # specifically requested. This will be the default behavior in a future release. return unless (stored_credential = options[:stored_credential]) - add_external_vault(parameters, stored_credential) - - if options[:stored_credentials_v2] - stored_credentials_v2(parameters, stored_credential) - else - stored_credentials_v1(parameters, stored_credential) - end + add_external_vault(parameters, options) + stored_credentials(parameters, stored_credential) end - def stored_credentials_v2(parameters, stored_credential) - # Differences between v1 and v2 are - # initial_transaction + recurring/installment should be labeled {{reason_type}}_first - # unscheduled in AM should map to '' at BT because unscheduled here means not on a fixed timeline or fixed amount + def stored_credentials(parameters, stored_credential) case stored_credential[:reason_type] when 'recurring', 'installment' if stored_credential[:initial_transaction] @@ -942,27 +943,14 @@ def stored_credentials_v2(parameters, stored_credential) end end - def stored_credentials_v1(parameters, stored_credential) - if stored_credential[:initiator] == 'merchant' - if stored_credential[:reason_type] == 'installment' - parameters[:transaction_source] = 'recurring' - else - parameters[:transaction_source] = stored_credential[:reason_type] - end - elsif %w(recurring_first moto).include?(stored_credential[:reason_type]) - parameters[:transaction_source] = stored_credential[:reason_type] - else - parameters[:transaction_source] = '' - end - end - - def add_external_vault(parameters, stored_credential) + def add_external_vault(parameters, options = {}) + stored_credential = options[:stored_credential] parameters[:external_vault] = {} if stored_credential[:initial_transaction] parameters[:external_vault][:status] = 'will_vault' else parameters[:external_vault][:status] = 'vaulted' - parameters[:external_vault][:previous_network_transaction_id] = stored_credential[:network_transaction_id] + parameters[:external_vault][:previous_network_transaction_id] = options[:network_transaction_id] || stored_credential[:network_transaction_id] end end @@ -1059,7 +1047,7 @@ def bank_account_errors(payment_method, options) end def add_bank_account_to_customer(payment_method, options) - bank_account_nonce, error_message = TokenNonce.new(@braintree_gateway, options).create_token_nonce_for_payment_method payment_method + bank_account_nonce, error_message = TokenNonce.new(@braintree_gateway, options).create_token_nonce_for_payment_method(payment_method, options) return Response.new(false, error_message) unless bank_account_nonce.present? result = @braintree_gateway.payment_method.create( diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index 28cd1014a0c..631c552a5cb 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -33,6 +33,7 @@ def authorize(amount, payment_method, options = {}) post = {} post[:capture] = false build_auth_or_purchase(post, amount, payment_method, options) + options[:incremental_authorization] ? commit(:incremental_authorize, post, options, options[:incremental_authorization]) : commit(:authorize, post, options) end @@ -80,6 +81,10 @@ def verify(credit_card, options = {}) authorize(0, credit_card, options) end + def inquire(authorization, options = {}) + verify_payment(authorization, {}) + end + def verify_payment(authorization, options = {}) commit(:verify_payment, nil, options, authorization, :get) end @@ -144,6 +149,8 @@ def build_auth_or_purchase(post, amount, payment_method, options) add_recipient_data(post, options) add_processing_data(post, options) add_payment_sender_data(post, options) + add_risk_data(post, options) + truncate_amex_reference_id(post, options, payment_method) end def add_invoice(post, money, options) @@ -159,6 +166,10 @@ def add_invoice(post, money, options) post[:metadata][:udf5] = application_id || 'ActiveMerchant' end + def truncate_amex_reference_id(post, options, payment_method) + post[:reference] = truncate(options[:order_id], 30) if payment_method.respond_to?(:brand) && payment_method.brand == 'american_express' + end + def add_recipient_data(post, options) return unless options[:recipient].is_a?(Hash) @@ -191,6 +202,20 @@ def add_processing_data(post, options) post[:processing] = options[:processing] end + def add_risk_data(post, options) + return unless options[:risk].is_a?(Hash) + + risk = options[:risk] + post[:risk] = {} unless risk.empty? + + if risk[:enabled].to_s == 'true' + post[:risk][:enabled] = true + post[:risk][:device_session_id] = risk[:device_session_id] if risk[:device_session_id] + elsif risk[:enabled].to_s == 'false' + post[:risk][:enabled] = false + end + end + def add_payment_sender_data(post, options) return unless options[:sender].is_a?(Hash) @@ -344,6 +369,7 @@ def add_transaction_data(post, options = {}) end def merchant_initiated_override(post, options) + post[:payment_type] ||= 'Regular' post[:merchant_initiated] = true post[:source][:stored] = true post[:previous_payment_id] = options[:merchant_initiated_transaction_id] @@ -362,7 +388,7 @@ def add_stored_credentials_using_normalized_fields(post, options) def add_stored_credential_options(post, options = {}) return unless options[:stored_credential] - post[:payment_type] = 'Recurring' if %w(recurring installment).include? options[:stored_credential][:reason_type] + post[:payment_type] = options[:stored_credential][:reason_type]&.capitalize if options[:merchant_initiated_transaction_id] merchant_initiated_override(post, options) @@ -640,12 +666,7 @@ def message_from(succeeded, response, options) elsif response['error_type'] response['error_type'] + ': ' + response['error_codes'].first else - response_summary = if options[:threeds_response_message] - response['response_summary'] || response.dig('actions', 0, 'response_summary') - else - response['response_summary'] - end - + response_summary = response['response_summary'] || response.dig('actions', 0, 'response_summary') response_summary || response['response_code'] || response['status'] || response['message'] || 'Unable to read error message' end end @@ -675,11 +696,7 @@ def error_code_from(succeeded, response, options) elsif response['error_type'] response['error_type'] else - response_code = if options[:threeds_response_message] - response['response_code'] || response.dig('actions', 0, 'response_code') - else - response['response_code'] - end + response_code = response['response_code'] || response.dig('actions', 0, 'response_code') STANDARD_ERROR_CODE_MAPPING[response_code] end diff --git a/lib/active_merchant/billing/gateways/commerce_hub.rb b/lib/active_merchant/billing/gateways/commerce_hub.rb index 3bbd52925cc..b254ad5b075 100644 --- a/lib/active_merchant/billing/gateways/commerce_hub.rb +++ b/lib/active_merchant/billing/gateways/commerce_hub.rb @@ -1,7 +1,7 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class CommerceHubGateway < Gateway - self.test_url = 'https://cert.api.fiservapps.com/ch' + self.test_url = 'https://connect-cert.fiservapps.com/ch' self.live_url = 'https://prod.api.fiservapps.com/ch' self.supported_countries = ['US'] @@ -112,6 +112,7 @@ def scrub(transcript) transcript. gsub(%r((Authorization: )[a-zA-Z0-9+./=]+), '\1[FILTERED]'). gsub(%r((Api-Key: )\w+), '\1[FILTERED]'). + gsub(%r(("apiKey\\?":\\?")\w+), '\1[FILTERED]'). gsub(%r(("cardData\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r(("securityCode\\?":\\?")\d+), '\1[FILTERED]'). gsub(%r(("cavv\\?":\\?")\w+), '\1[FILTERED]') @@ -171,10 +172,7 @@ def add_billing_address(post, payment, options) return unless billing = options[:billing_address] billing_address = {} - if payment.is_a?(CreditCard) - billing_address[:firstName] = payment.first_name if payment.first_name - billing_address[:lastName] = payment.last_name if payment.last_name - end + name_from_address(billing_address, billing) || name_from_payment(billing_address, payment) address = {} address[:street] = billing[:address1] if billing[:address1] address[:houseNumberOrName] = billing[:address2] if billing[:address2] @@ -192,6 +190,22 @@ def add_billing_address(post, payment, options) post[:billingAddress] = billing_address end + def name_from_payment(billing_address, payment) + return unless payment.respond_to?(:first_name) && payment.respond_to?(:last_name) + + billing_address[:firstName] = payment.first_name if payment.first_name + billing_address[:lastName] = payment.last_name if payment.last_name + end + + def name_from_address(billing_address, billing) + return unless address = billing + + first_name, last_name = split_names(address[:name]) if address[:name] + + billing_address[:firstName] = first_name if first_name + billing_address[:lastName] = last_name if last_name + end + def add_shipping_address(post, options) return unless shipping = options[:shipping_address] diff --git a/lib/active_merchant/billing/gateways/credorax.rb b/lib/active_merchant/billing/gateways/credorax.rb index 80b241616c0..22ff57b5c21 100644 --- a/lib/active_merchant/billing/gateways/credorax.rb +++ b/lib/active_merchant/billing/gateways/credorax.rb @@ -415,7 +415,7 @@ def add_normalized_3d_secure_2_data(post, options) three_d_secure_options[:eci], three_d_secure_options[:cavv] ) - post[:'3ds_version'] = three_d_secure_options[:version]&.start_with?('2') ? '2.0' : three_d_secure_options[:version] + post[:'3ds_version'] = three_d_secure_options[:version] == '2' ? '2.0' : three_d_secure_options[:version] post[:'3ds_dstrxid'] = three_d_secure_options[:ds_transaction_id] end diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index 78cc67b7d5d..108c6f64d95 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -62,7 +62,8 @@ class CyberSourceGateway < Gateway jcb: '007', dankort: '034', maestro: '042', - elo: '054' + elo: '054', + carnet: '002' } @@decision_codes = { @@ -176,7 +177,7 @@ def initialize(options = {}) end def authorize(money, payment_method, options = {}) - if valid_payment_method?(payment_method) + if valid_payment_method?(payment_method, options) setup_address_hash(options) commit(build_auth_request(money, payment_method, options), :authorize, money, options) else @@ -192,7 +193,7 @@ def capture(money, authorization, options = {}) end def purchase(money, payment_method, options = {}) - if valid_payment_method?(payment_method) + if valid_payment_method?(payment_method, options) setup_address_hash(options) commit(build_purchase_request(money, payment_method, options), :purchase, money, options) else @@ -232,7 +233,7 @@ def credit(money, creditcard_or_reference, options = {}) # To charge the card while creating a profile, pass # options[:setup_fee] => money def store(payment_method, options = {}) - if valid_payment_method?(payment_method) + if valid_payment_method?(payment_method, options) setup_address_hash(options) commit(build_create_subscription_request(payment_method, options), :store, nil, options) else @@ -320,10 +321,12 @@ def verify_credentials private - def valid_payment_method?(payment_method) + def valid_payment_method?(payment_method, options) return true unless payment_method.is_a?(NetworkTokenizationCreditCard) - %w(visa master american_express).include?(card_brand(payment_method)) + brands = %w(visa master american_express) + brands << 'discover' if options[:enable_cybs_discover_apple_pay] + brands.include?(card_brand(payment_method)) end # Create all required address hash key value pairs @@ -609,12 +612,24 @@ def add_merchant_data(xml, options) end def add_merchant_descriptor(xml, options) - return unless options[:merchant_descriptor] || options[:user_po] || options[:taxable] || options[:reference_data_code] || options[:invoice_number] + return unless options[:merchant_descriptor] || + options[:user_po] || + options[:taxable] || + options[:reference_data_code] || + options[:invoice_number] || + options[:merchant_descriptor_city] || + options[:submerchant_id] || + options[:merchant_descriptor_state] || + options[:merchant_descriptor_country] xml.tag! 'invoiceHeader' do xml.tag! 'merchantDescriptor', options[:merchant_descriptor] if options[:merchant_descriptor] + xml.tag! 'merchantDescriptorCity', options[:merchant_descriptor_city] if options[:merchant_descriptor_city] + xml.tag! 'merchantDescriptorState', options[:merchant_descriptor_state] if options[:merchant_descriptor_state] + xml.tag! 'merchantDescriptorCountry', options[:merchant_descriptor_country] if options[:merchant_descriptor_country] xml.tag! 'userPO', options[:user_po] if options[:user_po] xml.tag! 'taxable', options[:taxable] if options[:taxable] + xml.tag! 'submerchantID', options[:submerchant_id] if options[:submerchant_id] xml.tag! 'referenceDataCode', options[:reference_data_code] if options[:reference_data_code] xml.tag! 'invoiceNumber', options[:invoice_number] if options[:invoice_number] end @@ -860,13 +875,15 @@ def add_threeds_2_ucaf_data(xml, payment_method, options) end def stored_credential_commerce_indicator(options) - return unless options[:stored_credential] + return unless (reason_type = options.dig(:stored_credential, :reason_type)) - return if options[:stored_credential][:initial_transaction] - - case options[:stored_credential][:reason_type] - when 'installment' then 'install' - when 'recurring' then 'recurring' + case reason_type + when 'installment' + 'install' + when 'recurring' + 'recurring' + else + 'internet' end end @@ -882,9 +899,10 @@ def subsequent_nt_apple_pay_auth(source, options) end def add_auth_network_tokenization(xml, payment_method, options) + commerce_indicator = stored_credential_commerce_indicator(options) || 'internet' xml.tag! 'ccAuthService', { 'run' => 'true' } do xml.tag!('networkTokenCryptogram', payment_method.payment_cryptogram) - xml.tag!('commerceIndicator', 'internet') + xml.tag!('commerceIndicator', commerce_indicator) xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end end @@ -915,6 +933,13 @@ def add_auth_wallet(xml, payment_method, options) xml.tag!('xid', Base64.encode64(cryptogram[20...40])) if cryptogram.bytes.count > 20 xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id] end + when :discover + return unless options[:enable_cybs_discover_apple_pay] + + xml.tag! 'ccAuthService', { 'run' => 'true' } do + xml.tag!('cavv', payment_method.payment_cryptogram) unless commerce_indicator + xml.tag!('commerceIndicator', 'internet') + end end end @@ -1104,7 +1129,7 @@ def add_stored_credential_subsequent_auth(xml, options = {}) def add_stored_credential_options(xml, options = {}) return unless options[:stored_credential] || options[:stored_credential_overrides] - stored_credential_subsequent_auth_first = 'true' if options.dig(:stored_credential, :initial_transaction) + stored_credential_subsequent_auth_first = 'true' if cardholder_or_initiated_transaction?(options) stored_credential_transaction_id = options.dig(:stored_credential, :network_transaction_id) if options.dig(:stored_credential, :initiator) == 'merchant' stored_credential_subsequent_auth_stored_cred = 'true' if subsequent_cardholder_initiated_transaction?(options) || unscheduled_merchant_initiated_transaction?(options) || threeds_stored_credential_exemption?(options) @@ -1117,6 +1142,10 @@ def add_stored_credential_options(xml, options = {}) xml.subsequentAuthStoredCredential override_subsequent_auth_stored_cred.nil? ? stored_credential_subsequent_auth_stored_cred : override_subsequent_auth_stored_cred end + def cardholder_or_initiated_transaction?(options) + options.dig(:stored_credential, :initiator) == 'cardholder' || options.dig(:stored_credential, :initial_transaction) + end + def subsequent_cardholder_initiated_transaction?(options) options.dig(:stored_credential, :initiator) == 'cardholder' && !options.dig(:stored_credential, :initial_transaction) end diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index 0a5e65711c8..b0f9273744d 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -28,7 +28,8 @@ class CyberSourceRestGateway < Gateway maestro: '042', master: '002', unionpay: '062', - visa: '001' + visa: '001', + carnet: '002' } WALLET_PAYMENT_SOLUTION = { @@ -106,6 +107,23 @@ def scrub(transcript) private + def add_level_2_data(post, options) + return unless options[:purchase_order_number] + + post[:orderInformation][:invoiceDetails] ||= {} + post[:orderInformation][:invoiceDetails][:purchaseOrderNumber] = options[:purchase_order_number] + end + + def add_level_3_data(post, options) + return unless options[:line_items] + + post[:orderInformation][:lineItems] = options[:line_items] + post[:processingInformation][:purchaseLevel] = '3' + post[:orderInformation][:shipping_details] = { shipFromPostalCode: options[:ships_from_postal_code] } + post[:orderInformation][:amountDetails] ||= {} + post[:orderInformation][:amountDetails][:discountAmount] = options[:discount_amount] + end + def add_three_ds(post, payment_method, options) return unless three_d_secure = options[:three_d_secure] @@ -149,6 +167,8 @@ def build_auth_request(amount, payment, options) add_partner_solution_id(post) add_stored_credentials(post, payment, options) add_three_ds(post, payment, options) + add_level_2_data(post, options) + add_level_3_data(post, options) end.compact end @@ -294,56 +314,43 @@ def add_merchant_description(post, options) end def add_stored_credentials(post, payment, options) - return unless stored_credential = options[:stored_credential] + return unless options[:stored_credential] - options = stored_credential_options(stored_credential, options.fetch(:reason_code, '')) - post[:processingInformation][:commerceIndicator] = options.fetch(:transaction_type, 'internet') - stored_credential[:initial_transaction] ? initial_transaction(post, options) : subsequent_transaction(post, options) + post[:processingInformation][:commerceIndicator] = commerce_indicator(options.dig(:stored_credential, :reason_type)) + add_authorization_options(post, payment, options) end - def stored_credential_options(options, reason_code) - transaction_type = options[:reason_type] - transaction_type = 'install' if transaction_type == 'installment' - initiator = options[:initiator] if options[:initiator] - initiator = 'customer' if initiator == 'cardholder' - stored_on_file = options[:reason_type] == 'recurring' - options.merge({ - transaction_type: transaction_type, - initiator: initiator, - reason_code: reason_code, - stored_on_file: stored_on_file - }) + def commerce_indicator(reason_type) + case reason_type + when 'recurring' + 'recurring' + when 'installment' + 'install' + else + 'internet' + end end - def add_processing_information(initiator, merchant_initiated_transaction_hash = {}) - { + def add_authorization_options(post, payment, options) + initiator = options.dig(:stored_credential, :initiator) == 'cardholder' ? 'customer' : 'merchant' + authorization_options = { authorizationOptions: { initiator: { - type: initiator, - merchantInitiatedTransaction: merchant_initiated_transaction_hash, - storedCredentialUsed: true + type: initiator } } }.compact - end - - def initial_transaction(post, options) - processing_information = add_processing_information(options[:initiator], { - reason: options[:reason_code] - }) - post[:processingInformation].merge!(processing_information) - end - - def subsequent_transaction(post, options) - network_transaction_id = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) || '' - processing_information = add_processing_information(options[:initiator], { - originalAuthorizedAmount: post.dig(:orderInformation, :amountDetails, :totalAmount), - previousTransactionID: network_transaction_id, - reason: options[:reason_code], - storedCredentialUsed: options[:stored_on_file] - }) - post[:processingInformation].merge!(processing_information) + authorization_options[:authorizationOptions][:initiator][:storedCredentialUsed] = true if initiator == 'merchant' + authorization_options[:authorizationOptions][:initiator][:credentialStoredOnFile] = true if options.dig(:stored_credential, :initial_transaction) + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction] ||= {} + unless options.dig(:stored_credential, :initial_transaction) + network_transaction_id = options[:network_transaction_id] || options.dig(:stored_credential, :network_transaction_id) || '' + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:previousTransactionID] = network_transaction_id + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:originalAuthorizedAmount] = post.dig(:orderInformation, :amountDetails, :totalAmount) if card_brand(payment) == 'discover' + end + authorization_options[:authorizationOptions][:initiator][:merchantInitiatedTransaction][:reason] = options[:reason_code] if options[:reason_code] + post[:processingInformation].merge!(authorization_options) end def network_transaction_id_from(response) @@ -477,7 +484,8 @@ def add_sec_code(post, options) def add_invoice_number(post, options) return unless options[:invoice_number].present? - post[:orderInformation][:invoiceDetails] = { invoiceNumber: options[:invoice_number] } + post[:orderInformation][:invoiceDetails] ||= {} + post[:orderInformation][:invoiceDetails][:invoiceNumber] = options[:invoice_number] end def add_partner_solution_id(post) diff --git a/lib/active_merchant/billing/gateways/d_local.rb b/lib/active_merchant/billing/gateways/d_local.rb index c98d551ceec..9b52fe21488 100644 --- a/lib/active_merchant/billing/gateways/d_local.rb +++ b/lib/active_merchant/billing/gateways/d_local.rb @@ -118,16 +118,18 @@ def lookup_country_code(country_field) def add_payer(post, card, options) address = options[:billing_address] || options[:address] + phone_number = address[:phone] || address[:phone_number] if address + post[:payer] = {} post[:payer][:name] = card.name post[:payer][:email] = options[:email] if options[:email] post[:payer][:birth_date] = options[:birth_date] if options[:birth_date] - post[:payer][:phone] = address[:phone] if address && address[:phone] + post[:payer][:phone] = phone_number if phone_number post[:payer][:document] = options[:document] if options[:document] post[:payer][:document2] = options[:document2] if options[:document2] post[:payer][:user_reference] = options[:user_reference] if options[:user_reference] post[:payer][:event_uuid] = options[:device_id] if options[:device_id] - post[:payer][:onboarding_ip_address] = options[:ip] if options[:ip] + post[:payer][:ip] = options[:ip] if options[:ip] post[:payer][:address] = add_address(post, card, options) end diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb index 6d1a3c686d9..0b7042d0658 100644 --- a/lib/active_merchant/billing/gateways/datatrans.rb +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -1,8 +1,8 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: class DatatransGateway < Gateway - self.test_url = 'https://api.sandbox.datatrans.com/v1/transactions/' - self.live_url = 'https://api.datatrans.com/v1/transactions/' + self.test_url = 'https://api.sandbox.datatrans.com/v1/' + self.live_url = 'https://api.datatrans.com/v1/' self.supported_countries = %w(CH GR US) # to confirm the countries supported. self.default_currency = 'CHF' @@ -35,6 +35,13 @@ def purchase(money, payment, options = {}) authorize(money, payment, options.merge(auto_settle: true)) end + def verify(payment, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(100, payment, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + def authorize(money, payment, options = {}) post = { refno: options.fetch(:order_id, '') } add_payment_method(post, payment) @@ -65,6 +72,28 @@ def void(authorization, options = {}) commit('cancel', post, { transaction_id: transaction_id }) end + def store(payment_method, options = {}) + exp_year = format(payment_method.year, :two_digits) + exp_month = format(payment_method.month, :two_digits) + + post = { + requests: [ + { + type: 'CARD', + pan: payment_method.number, + expiryMonth: exp_month, + expiryYear: exp_year + } + ] + } + commit('tokenize', post, { expiry_month: exp_month, expiry_year: exp_year }) + end + + def unstore(authorization, options = {}) + data_alias = authorization.split('|')[2] + commit('delete_alias', {}, { alias_id: data_alias }, :delete) + end + def supports_scrubbing? true end @@ -79,27 +108,33 @@ def scrub(transcript) private def add_payment_method(post, payment_method) - card = build_card(payment_method) - post[:card] = { - expiryMonth: format(payment_method.month, :two_digits), - expiryYear: format(payment_method.year, :two_digits) - }.merge(card) - end - - def build_card(payment_method) - if payment_method.is_a?(NetworkTokenizationCreditCard) - { + case payment_method + when String + token, exp_month, exp_year = payment_method.split('|')[2..4] + card = { + type: 'ALIAS', + alias: token, + expiryMonth: exp_month, + expiryYear: exp_year + } + when NetworkTokenizationCreditCard + card = { type: DEVICE_SOURCE[payment_method.source] ? 'DEVICE_TOKEN' : 'NETWORK_TOKEN', tokenType: DEVICE_SOURCE[payment_method.source] || CREDIT_CARD_SOURCE[card_brand(payment_method)], token: payment_method.number, - cryptogram: payment_method.payment_cryptogram + cryptogram: payment_method.payment_cryptogram, + expiryMonth: format(payment_method.month, :two_digits), + expiryYear: format(payment_method.year, :two_digits) } - else - { + when CreditCard + card = { number: payment_method.number, - cvv: payment_method.verification_value.to_s + cvv: payment_method.verification_value.to_s, + expiryMonth: format(payment_method.month, :two_digits), + expiryYear: format(payment_method.year, :two_digits) } end + post[:card] = card end def add_3ds_data(post, payment_method, options) @@ -124,6 +159,12 @@ def add_3ds_data(post, payment_method, options) post[:card].merge!(three_ds) end + def country_code(country) + Country.find(country).code(:alpha3).value if country + rescue InvalidCountryCodeError + nil + end + def add_billing_address(post, options) return unless billing_address = options[:billing_address] @@ -132,7 +173,7 @@ def add_billing_address(post, options) street: billing_address[:address1], street2: billing_address[:address2], city: billing_address[:city], - country: Country.find(billing_address[:country]).code(:alpha3).value, # pass country alpha 2 to country alpha 3, + country: country_code(billing_address[:country]), phoneNumber: billing_address[:phone], zipCode: billing_address[:zip], email: options[:email] @@ -144,15 +185,15 @@ def add_currency_amount(post, money, options) post[:amount] = amount(money) end - def commit(action, post, options = {}) - response = parse(ssl_post(url(action, options), post.to_json, headers)) + def commit(action, post, options = {}, method = :post) + response = parse(ssl_request(method, url(action, options), post.to_json, headers)) succeeded = success_from(action, response) Response.new( succeeded, message_from(succeeded, response), response, - authorization: authorization_from(response), + authorization: authorization_from(response, action, options), test: test?, error_code: error_code_from(response) ) @@ -183,26 +224,36 @@ def headers def url(endpoint, options = {}) case endpoint when 'settle', 'credit', 'cancel' - "#{test? ? test_url : live_url}#{options[:transaction_id]}/#{endpoint}" + "#{test? ? test_url : live_url}transactions/#{options[:transaction_id]}/#{endpoint}" + when 'tokenize' + "#{test? ? test_url : live_url}aliases/#{endpoint}" + when 'delete_alias' + "#{test? ? test_url : live_url}aliases/#{options[:alias_id]}" else - "#{test? ? test_url : live_url}#{endpoint}" + "#{test? ? test_url : live_url}transactions/#{endpoint}" end end def success_from(action, response) case action when 'authorize', 'credit' - true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode') + response.include?('transactionId') && response.include?('acquirerAuthorizationCode') when 'settle', 'cancel' - true if response.dig('response_code') == 204 + response.dig('response_code') == 204 + when 'tokenize' + response.dig('responses', 0, 'alias') && response.dig('overview', 'failed') == 0 + when 'delete_alias' + response.dig('response_code') == 204 else false end end - def authorization_from(response) - auth = [response['transactionId'], response['acquirerAuthorizationCode']].join('|') - return auth unless auth == '|' + def authorization_from(response, action, options) + token_array = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('|') if action == 'tokenize' + + auth = [response['transactionId'], response['acquirerAuthorizationCode'], token_array].join('|') + return auth unless auth == '||' end def message_from(succeeded, response) diff --git a/lib/active_merchant/billing/gateways/decidir.rb b/lib/active_merchant/billing/gateways/decidir.rb index 58167d5ede8..b41d84c8372 100644 --- a/lib/active_merchant/billing/gateways/decidir.rb +++ b/lib/active_merchant/billing/gateways/decidir.rb @@ -106,7 +106,9 @@ def scrub(transcript) gsub(%r((apikey: )\w+)i, '\1[FILTERED]'). gsub(%r((\"card_number\\\":\\\")\d+), '\1[FILTERED]'). gsub(%r((\"security_code\\\":\\\")\d+), '\1[FILTERED]'). - gsub(%r((\"emv_issuer_data\\\":\\\")\d+), '\1[FILTERED]') + gsub(%r((\"emv_issuer_data\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"cryptogram\\\":\\\"/)\w+), '\1[FILTERED]'). + gsub(%r((\"token_card_data\\\":{.*\\\"token\\\":\\\")\d+), '\1[FILTERED]') end private @@ -198,8 +200,11 @@ def add_network_token(post, payment_method, options) post[:fraud_detection] ||= {} post[:fraud_detection][:sent_to_cs] = false post[:card_data][:last_four_digits] = options[:last_4] + post[:card_data][:security_code] = payment_method.verification_value if payment_method.verification_value? post[:token_card_data] = { + expiration_month: format(payment_method.month, :two_digits), + expiration_year: format(payment_method.year, :two_digits), token: payment_method.number, eci: payment_method.eci, cryptogram: payment_method.payment_cryptogram diff --git a/lib/active_merchant/billing/gateways/decidir_plus.rb b/lib/active_merchant/billing/gateways/decidir_plus.rb index 5cefebf92e5..9a7477bd6cb 100644 --- a/lib/active_merchant/billing/gateways/decidir_plus.rb +++ b/lib/active_merchant/billing/gateways/decidir_plus.rb @@ -321,8 +321,11 @@ def error_message(response) return error_code_from(response) unless validation_errors = response.dig('validation_errors') validation_errors = validation_errors[0] + message = "#{validation_errors&.dig('code')}: #{validation_errors&.dig('param')}" + return message unless message == ': ' - "#{validation_errors.dig('code')}: #{validation_errors.dig('param')}" + errors = response['validation_errors'].map { |k, v| "#{k}: #{v}" }.join(', ') + "#{response['error_type']} - #{errors}" end def rejected?(response) diff --git a/lib/active_merchant/billing/gateways/ebanx.rb b/lib/active_merchant/billing/gateways/ebanx.rb index 4588eddb7f7..b0bbb84e071 100644 --- a/lib/active_merchant/billing/gateways/ebanx.rb +++ b/lib/active_merchant/billing/gateways/ebanx.rb @@ -50,6 +50,7 @@ def purchase(money, payment, options = {}) add_address(post, options) add_customer_responsible_person(post, payment, options) add_additional_data(post, options) + add_stored_credentials(post, options) commit(:purchase, post) end @@ -64,6 +65,7 @@ def authorize(money, payment, options = {}) add_address(post, options) add_customer_responsible_person(post, payment, options) add_additional_data(post, options) + add_stored_credentials(post, options) post[:payment][:creditcard][:auto_capture] = false commit(:authorize, post) @@ -168,6 +170,28 @@ def add_customer_responsible_person(post, payment, options) end end + def add_stored_credentials(post, options) + return unless (stored_creds = options[:stored_credential]) + + post[:cof_info] = { + cof_type: stored_creds[:initial_transaction] ? 'initial' : 'stored', + initiator: stored_creds[:initiator] == 'cardholder' ? 'CIT' : 'MIT', + trans_type: add_trans_type(stored_creds), + mandate_id: stored_creds[:network_transaction_id] + }.compact + end + + def add_trans_type(options) + case options[:reason_type] + when 'recurring' + 'SCHEDULED_RECURRING' + when 'installment' + 'INSTALLMENT' + else + options[:initiator] == 'cardholder' ? 'CUSTOMER_COF' : 'MERCHANT_COF' + end + end + def add_address(post, options) if address = options[:billing_address] || options[:address] post[:payment][:address] = address[:address1].split[1..-1].join(' ') if address[:address1] diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index 5e11f579f32..04583338366 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -43,12 +43,7 @@ def purchase(money, payment_method, options = {}) xml.ssl_transaction_type self.actions[:purchase] xml.ssl_amount amount(money) - if payment_method.is_a?(String) - add_token(xml, payment_method) - else - add_creditcard(xml, payment_method) - end - + add_payment(xml, payment_method, options) add_invoice(xml, options) add_salestax(xml, options) add_currency(xml, money, options) @@ -56,27 +51,26 @@ def purchase(money, payment_method, options = {}) add_customer_email(xml, options) add_test_mode(xml, options) add_ip(xml, options) - add_auth_purchase_params(xml, options) + add_auth_purchase_params(xml, payment_method, options) add_level_3_fields(xml, options) if options[:level_3_data] end commit(request) end - def authorize(money, creditcard, options = {}) + def authorize(money, payment_method, options = {}) request = build_xml_request do |xml| xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] xml.ssl_transaction_type self.actions[:authorize] xml.ssl_amount amount(money) - add_salestax(xml, options) add_invoice(xml, options) - add_creditcard(xml, creditcard) + add_payment(xml, payment_method, options) add_currency(xml, money, options) add_address(xml, options) add_customer_email(xml, options) add_test_mode(xml, options) add_ip(xml, options) - add_auth_purchase_params(xml, options) + add_auth_purchase_params(xml, payment_method, options) add_level_3_fields(xml, options) if options[:level_3_data] end commit(request) @@ -92,7 +86,7 @@ def capture(money, authorization, options = {}) add_salestax(xml, options) add_approval_code(xml, authorization) add_invoice(xml, options) - add_creditcard(xml, options[:credit_card]) + add_creditcard(xml, options[:credit_card], options) add_currency(xml, money, options) add_address(xml, options) add_customer_email(xml, options) @@ -139,7 +133,7 @@ def credit(money, creditcard, options = {}) xml.ssl_transaction_type self.actions[:credit] xml.ssl_amount amount(money) add_invoice(xml, options) - add_creditcard(xml, creditcard) + add_creditcard(xml, creditcard, options) add_currency(xml, money, options) add_address(xml, options) add_customer_email(xml, options) @@ -152,7 +146,7 @@ def verify(credit_card, options = {}) request = build_xml_request do |xml| xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] xml.ssl_transaction_type self.actions[:verify] - add_creditcard(xml, credit_card) + add_creditcard(xml, credit_card, options) add_address(xml, options) add_test_mode(xml, options) add_ip(xml, options) @@ -165,7 +159,7 @@ def store(creditcard, options = {}) xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] xml.ssl_transaction_type self.actions[:store] xml.ssl_add_token 'Y' - add_creditcard(xml, creditcard) + add_creditcard(xml, creditcard, options) add_address(xml, options) add_customer_email(xml, options) add_test_mode(xml, options) @@ -178,8 +172,8 @@ def update(token, creditcard, options = {}) request = build_xml_request do |xml| xml.ssl_vendor_id @options[:ssl_vendor_id] || options[:ssl_vendor_id] xml.ssl_transaction_type self.actions[:update] - add_token(xml, token) - add_creditcard(xml, creditcard) + xml.ssl_token token + add_creditcard(xml, creditcard, options) add_address(xml, options) add_customer_email(xml, options) add_test_mode(xml, options) @@ -200,6 +194,16 @@ def scrub(transcript) private + def add_payment(xml, payment, options) + if payment.is_a?(String) || options[:ssl_token] + xml.ssl_token options[:ssl_token] || payment + elsif payment.is_a?(NetworkTokenizationCreditCard) + add_network_token(xml, payment) + else + add_creditcard(xml, payment, options) + end + end + def add_invoice(xml, options) xml.ssl_invoice_number url_encode_truncate((options[:order_id] || options[:invoice]), 25) xml.ssl_description url_encode_truncate(options[:description], 255) @@ -213,11 +217,21 @@ def add_txn_id(xml, authorization) xml.ssl_txn_id authorization.split(';').last end - def add_creditcard(xml, creditcard) + def add_network_token(xml, payment_method) + payment = payment_method.payment_data&.gsub('=>', ':') + case payment_method.source + when :apple_pay + xml.ssl_applepay_web url_encode(payment) + when :google_pay + xml.ssl_google_pay url_encode(payment) + end + end + + def add_creditcard(xml, creditcard, options) xml.ssl_card_number creditcard.number xml.ssl_exp_date expdate(creditcard) - add_verification_value(xml, creditcard) if creditcard.verification_value? + add_verification_value(xml, creditcard, options) xml.ssl_first_name url_encode_truncate(creditcard.first_name, 20) xml.ssl_last_name url_encode_truncate(creditcard.last_name, 30) @@ -230,12 +244,10 @@ def add_currency(xml, money, options) xml.ssl_transaction_currency currency end - def add_token(xml, token) - xml.ssl_token token - end + def add_verification_value(xml, credit_card, options) + return unless credit_card.verification_value? - def add_verification_value(xml, creditcard) - xml.ssl_cvv2cvc2 creditcard.verification_value + xml.ssl_cvv2cvc2 credit_card.verification_value xml.ssl_cvv2cvc2_indicator 1 end @@ -294,16 +306,16 @@ def add_ip(xml, options) end # add_recurring_token is a field that can be sent in to obtain a token from Elavon for use with their tokenization program - def add_auth_purchase_params(xml, options) + def add_auth_purchase_params(xml, payment_method, options) xml.ssl_dynamic_dba options[:dba] if options.has_key?(:dba) xml.ssl_merchant_initiated_unscheduled merchant_initiated_unscheduled(options) if merchant_initiated_unscheduled(options) xml.ssl_add_token options[:add_recurring_token] if options.has_key?(:add_recurring_token) - xml.ssl_token options[:ssl_token] if options[:ssl_token] xml.ssl_customer_code options[:customer] if options.has_key?(:customer) xml.ssl_customer_number options[:customer_number] if options.has_key?(:customer_number) - xml.ssl_entry_mode entry_mode(options) if entry_mode(options) + xml.ssl_entry_mode entry_mode(payment_method, options) if entry_mode(payment_method, options) add_custom_fields(xml, options) if options[:custom_fields] - add_stored_credential(xml, options) if options[:stored_credential] + add_stored_credential(xml, payment_method, options) + add_installment_fields(xml, options) end def add_custom_fields(xml, options) @@ -352,30 +364,50 @@ def add_line_items(xml, level_3_data) } end - def add_stored_credential(xml, options) + def add_stored_credential(xml, payment_method, options) + return unless options[:stored_credential] + network_transaction_id = options.dig(:stored_credential, :network_transaction_id) - case - when network_transaction_id.nil? - return - when network_transaction_id.to_s.include?('|') - oar_data, ps2000_data = options[:stored_credential][:network_transaction_id].split('|') - xml.ssl_oar_data oar_data unless oar_data.nil? || oar_data.empty? - xml.ssl_ps2000_data ps2000_data unless ps2000_data.nil? || ps2000_data.empty? - when network_transaction_id.to_s.length > 22 - xml.ssl_oar_data options.dig(:stored_credential, :network_transaction_id) - else - xml.ssl_ps2000_data options.dig(:stored_credential, :network_transaction_id) + xml.ssl_recurring_flag recurring_flag(options) if recurring_flag(options) + xml.ssl_par_value options[:par_value] if options[:par_value] + xml.ssl_association_token_data options[:association_token_data] if options[:association_token_data] + + unless payment_method.is_a?(String) || options[:ssl_token].present? + xml.ssl_approval_code options[:approval_code] if options[:approval_code] + if network_transaction_id.to_s.include?('|') + oar_data, ps2000_data = network_transaction_id.split('|') + xml.ssl_oar_data oar_data unless oar_data.blank? + xml.ssl_ps2000_data ps2000_data unless ps2000_data.blank? + elsif network_transaction_id.to_s.length > 22 + xml.ssl_oar_data network_transaction_id + elsif network_transaction_id.present? + xml.ssl_ps2000_data network_transaction_id + end end end + def recurring_flag(options) + return unless reason = options.dig(:stored_credential, :reason_type) + return 1 if reason == 'recurring' + return 2 if reason == 'installment' + end + def merchant_initiated_unscheduled(options) return options[:merchant_initiated_unscheduled] if options[:merchant_initiated_unscheduled] - return 'Y' if options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' || options.dig(:stored_credential, :reason_type) == 'recurring' + return 'Y' if options.dig(:stored_credential, :initiator) == 'merchant' && %w(unscheduled recurring).include?(options.dig(:stored_credential, :reason_type)) + end + + def add_installment_fields(xml, options) + return unless options.dig(:stored_credential, :reason_type) == 'installment' + + xml.ssl_payment_number options[:payment_number] + xml.ssl_payment_count options[:installments] end - def entry_mode(options) + def entry_mode(payment_method, options) return options[:entry_mode] if options[:entry_mode] - return 12 if options[:stored_credential] + return if payment_method.is_a?(String) || options[:ssl_token] + return 12 if options.dig(:stored_credential, :reason_type) == 'unscheduled' end def build_xml_request diff --git a/lib/active_merchant/billing/gateways/fat_zebra.rb b/lib/active_merchant/billing/gateways/fat_zebra.rb index 0d534db434c..3a9f11b656f 100644 --- a/lib/active_merchant/billing/gateways/fat_zebra.rb +++ b/lib/active_merchant/billing/gateways/fat_zebra.rb @@ -151,7 +151,7 @@ def add_three_ds(post, options) par: three_d_secure[:authentication_response_status], ver: formatted_enrollment(three_d_secure[:enrolled]), threeds_version: three_d_secure[:version], - ds_transaction_id: three_d_secure[:ds_transaction_id] + directory_server_txn_id: three_d_secure[:ds_transaction_id] }.compact end diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index 4925abe3196..a7a73db4aed 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -17,10 +17,14 @@ class FlexChargeGateway < Gateway sync: 'outcome', refund: 'orders/%s/refund', store: 'tokenize', - inquire: 'outcome' + inquire: 'orders/%s', + capture: 'capture', + void: 'orders/%s/cancel' } - SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING).freeze + SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING CAPTUREREQUIRED).freeze + + NO_ERROR_KEYS = %w(TraceId access_token token_expires).freeze def initialize(options = {}) requires!(options, :app_key, :app_secret, :site_id, :mid) @@ -28,27 +32,52 @@ def initialize(options = {}) end def purchase(money, credit_card, options = {}) - post = {} - address = options[:billing_address] || options[:address] + post = { transactionType: options.fetch(:transactionType, 'Purchase') } + add_merchant_data(post, options) add_base_data(post, options) add_invoice(post, money, credit_card, options) add_mit_data(post, options) - add_payment_method(post, credit_card, address, options) - add_address(post, credit_card, address) + add_payment_method(post, credit_card, address(options), options) + add_address(post, credit_card, address(options), :billingInformation) + add_address(post, credit_card, options[:shipping_address], :shippingInformation) add_customer_data(post, options) add_three_ds(post, options) + add_metadata(post, options) commit(:purchase, post) end + def authorize(money, credit_card, options = {}) + options[:transactionType] = 'Authorization' + purchase(money, credit_card, options) + end + + def capture(money, authorization, options = {}) + order_id, currency = authorization.split('#') + post = { + idempotencyKey: options[:idempotency_key] || SecureRandom.uuid, + orderId: order_id, + amount: money, + currency: currency + } + + commit(:capture, post, authorization) + end + def refund(money, authorization, options = {}) - commit(:refund, { amountToRefund: (money.to_f / 100).round(2) }, authorization) + order_id, _currency = authorization.split('#') + self.money_format = :dollars + commit(:refund, { amountToRefund: localized_amount(money, 2).to_f }, order_id) + end + + def void(authorization, options = {}) + order_id, _currency = authorization.split('#') + commit(:void, {}, order_id) end def store(credit_card, options = {}) - address = options[:billing_address] || options[:address] || {} - first_name, last_name = address_names(address[:name], credit_card) + first_name, last_name = names_from_address(address(options), credit_card) post = { payment_method: { @@ -84,11 +113,21 @@ def scrub(transcript) end def inquire(authorization, options = {}) - commit(:inquire, { orderSessionKey: authorization }, authorization) + order_id, _currency = authorization.split('#') + commit(:inquire, {}, order_id, :get) + end + + def add_metadata(post, options) + post[:Source] = 'Spreedly' + post[:ExtraData] = options[:extra_data] if options[:extra_data].present? end private + def address(options) + options[:billing_address] || options[:address] || {} + end + def add_three_ds(post, options) return unless three_d_secure = options[:three_d_secure] @@ -113,7 +152,8 @@ def add_merchant_data(post, options) def add_base_data(post, options) post[:isDeclined] = cast_bool(options[:is_declined]) post[:orderId] = options[:order_id] - post[:idempotencyKey] = options[:idempotency_key] || options[:order_id] + post[:idempotencyKey] = options[:idempotency_key] || SecureRandom.uuid + post[:senseKey] = options[:sense_key] end def add_mit_data(post, options) @@ -128,10 +168,12 @@ def add_customer_data(post, options) post[:payer] = { email: options[:email] || 'NA', phone: phone_from(options) }.compact end - def add_address(post, payment, address) - first_name, last_name = address_names(address[:name], payment) + def add_address(post, payment, address, address_type) + return unless address.present? - post[:billingInformation] = { + first_name, last_name = names_from_address(address, payment) + + post[address_type] = { firstName: first_name, lastName: last_name, country: address[:country], @@ -165,27 +207,31 @@ def add_payment_method(post, credit_card, address, options) payment_method = case credit_card when String { Token: true, cardNumber: credit_card } - else - { - holderName: credit_card.name, - cardType: 'CREDIT', - cardBrand: credit_card.brand&.upcase, - cardCountry: address[:country], - expirationMonth: credit_card.month, - expirationYear: credit_card.year, - cardBinNumber: credit_card.number[0..5], - cardLast4Digits: credit_card.number[-4..-1], - cardNumber: credit_card.number, - Token: false - } + when CreditCard + if credit_card.number + { + holderName: credit_card.name, + cardType: 'CREDIT', + cardBrand: credit_card.brand&.upcase, + cardCountry: address[:country], + expirationMonth: credit_card.month, + expirationYear: credit_card.year, + cardBinNumber: credit_card.number[0..5], + cardLast4Digits: credit_card.number[-4..-1], + cardNumber: credit_card.number, + Token: false + } + else + {} + end end post[:paymentMethod] = payment_method.compact end - def address_names(address_name, payment_method) - split_names(address_name).tap do |names| - names[0] = payment_method&.first_name unless names[0].present? - names[1] = payment_method&.last_name unless names[1].present? + def names_from_address(address, payment_method) + split_names(address[:name]).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) + names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) end end @@ -204,10 +250,10 @@ def fetch_access_token @options[:access_token] = response[:accessToken] @options[:token_expires] = response[:expires] @options[:new_credentials] = true - + success = response[:accessToken].present? Response.new( - response[:accessToken].present?, - message_from(response), + success, + message_from(response, success), response, test: test?, error_code: response[:statusCode] @@ -227,6 +273,8 @@ def headers end def parse(body) + body = '{}' if body.blank? + JSON.parse(body).with_indifferent_access rescue JSON::ParserError { @@ -235,27 +283,27 @@ def parse(body) }.with_indifferent_access end - def commit(action, post, authorization = nil) + def commit(action, post, authorization = nil, method = :post) MultiResponse.run do |r| r.process { fetch_access_token } unless access_token_valid? r.process do - api_request(action, post, authorization).tap do |response| + api_request(action, post, authorization, method).tap do |response| response.params.merge!(@options.slice(:access_token, :token_expires)) if @options[:new_credentials] end end end end - def api_request(action, post, authorization = nil) - response = parse ssl_post(url(action, authorization), post.to_json, headers) - + def api_request(action, post, authorization = nil, method = :post) + response = parse ssl_request(method, url(action, authorization), post.to_json, headers) + success = success_from(action, response) Response.new( - success_from(response), - message_from(response), + success, + message_from(response, success), response, - authorization: authorization_from(action, response), + authorization: authorization_from(action, response, post), test: test?, - error_code: error_code_from(response) + error_code: error_code_from(success, response) ) rescue ResponseError => e response = parse(e.response.body) @@ -264,29 +312,44 @@ def api_request(action, post, authorization = nil) @options[:access_token] = '' @options[:new_credentials] = true end - Response.new(false, message_from(response), response, test: test?) + Response.new(false, message_from(response, false), response, test: test?) end - def success_from(response) - response[:success] && SUCCESS_MESSAGES.include?(response[:status]) || - response.dig(:transaction, :payment_method, :token).present? + def success_from(action, response) + case action + when :store then response.dig(:transaction, :payment_method, :token).present? + when :inquire then response[:id].present? && SUCCESS_MESSAGES.include?(response[:statusName]) + when :void then response.empty? + else + response[:success] && SUCCESS_MESSAGES.include?(response[:status]) + end end - def message_from(response) - response[:title] || response[:responseMessage] || response[:status] + def message_from(response, success_status) + return extract_error(response) unless success_status || response['TraceId'].nil? + + response[:title] || response[:responseMessage] || response[:statusName] || response[:status] end - def authorization_from(action, response) - action == :store ? response.dig(:transaction, :payment_method, :token) : response[:orderSessionKey] + def authorization_from(action, response, options) + if action == :store + response.dig(:transaction, :payment_method, :token) + elsif success_from(action, response) + [response[:orderId], options[:currency] || default_currency].compact.join('#') + end end - def error_code_from(response) - response[:status] unless success_from(response) + def error_code_from(success, response) + (response[:statusName] || response[:status]) unless success end def cast_bool(value) ![false, 0, '', '0', 'f', 'F', 'false', 'FALSE'].include?(value) end + + def extract_error(response) + response.except(*NO_ERROR_KEYS).to_json + end end end end diff --git a/lib/active_merchant/billing/gateways/hps.rb b/lib/active_merchant/billing/gateways/hps.rb index a4a6370b992..94b28aef9e0 100644 --- a/lib/active_merchant/billing/gateways/hps.rb +++ b/lib/active_merchant/billing/gateways/hps.rb @@ -17,11 +17,6 @@ class HpsGateway < Gateway PAYMENT_DATA_SOURCE_MAPPING = { apple_pay: 'ApplePay', - master: 'MasterCard 3DSecure', - visa: 'Visa 3DSecure', - american_express: 'AMEX 3DSecure', - discover: 'Discover 3DSecure', - android_pay: 'GooglePayApp', google_pay: 'GooglePayApp' } @@ -30,15 +25,16 @@ def initialize(options = {}) super end - def authorize(money, card_or_token, options = {}) + def authorize(money, payment_method, options = {}) commit('CreditAuth') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_card_or_token_customer_data(xml, card_or_token, options) + add_card_or_token_customer_data(xml, payment_method, options) add_details(xml, options) add_descriptor_name(xml, options) - add_card_or_token_payment(xml, card_or_token, options) - add_three_d_secure(xml, card_or_token, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) add_stored_credentials(xml, options) end end @@ -110,7 +106,8 @@ def scrub(transcript) gsub(%r(()[^<]*(<\/hps:SecretAPIKey>))i, '\1[FILTERED]\2'). gsub(%r(()[^<]*(<\/hps:PaymentData>))i, '\1[FILTERED]\2'). gsub(%r(()[^<]*(<\/hps:RoutingNumber>))i, '\1[FILTERED]\2'). - gsub(%r(()[^<]*(<\/hps:AccountNumber>))i, '\1[FILTERED]\2') + gsub(%r(()[^<]*(<\/hps:AccountNumber>))i, '\1[FILTERED]\2'). + gsub(%r(()[^<]*(<\/hps:Cryptogram>))i, '\1[FILTERED]\2') end private @@ -125,28 +122,30 @@ def commit_check_sale(money, check, options) end end - def commit_credit_sale(money, card_or_token, options) + def commit_credit_sale(money, payment_method, options) commit('CreditSale') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_card_or_token_customer_data(xml, card_or_token, options) + add_card_or_token_customer_data(xml, payment_method, options) add_details(xml, options) add_descriptor_name(xml, options) - add_card_or_token_payment(xml, card_or_token, options) - add_three_d_secure(xml, card_or_token, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) add_stored_credentials(xml, options) end end - def commit_recurring_billing_sale(money, card_or_token, options) + def commit_recurring_billing_sale(money, payment_method, options) commit('RecurringBilling') do |xml| add_amount(xml, money) add_allow_dup(xml) - add_card_or_token_customer_data(xml, card_or_token, options) + add_card_or_token_customer_data(xml, payment_method, options) add_details(xml, options) add_descriptor_name(xml, options) - add_card_or_token_payment(xml, card_or_token, options) - add_three_d_secure(xml, card_or_token, options) + add_card_or_token_payment(xml, payment_method, options) + add_wallet_data(xml, payment_method, options) + add_three_d_secure(xml, payment_method, options) add_stored_credentials(xml, options) add_stored_credentials_for_recurring_billing(xml, options) end @@ -254,32 +253,24 @@ def add_descriptor_name(xml, options) xml.hps :TxnDescriptor, options[:descriptor_name] if options[:descriptor_name] end - def add_three_d_secure(xml, card_or_token, options) - if card_or_token.is_a?(NetworkTokenizationCreditCard) - build_three_d_secure(xml, { - source: card_or_token.source, - cavv: card_or_token.payment_cryptogram, - eci: card_or_token.eci, - xid: card_or_token.transaction_id - }) - elsif options[:three_d_secure] - options[:three_d_secure][:source] ||= card_brand(card_or_token) - build_three_d_secure(xml, options[:three_d_secure]) + def add_wallet_data(xml, payment_method, options) + return unless payment_method.is_a?(NetworkTokenizationCreditCard) + + xml.hps :WalletData do + xml.hps :PaymentSource, PAYMENT_DATA_SOURCE_MAPPING[payment_method.source] + xml.hps :Cryptogram, payment_method.payment_cryptogram + xml.hps :ECI, strip_leading_zero(payment_method.eci) if payment_method.eci end end - def build_three_d_secure(xml, three_d_secure) - # PaymentDataSource is required when supplying the SecureECommerce data group, - # and the gateway currently only allows the values within the mapping - return unless PAYMENT_DATA_SOURCE_MAPPING[three_d_secure[:source].to_sym] + def add_three_d_secure(xml, card_or_token, options) + return unless (three_d_secure = options[:three_d_secure]) - xml.hps :SecureECommerce do - xml.hps :PaymentDataSource, PAYMENT_DATA_SOURCE_MAPPING[three_d_secure[:source].to_sym] - xml.hps :TypeOfPaymentData, '3DSecure' # Only type currently supported - xml.hps :PaymentData, three_d_secure[:cavv] if three_d_secure[:cavv] - # the gateway only allows a single character for the ECI - xml.hps :ECommerceIndicator, strip_leading_zero(three_d_secure[:eci]) if three_d_secure[:eci] - xml.hps :XID, three_d_secure[:xid] if three_d_secure[:xid] + xml.hps :Secure3D do + xml.hps :Version, three_d_secure[:version] + xml.hps :AuthenticationValue, three_d_secure[:cavv] if three_d_secure[:cavv] + xml.hps :ECI, strip_leading_zero(three_d_secure[:eci]) if three_d_secure[:eci] + xml.hps :DirectoryServerTxnId, three_d_secure[:ds_transaction_id] if three_d_secure[:ds_transaction_id] end end diff --git a/lib/active_merchant/billing/gateways/iveri.rb b/lib/active_merchant/billing/gateways/iveri.rb index c2cb8aa141a..d3a429c86ff 100644 --- a/lib/active_merchant/billing/gateways/iveri.rb +++ b/lib/active_merchant/billing/gateways/iveri.rb @@ -55,7 +55,8 @@ def refund(money, authorization, options = {}) end def void(authorization, options = {}) - post = build_vxml_request('Void', options) do |xml| + txn_type = options[:reference_type] == :authorize ? 'AuthorisationReversal' : 'Void' + post = build_vxml_request(txn_type, options) do |xml| add_authorization(xml, authorization, options) end @@ -65,7 +66,7 @@ def void(authorization, options = {}) def verify(credit_card, options = {}) MultiResponse.run(:use_first_response) do |r| r.process { authorize(100, credit_card, options) } - r.process(:ignore_result) { void(r.authorization, options) } + r.process(:ignore_result) { void(r.authorization, options.merge(reference_type: :authorize)) } end end diff --git a/lib/active_merchant/billing/gateways/litle.rb b/lib/active_merchant/billing/gateways/litle.rb index 9dfa38740d7..49b4eed8e44 100644 --- a/lib/active_merchant/billing/gateways/litle.rb +++ b/lib/active_merchant/billing/gateways/litle.rb @@ -561,7 +561,7 @@ def commit(kind, request, money = nil) end def success_from(kind, parsed) - return %w(000 001 010).any?(parsed[:response]) unless kind == :registerToken + return %w(000 001 010 141 142).any?(parsed[:response]) unless kind == :registerToken %w(000 801 802).include?(parsed[:response]) end diff --git a/lib/active_merchant/billing/gateways/mercado_pago.rb b/lib/active_merchant/billing/gateways/mercado_pago.rb index 36949c0422e..f98bdf11845 100644 --- a/lib/active_merchant/billing/gateways/mercado_pago.rb +++ b/lib/active_merchant/billing/gateways/mercado_pago.rb @@ -43,6 +43,7 @@ def refund(money, authorization, options = {}) post = {} authorization, original_amount = authorization.split('|') post[:amount] = amount(money).to_f if original_amount && original_amount.to_f > amount(money).to_f + add_idempotency_key(post, options) commit('refund', "payments/#{authorization}/refunds", post) end @@ -105,7 +106,9 @@ def purchase_request(money, payment, options = {}) add_net_amount(post, options) add_taxes(post, options) add_notification_url(post, options) - post[:binary_mode] = (options[:binary_mode].nil? ? true : options[:binary_mode]) + add_idempotency_key(post, options) + add_3ds(post, options) + post[:binary_mode] = options.fetch(:binary_mode, true) unless options[:execute_threed] post end @@ -134,7 +137,7 @@ def add_merchant_services(post, options) end def add_additional_data(post, options) - post[:sponsor_id] = options[:sponsor_id] + post[:sponsor_id] = options[:sponsor_id] unless test? post[:metadata] = options[:metadata] if options[:metadata] post[:device_id] = options[:device_id] if options[:device_id] post[:additional_info] = { @@ -211,6 +214,10 @@ def add_net_amount(post, options) post[:net_amount] = Float(options[:net_amount]) if options[:net_amount] end + def add_idempotency_key(post, options) + post[:idempotency_key] = options[:idempotency_key] if options[:idempotency_key] + end + def add_notification_url(post, options) post[:notification_url] = options[:notification_url] if options[:notification_url] end @@ -287,7 +294,7 @@ def success_from(action, response) if action == 'refund' response['status'] != 404 && response['error'].nil? else - %w[active approved authorized cancelled in_process].include?(response['status']) + %w[active approved authorized cancelled in_process pending].include?(response['status']) end end @@ -300,7 +307,11 @@ def authorization_from(response, params) end def post_data(parameters = {}) - parameters.clone.tap { |p| p.delete(:device_id) }.to_json + params = parameters.clone.tap do |p| + p.delete(:device_id) + p.delete(:idempotency_key) + end + params.to_json end def inquire_path(authorization, options) @@ -322,6 +333,13 @@ def error_code_from(action, response) end end + def add_3ds(post, options) + return unless options[:execute_threed] + + post[:three_d_secure_mode] = options[:three_ds_mode] == 'mandatory' ? 'mandatory' : 'optional' + post[:notification_url] = options[:notification_url] if options[:notification_url] + end + def url(action) full_url = (test? ? test_url : live_url) full_url + "/#{action}?access_token=#{CGI.escape(@options[:access_token])}" @@ -332,6 +350,7 @@ def headers(options = {}) 'Content-Type' => 'application/json' } headers['X-meli-session-id'] = options[:device_id] if options[:device_id] + headers['X-Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] headers end diff --git a/lib/active_merchant/billing/gateways/merchant_warrior.rb b/lib/active_merchant/billing/gateways/merchant_warrior.rb index 18be04361f1..614eae2d6f8 100644 --- a/lib/active_merchant/billing/gateways/merchant_warrior.rb +++ b/lib/active_merchant/billing/gateways/merchant_warrior.rb @@ -33,6 +33,7 @@ def authorize(money, payment_method, options = {}) add_recurring_flag(post, options) add_soft_descriptors(post, options) add_three_ds(post, options) + post['storeID'] = options[:store_id] if options[:store_id] commit('processAuth', post) end @@ -45,6 +46,7 @@ def purchase(money, payment_method, options = {}) add_recurring_flag(post, options) add_soft_descriptors(post, options) add_three_ds(post, options) + post['storeID'] = options[:store_id] if options[:store_id] commit('processCard', post) end @@ -113,9 +115,9 @@ def add_address(post, options) post['customerCity'] = address[:city] post['customerAddress'] = address[:address1] post['customerPostCode'] = address[:zip] - post['customerIP'] = address[:ip] - post['customerPhone'] = address[:phone] - post['customerEmail'] = address[:email] + post['customerIP'] = address[:ip] || options[:ip] + post['customerPhone'] = address[:phone] || address[:phone_number] + post['customerEmail'] = address[:email] || options[:email] end def add_order_id(post, options) diff --git a/lib/active_merchant/billing/gateways/mercury.rb b/lib/active_merchant/billing/gateways/mercury.rb index 5648640d734..49c2b3e08ad 100644 --- a/lib/active_merchant/billing/gateways/mercury.rb +++ b/lib/active_merchant/billing/gateways/mercury.rb @@ -349,7 +349,9 @@ def escape_xml(xml) end def unescape_xml(escaped_xml) - escaped_xml.gsub(/\>/, '>').gsub(/\</, '<') + xml = escaped_xml.gsub(/\>/, '>').gsub(/\r/, '').gsub(/\n/, '').gsub(/\t/, '').gsub('<', '<') + xml.slice! "" # rubocop:disable Style/StringLiterals + xml end end end diff --git a/lib/active_merchant/billing/gateways/moneris.rb b/lib/active_merchant/billing/gateways/moneris.rb index 2df428bb68c..2123115ccf7 100644 --- a/lib/active_merchant/billing/gateways/moneris.rb +++ b/lib/active_merchant/billing/gateways/moneris.rb @@ -218,7 +218,7 @@ def add_external_mpi_fields(post, options) three_d_secure_options = options[:three_d_secure] post[:threeds_version] = three_d_secure_options[:version] - post[:crypt_type] = three_d_secure_options[:eci] + post[:crypt_type] = three_d_secure_options.dig(:eci)&.to_s&.sub!(/^0/, '') post[:cavv] = three_d_secure_options[:cavv] post[:threeds_server_trans_id] = three_d_secure_options[:three_ds_server_trans_id] post[:ds_trans_id] = three_d_secure_options[:ds_transaction_id] diff --git a/lib/active_merchant/billing/gateways/nmi.rb b/lib/active_merchant/billing/gateways/nmi.rb index bb53c39eedf..6819f3af3ac 100644 --- a/lib/active_merchant/billing/gateways/nmi.rb +++ b/lib/active_merchant/billing/gateways/nmi.rb @@ -34,6 +34,7 @@ def initialize(options = {}) def purchase(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) add_stored_credential(post, options) add_customer_data(post, options) @@ -48,6 +49,7 @@ def purchase(amount, payment_method, options = {}) def authorize(amount, payment_method, options = {}) post = {} add_invoice(post, amount, options) + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) add_stored_credential(post, options) add_customer_data(post, options) @@ -97,6 +99,7 @@ def credit(amount, payment_method, options = {}) def verify(payment_method, options = {}) post = {} + add_customer_vault_data(post, options) add_payment_method(post, payment_method, options) add_customer_data(post, options) add_vendor_data(post, options) @@ -134,6 +137,7 @@ def scrub(transcript) gsub(%r((cvv=)\d+), '\1[FILTERED]'). gsub(%r((checkaba=)\d+), '\1[FILTERED]'). gsub(%r((checkaccount=)\d+), '\1[FILTERED]'). + gsub(%r((cavv=)[^&\n]*), '\1[FILTERED]'). gsub(%r((cryptogram=)[^&]+(&?)), '\1[FILTERED]\2') end @@ -166,7 +170,7 @@ def add_payment_method(post, payment_method, options) elsif payment_method.is_a?(NetworkTokenizationCreditCard) post[:ccnumber] = payment_method.number post[:ccexp] = exp_date(payment_method) - post[:token_cryptogram] = payment_method.payment_cryptogram + add_network_token_fields(post, payment_method) elsif card_brand(payment_method) == 'check' post[:payment] = 'check' post[:firstname] = payment_method.first_name @@ -187,6 +191,17 @@ def add_payment_method(post, payment_method, options) end end + def add_network_token_fields(post, payment_method) + if payment_method.source == :apple_pay || payment_method.source == :google_pay + post[:cavv] = payment_method.payment_cryptogram + post[:eci] = payment_method.eci + post[:decrypted_applepay_data] = 1 if payment_method.source == :apple_pay + post[:decrypted_googlepay_data] = 1 if payment_method.source == :google_pay + else + post[:token_cryptogram] = payment_method.payment_cryptogram + end + end + def add_stored_credential(post, options) return unless (stored_credential = options[:stored_credential]) @@ -267,6 +282,11 @@ def add_vendor_data(post, options) post[:processor_id] = options[:processor_id] if options[:processor_id] end + def add_customer_vault_data(post, options) + post[:customer_vault] = options[:customer_vault] if options[:customer_vault] + post[:customer_vault_id] = options[:customer_vault_id] if options[:customer_vault_id] + end + def add_merchant_defined_fields(post, options) (1..20).each do |each| key = "merchant_defined_field_#{each}".to_sym diff --git a/lib/active_merchant/billing/gateways/nuvei.rb b/lib/active_merchant/billing/gateways/nuvei.rb new file mode 100644 index 00000000000..f2c6c1df87f --- /dev/null +++ b/lib/active_merchant/billing/gateways/nuvei.rb @@ -0,0 +1,267 @@ +module ActiveMerchant + module Billing + class NuveiGateway < Gateway + self.test_url = 'https://ppp-test.nuvei.com/ppp/api/v1' + self.live_url = 'https://secure.safecharge.com/ppp/api/v1' + + self.supported_countries = %w[US CA IN NZ GB AU US] + self.default_currency = 'USD' + self.money_format = :cents + self.supported_cardtypes = %i[visa master american_express discover union_pay] + self.currencies_without_fractions = %w[CLP KRW JPY ISK MMK PYG UGX VND XAF XOF] + self.homepage_url = 'https://www.nuvei.com/' + self.display_name = 'Nuvei' + + ENDPOINTS_MAPPING = { + authenticate: '/getSessionToken', + purchase: '/payment', # /authorize with transactionType: "Auth" + capture: '/settleTransaction', + refund: '/refundTransaction', + void: '/voidTransaction', + general_credit: '/payout' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :merchant_site_id, :secret_key) + super + fetch_session_token unless session_token_valid? + end + + def authorize(money, payment, options = {}, transaction_type = 'Auth') + post = { transactionType: transaction_type } + + build_post_data(post) + add_amount(post, money, options) + add_payment_method(post, payment) + add_address(post, payment, options) + add_customer_ip(post, options) + + commit(:purchase, post) + end + + def purchase(money, payment, options = {}) + authorize(money, payment, options, 'Sale') + end + + def capture(money, authorization, options = {}) + post = { relatedTransactionId: authorization } + + build_post_data(post) + add_amount(post, money, options) + + commit(:capture, post) + end + + def refund(money, authorization, options = {}) + post = { relatedTransactionId: authorization } + + build_post_data(post) + add_amount(post, money, options) + + commit(:refund, post) + end + + def void(authorization, options = {}) + post = { relatedTransactionId: authorization } + build_post_data(post) + + commit(:void, post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(0, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def credit(money, payment, options = {}) + post = { userTokenId: options[:user_token_id] } + + build_post_data(post) + add_amount(post, money, options) + add_payment_method(post, payment, :cardData) + add_address(post, payment, options) + add_customer_ip(post, options) + + commit(:general_credit, post.compact) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("cardNumber\\?":\\?")[^"\\]*)i, '\1[FILTERED]'). + gsub(%r(("cardCvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantSiteId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantKey\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def add_customer_ip(post, options) + return unless options[:ip] + + post[:deviceDetails] = { ipAddress: options[:ip] } + end + + def add_amount(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + end + + def credit_card_hash(payment) + { + cardNumber: payment.number, + cardHolderName: payment.name, + expirationMonth: format(payment.month, :two_digits), + expirationYear: format(payment.year, :four_digits), + CVV: payment.verification_value + } + end + + def add_payment_method(post, payment, key = :paymentOption) + payment_data = payment.is_a?(CreditCard) ? credit_card_hash(payment) : { cardToken: payment } + post[key] = key == :cardData ? payment_data : { card: payment_data } + end + + def add_customer_names(full_name, payment_method) + split_names(full_name).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) + names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) + end + end + + def add_address(post, payment, options) + return unless address = options[:billing_address] || options[:address] + + first_name, last_name = add_customer_names(address[:name], payment) + + post[:billingAddress] = { + email: options[:email], + country: address[:country], + phone: options[:phone] || address[:phone], + firstName: first_name, + lastName: last_name + }.compact + end + + def current_timestamp + Time.now.utc.strftime('%Y%m%d%H%M%S') + end + + def build_post_data(post) + post[:merchantId] = @options[:merchant_id] + post[:merchantSiteId] = @options[:merchant_site_id] + post[:timeStamp] = current_timestamp.to_i + post[:clientRequestId] = SecureRandom.uuid + post[:clientUniqueId] = SecureRandom.hex(16) + end + + def calculate_checksum(post, action) + common_keys = %i[merchantId merchantSiteId clientRequestId] + keys = case action + when :authenticate + [:timeStamp] + when :capture, :refund, :void + %i[clientUniqueId amount currency relatedTransactionId timeStamp] + else + %i[amount currency timeStamp] + end + + to_sha = post.values_at(*common_keys.concat(keys)).push(@options[:secret_key]).join + Digest::SHA256.hexdigest(to_sha) + end + + def send_session_request(post) + post[:checksum] = calculate_checksum(post, 'authenticate') + response = parse(ssl_post(url(:authenticate), post.to_json, headers)).with_indifferent_access + expiration_time = post[:timeStamp] + @options[:session_token] = response.dig('sessionToken') + @options[:token_expires] = expiration_time + + Response.new( + response[:sessionToken].present?, + message_from(response), + response, + test: test?, + error_code: error_code_from(response) + ) + end + + def fetch_session_token(post = {}) + build_post_data(post) + send_session_request(post) + end + + def session_token_valid? + return false unless @options[:session_token] && @options[:token_expires] + + (Time.now.utc.to_i - @options[:token_expires].to_i) < 900 # 15 minutes + end + + def commit(action, post, authorization = nil, method = :post) + post[:sessionToken] = @options[:session_token] unless action == :capture + post[:checksum] = calculate_checksum(post, action) + + response = parse(ssl_request(method, url(action, authorization), post.to_json, headers)) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(action, response, post), + test: test?, + error_code: error_code_from(response) + ) + rescue ResponseError => e + response = parse(e.response.body) + @options[:session_token] = '' if e.response.code == '401' + + Response.new(false, message_from(response), response, test: test?) + end + + def url(action, id = nil) + "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" + end + + def error_code_from(response) + response[:errCode] == 0 ? response[:gwErrorCode] : response[:errCode] + end + + def headers + { 'Content-Type' => 'application/json' }.tap do |headers| + headers['Authorization'] = "Bearer #{@options[:session_token]}" if @options[:session_token] + end + end + + def parse(body) + body = '{}' if body.blank? + + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError + { + errors: body, + status: 'Unable to parse JSON response' + }.with_indifferent_access + end + + def success_from(response) + response[:status] == 'SUCCESS' && response[:transactionStatus] == 'APPROVED' + end + + def authorization_from(action, response, post) + response.dig(:transactionId) + end + + def message_from(response) + reason = response[:reason]&.present? ? response[:reason] : nil + response[:gwErrorReason] || reason || response[:transactionStatus] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/orbital.rb b/lib/active_merchant/billing/gateways/orbital.rb index 872fda576c7..eea1c40f16e 100644 --- a/lib/active_merchant/billing/gateways/orbital.rb +++ b/lib/active_merchant/billing/gateways/orbital.rb @@ -709,10 +709,14 @@ def add_xid(xml, credit_card, three_d_secure) xml.tag!(:XID, three_d_secure[:xid]) if three_d_secure[:xid] end + PYMT_PROGRAM_CODE_BY_BRAND = { + 'american_express' => 'ASK', + 'discover' => 'DPB' + }.freeze def add_pymt_brand_program_code(xml, credit_card, three_d_secure) - return unless three_d_secure && credit_card.brand == 'american_express' + return unless three_d_secure && (code = PYMT_PROGRAM_CODE_BY_BRAND[credit_card.brand]) - xml.tag!(:PymtBrandProgramCode, 'ASK') + xml.tag!(:PymtBrandProgramCode, code) end def mastercard?(payment_source) @@ -724,7 +728,7 @@ def add_mastercard_fields(xml, credit_card, parameters, three_d_secure) add_mc_sca_recurring(xml, credit_card, parameters, three_d_secure) add_mc_program_protocol(xml, credit_card, three_d_secure) add_mc_directory_trans_id(xml, credit_card, three_d_secure) - add_mc_ucafind(xml, credit_card, three_d_secure) + add_mc_ucafind(xml, credit_card, three_d_secure, parameters) end def add_mc_sca_merchant_initiated(xml, credit_card, parameters, three_d_secure) @@ -753,10 +757,16 @@ def add_mc_directory_trans_id(xml, credit_card, three_d_secure) xml.tag!(:MCDirectoryTransID, three_d_secure[:ds_transaction_id]) if three_d_secure[:ds_transaction_id] end - def add_mc_ucafind(xml, credit_card, three_d_secure) + def add_mc_ucafind(xml, credit_card, three_d_secure, options) return unless three_d_secure - xml.tag! :UCAFInd, '4' + if options[:alternate_ucaf_flow] + return unless %w(4 6 7).include?(three_d_secure[:eci]) + + xml.tag! :UCAFInd, options[:ucaf_collection_indicator] if options[:ucaf_collection_indicator] + else + xml.tag! :UCAFInd, options[:ucaf_collection_indicator] || '4' + end end #=====SCA (STORED CREDENTIAL) FIELDS===== diff --git a/lib/active_merchant/billing/gateways/pay_trace.rb b/lib/active_merchant/billing/gateways/pay_trace.rb index d4a159d1d87..0ce064a009b 100644 --- a/lib/active_merchant/billing/gateways/pay_trace.rb +++ b/lib/active_merchant/billing/gateways/pay_trace.rb @@ -95,7 +95,7 @@ def authorize(money, payment_or_customer_id, options = {}) def capture(money, authorization, options = {}) if visa_or_mastercard?(options) - MultiResponse.run do |r| + MultiResponse.run(:use_first_response) do |r| r.process { commit(ENDPOINTS[:capture], build_capture_request(money, authorization, options)) } r.process { commit(ENDPOINTS[:"level_3_#{options[:visa_or_mastercard]}"], send_level_3_data(r, options)) } end diff --git a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb b/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb index a02d4bff1b6..c70d94b58df 100644 --- a/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +++ b/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb @@ -179,6 +179,11 @@ def credit(money, identification, options = {}) # . (period) # {space} # + + def inquire(authorization, options = {}) + transaction_details(authorization) + end + def reference_transaction(money, options = {}) requires!(options, :reference_id) commit 'DoReferenceTransaction', build_reference_transaction_request(money, options) diff --git a/lib/active_merchant/billing/gateways/pin.rb b/lib/active_merchant/billing/gateways/pin.rb index 0562ff14134..b90d12fdb7d 100644 --- a/lib/active_merchant/billing/gateways/pin.rb +++ b/lib/active_merchant/billing/gateways/pin.rb @@ -82,6 +82,11 @@ def void(token, options = {}) commit(:put, "charges/#{CGI.escape(token)}/void", {}, options) end + # Verify a previously authorized charge. + def verify_3ds(session_token, options = {}) + commit(:get, "/charges/verify?session_token=#{session_token}", nil, options) + end + # Updates the credit card for the customer. def update(token, creditcard, options = {}) post = {} @@ -183,10 +188,16 @@ def add_platform_adjustment(post, options) def add_3ds(post, options) if options[:three_d_secure] post[:three_d_secure] = {} - post[:three_d_secure][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version] - post[:three_d_secure][:eci] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] - post[:three_d_secure][:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] - post[:three_d_secure][:transaction_id] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid] + if options[:three_d_secure][:enabled] + post[:three_d_secure][:enabled] = true + post[:three_d_secure][:fallback_ok] = options[:three_d_secure][:fallback_ok] unless options[:three_d_secure][:fallback_ok].nil? + post[:three_d_secure][:callback_url] = options[:three_d_secure][:callback_url] if options[:three_d_secure][:callback_url] + else + post[:three_d_secure][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version] + post[:three_d_secure][:eci] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci] + post[:three_d_secure][:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv] + post[:three_d_secure][:transaction_id] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid] + end end end @@ -271,6 +282,8 @@ def parse(body) end def post_data(parameters = {}) + return nil unless parameters + parameters.to_json end end diff --git a/lib/active_merchant/billing/gateways/plexo.rb b/lib/active_merchant/billing/gateways/plexo.rb index 57a386e38ce..a19bd3b9104 100644 --- a/lib/active_merchant/billing/gateways/plexo.rb +++ b/lib/active_merchant/billing/gateways/plexo.rb @@ -92,7 +92,8 @@ def scrub(transcript) gsub(%r(("Number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("Cvc\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). gsub(%r(("InvoiceNumber\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). - gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') + gsub(%r(("MerchantId\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]'). + gsub(%r(("Cryptogram\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]') end private @@ -195,17 +196,37 @@ def add_invoice_number(post, options) end def add_payment_method(post, payment, options) - post[:paymentMethod] = {} + payment_method = build_payment_method(payment) - if payment&.is_a?(CreditCard) - post[:paymentMethod][:type] = 'card' - post[:paymentMethod][:Card] = {} - post[:paymentMethod][:Card][:Number] = payment.number - post[:paymentMethod][:Card][:ExpMonth] = format(payment.month, :two_digits) if payment.month - post[:paymentMethod][:Card][:ExpYear] = format(payment.year, :two_digits) if payment.year - post[:paymentMethod][:Card][:Cvc] = payment.verification_value if payment.verification_value + if payment_method.present? + add_card_holder(payment_method[:NetworkToken] || payment_method[:Card], payment, options) + post[:paymentMethod] = payment_method + end + end - add_card_holder(post[:paymentMethod][:Card], payment, options) + def build_payment_method(payment) + case payment + when NetworkTokenizationCreditCard + { + source: 'network-token', + id: payment.brand, + NetworkToken: { + Number: payment.number, + ExpMonth: (format(payment.month, :two_digits) if payment.month), + ExpYear: (format(payment.year, :two_digits) if payment.year), + Cryptogram: payment.payment_cryptogram + } + } + when CreditCard + { + type: 'card', + Card: { + Number: payment.number, + ExpMonth: (format(payment.month, :two_digits) if payment.month), + ExpYear: (format(payment.year, :two_digits) if payment.year), + Cvc: payment.verification_value + } + } end end diff --git a/lib/active_merchant/billing/gateways/rapyd.rb b/lib/active_merchant/billing/gateways/rapyd.rb index c2d21c3cec2..8ab61b0d5c8 100644 --- a/lib/active_merchant/billing/gateways/rapyd.rb +++ b/lib/active_merchant/billing/gateways/rapyd.rb @@ -243,6 +243,7 @@ def add_ewallet(post, options) def add_payment_fields(post, options) post[:description] = options[:description] if options[:description] post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor] + post[:save_payment_method] = options[:save_payment_method] if options[:save_payment_method] end def add_payment_urls(post, options, action = '') @@ -334,7 +335,7 @@ def commit(method, action, parameters) ) rescue ActiveMerchant::ResponseError => e response = e.response.body.present? ? parse(e.response.body) : { 'status' => { 'response_code' => e.response.msg } } - message = response['status'].slice('message', 'response_code').values.compact_blank.first || '' + message = response['status'].slice('message', 'response_code').values.select(&:present?).first || '' Response.new(false, message, response, test: test?, error_code: error_code_from(response)) end diff --git a/lib/active_merchant/billing/gateways/redsys_rest.rb b/lib/active_merchant/billing/gateways/redsys_rest.rb index 3e8de87ed68..60f268a6ea0 100644 --- a/lib/active_merchant/billing/gateways/redsys_rest.rb +++ b/lib/active_merchant/billing/gateways/redsys_rest.rb @@ -74,6 +74,15 @@ class RedsysRestGateway < Gateway 'UYU' => '858' } + THREEDS_EXEMPTIONS = { + corporate_card: 'COR', + delegated_authentication: 'ATD', + low_risk: 'TRA', + low_value: 'LWV', + stored_credential: 'MIT', + trusted_merchant: 'NDF' + } + # The set of supported transactions for this gateway. # More operations are supported by the gateway itself, but # are not supported in this library. @@ -186,6 +195,8 @@ def purchase(money, payment, options = {}) post = {} add_action(post, :purchase, options) add_amount(post, money, options) + add_stored_credentials(post, options) + add_threeds_exemption_data(post, options) add_order(post, options[:order_id]) add_payment(post, payment) add_description(post, options) @@ -201,6 +212,8 @@ def authorize(money, payment, options = {}) post = {} add_action(post, :authorize, options) add_amount(post, money, options) + add_stored_credentials(post, options) + add_threeds_exemption_data(post, options) add_order(post, options[:order_id]) add_payment(post, payment) add_description(post, options) @@ -277,7 +290,7 @@ def scrub(transcript) def add_direct_payment(post, options) # Direct payment skips 3DS authentication. We should only apply this if execute_threed is false # or authentication data is not present. Authentication data support to be added in the future. - return if options[:execute_threed] || options[:authentication_data] + return if options[:execute_threed] || options[:authentication_data] || options[:three_ds_exemption_type] == 'moto' post[:DS_MERCHANT_DIRECTPAYMENT] = true end @@ -378,6 +391,35 @@ def add_authentication(post, options) post[:DS_MERCHANT_MERCHANTCODE] = @options[:login] end + def add_stored_credentials(post, options) + return unless stored_credential = options[:stored_credential] + + post[:DS_MERCHANT_COF_INI] = stored_credential[:initial_transaction] ? 'S' : 'N' + + post[:DS_MERCHANT_COF_TYPE] = case stored_credential[:reason_type] + when 'recurring' + 'R' + when 'installment' + 'I' + else + 'C' + end + + post[:DS_MERCHANT_IDENTIFIER] = 'REQUIRED' if stored_credential[:initiator] == 'cardholder' + post[:DS_MERCHANT_COF_TXNID] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id] + end + + def add_threeds_exemption_data(post, options) + return unless options[:three_ds_exemption_type] + + if options[:three_ds_exemption_type] == 'moto' + post[:DS_MERCHANT_DIRECTPAYMENT] = 'MOTO' + else + exemption = options[:three_ds_exemption_type].to_sym + post[:DS_MERCHANT_EXCEP_SCA] = THREEDS_EXEMPTIONS[exemption] + end + end + def parse(body) JSON.parse(body) end diff --git a/lib/active_merchant/billing/gateways/safe_charge.rb b/lib/active_merchant/billing/gateways/safe_charge.rb index 3f522c0a1c0..f031c9b9159 100644 --- a/lib/active_merchant/billing/gateways/safe_charge.rb +++ b/lib/active_merchant/billing/gateways/safe_charge.rb @@ -195,6 +195,8 @@ def add_customer_details(post, payment, options) post[:sg_Zip] = address[:zip] if address[:zip] post[:sg_Country] = address[:country] if address[:country] post[:sg_Phone] = address[:phone] if address[:phone] + post[:sg_middleName] = options[:middle_name] if options[:middle_name] + post[:sg_doCardHolderNameVerification] = options[:card_holder_verification] if options[:card_holder_verification] end post[:sg_Email] = options[:email] diff --git a/lib/active_merchant/billing/gateways/stripe.rb b/lib/active_merchant/billing/gateways/stripe.rb index 17bc8c5035c..e5b51562856 100644 --- a/lib/active_merchant/billing/gateways/stripe.rb +++ b/lib/active_merchant/billing/gateways/stripe.rb @@ -148,7 +148,7 @@ def capture(money, authorization, options = {}) def void(identification, options = {}) post = {} post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer] - post[:metadata] = options[:metadata] if options[:metadata] + add_metadata(post, options) post[:reason] = options[:reason] if options[:reason] post[:expand] = [:charge] commit(:post, "charges/#{CGI.escape(identification)}/refunds", post, options) @@ -159,7 +159,7 @@ def refund(money, identification, options = {}) add_amount(post, money, options) post[:refund_application_fee] = true if options[:refund_application_fee] post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer] - post[:metadata] = options[:metadata] if options[:metadata] + add_metadata(post, options) post[:reason] = options[:reason] if options[:reason] post[:expand] = [:charge] @@ -617,8 +617,21 @@ def add_radar_data(post, options = {}) post[:radar_options] = radar_options unless radar_options.empty? end + def add_header_fields(response) + return unless @response_headers.present? + + headers = {} + headers['response_headers'] = {} + headers['response_headers']['idempotent_replayed'] = @response_headers['idempotent-replayed'] if @response_headers['idempotent-replayed'] + headers['response_headers']['stripe_should_retry'] = @response_headers['stripe-should-retry'] if @response_headers['stripe-should-retry'] + + response.merge!(headers) + end + def parse(body) - JSON.parse(body) + response = JSON.parse(body) + add_header_fields(response) + response end def post_data(params) @@ -752,6 +765,18 @@ def success_from(response, options) !response.key?('error') && response['status'] != 'failed' end + # Override the regular handle response so we can access the headers + # set header fields and values so we can add them to the response body + def handle_response(response) + @response_headers = response.each_header.to_h if response.respond_to?(:header) + case response.code.to_i + when 200...300 + response.body + else + raise ResponseError.new(response) + end + end + def response_error(raw_response) parse(raw_response) rescue JSON::ParserError diff --git a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb index 4a9582aced1..2685b2bb881 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -13,8 +13,7 @@ class StripePaymentIntentsGateway < StripeGateway DEFAULT_API_VERSION = '2020-08-27' DIGITAL_WALLETS = { apple_pay: 'apple_pay', - google_pay: 'google_pay_dpan', - untokenized_google_pay: 'google_pay_ecommerce_token' + google_pay: 'google_pay_dpan' } def create_intent(money, payment_method, options = {}) @@ -38,7 +37,7 @@ def create_intent(money, payment_method, options = {}) return result if result.is_a?(ActiveMerchant::Billing::Response) end - add_network_token_cryptogram_and_eci(post, payment_method) + add_network_token_info(post, payment_method, options) add_external_three_d_secure_auth_data(post, options) add_metadata(post, options) add_return_url(post, options) @@ -83,6 +82,7 @@ def confirm_intent(intent_id, payment_method, options = {}) return result if result.is_a?(ActiveMerchant::Billing::Response) end + add_network_token_info(post, payment_method, options) add_payment_method_types(post, options) CONFIRM_INTENT_ATTRIBUTES.each do |attribute| add_whitelisted_attribute(post, options, attribute) @@ -117,6 +117,11 @@ def add_payment_method_data(payment_method, options = {}) post[:billing_details] = add_address(billing, options) end + # wallet_type is only passed for non-tokenized GooglePay which acts as a CreditCard + if options[:wallet_type] + post[:metadata] ||= {} + post[:metadata][:input_method] = 'GooglePay' + end add_name_only(post, payment_method) if post[:billing_details].nil? add_network_token_data(post, payment_method, options) post @@ -140,6 +145,7 @@ def update_intent(money, intent_id, payment_method, options = {}) return result if result.is_a?(ActiveMerchant::Billing::Response) end + add_network_token_info(post, payment_method, options) add_payment_method_types(post, options) add_customer(post, options) add_metadata(post, options) @@ -167,11 +173,13 @@ def create_setup_intent(payment_method, options = {}) return result if result.is_a?(ActiveMerchant::Billing::Response) end + add_network_token_info(post, payment_method, options) add_metadata(post, options) add_return_url(post, options) add_fulfillment_date(post, options) request_three_d_secure(post, options) add_card_brand(post, options) + add_exemption(post, options) post[:on_behalf_of] = options[:on_behalf_of] if options[:on_behalf_of] post[:usage] = options[:usage] if %w(on_session off_session).include?(options[:usage]) post[:description] = options[:description] if options[:description] @@ -422,14 +430,23 @@ def add_network_token_data(post_data, payment_method, options) post_data end - def add_network_token_cryptogram_and_eci(post, payment_method) - return unless adding_network_token_card_data?(payment_method) + def add_network_token_info(post, payment_method, options) + # wallet_type is only passed for non-tokenized GooglePay which acts as a CreditCard + if options[:wallet_type] + post[:metadata] ||= {} + post[:metadata][:input_method] = 'GooglePay' + end + + return unless payment_method.is_a?(NetworkTokenizationCreditCard) && options.dig(:stored_credential, :initiator) != 'merchant' + return if digital_wallet_payment_method?(payment_method) && options[:new_ap_gp_route] != true post[:payment_method_options] ||= {} post[:payment_method_options][:card] ||= {} post[:payment_method_options][:card][:network_token] ||= {} - post[:payment_method_options][:card][:network_token][:cryptogram] = payment_method.payment_cryptogram if payment_method.payment_cryptogram - post[:payment_method_options][:card][:network_token][:electronic_commerce_indicator] = payment_method.eci if payment_method.eci + post[:payment_method_options][:card][:network_token].merge!({ + cryptogram: payment_method.respond_to?(:payment_cryptogram) ? payment_method.payment_cryptogram : options[:cryptogram], + electronic_commerce_indicator: format_eci(payment_method, options) + }.compact) end def add_digital_wallet(post, payment_method, options) @@ -442,24 +459,11 @@ def add_digital_wallet(post, payment_method, options) network_token: { number: payment_method.number, exp_month: payment_method.month, - exp_year: payment_method.year + exp_year: payment_method.year, + tokenization_method: DIGITAL_WALLETS[payment_method.source] } } } - - add_cryptogram_and_eci(post, payment_method, options) unless options[:wallet_type] - source = payment_method.respond_to?(:source) ? payment_method.source : options[:wallet_type] - post[:payment_method_data][:card][:network_token][:tokenization_method] = DIGITAL_WALLETS[source] - end - - def add_cryptogram_and_eci(post, payment_method, options) - post[:payment_method_options] ||= {} - post[:payment_method_options][:card] ||= {} - post[:payment_method_options][:card][:network_token] ||= {} - post[:payment_method_options][:card][:network_token] = { - cryptogram: payment_method.respond_to?(:payment_cryptogram) ? payment_method.payment_cryptogram : options[:cryptogram], - electronic_commerce_indicator: format_eci(payment_method, options) - }.compact end def format_eci(payment_method, options) @@ -531,7 +535,7 @@ def add_payment_method_types(post, options) end def add_exemption(post, options = {}) - return unless options[:confirm] + return unless options[:confirm] && options[:moto] post[:payment_method_options] ||= {} post[:payment_method_options][:card] ||= {} @@ -582,7 +586,7 @@ def add_stored_credential_transaction_type(post, options = {}) card_options = post[:payment_method_options][:card] card_options[:stored_credential_transaction_type] = stored_credential_type - card_options[:mit_exemption].delete(:network_transaction_id) if stored_credential_type == 'setup_on_session' + card_options[:mit_exemption].delete(:network_transaction_id) if %w(setup_on_session stored_on_session).include?(stored_credential_type) end def initial_transaction_stored_credential(post, stored_credential) @@ -652,7 +656,7 @@ def add_error_on_requires_action(post, options = {}) end def request_three_d_secure(post, options = {}) - return unless options[:request_three_d_secure] && %w(any automatic).include?(options[:request_three_d_secure]) + return unless options[:request_three_d_secure] && %w(any automatic challenge).include?(options[:request_three_d_secure]) post[:payment_method_options] ||= {} post[:payment_method_options][:card] ||= {} diff --git a/lib/active_merchant/billing/gateways/trans_first.rb b/lib/active_merchant/billing/gateways/trans_first.rb index 6cf3756843e..87a7098b9eb 100644 --- a/lib/active_merchant/billing/gateways/trans_first.rb +++ b/lib/active_merchant/billing/gateways/trans_first.rb @@ -166,6 +166,9 @@ def parse(data) end end + response + rescue StandardError + response[:message] = data&.to_s&.strip response end diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb index 8405d805250..48d938acd6b 100644 --- a/lib/active_merchant/billing/gateways/worldpay.rb +++ b/lib/active_merchant/billing/gateways/worldpay.rb @@ -111,6 +111,8 @@ def credit(money, payment_method, options = {}) payment_details = payment_details(payment_method, options) if options[:fast_fund_credit] fast_fund_credit_request(money, payment_method, payment_details.merge(credit: true, **options)) + elsif options[:account_funding_transaction] + aft_request(money, payment_method, payment_details.merge(**options)) else credit_request(money, payment_method, payment_details.merge(credit: true, **options)) end @@ -148,7 +150,8 @@ def scrub(transcript) gsub(%r(()\d+()), '\1[FILTERED]\2'). gsub(%r(()[^<]+()), '\1[FILTERED]\2'). gsub(%r(()\d+()), '\1[FILTERED]\2'). - gsub(%r(()[^<]+()), '\1[FILTERED]\2') + gsub(%r(()[^<]+()), '\1[FILTERED]\2'). + gsub(%r(()\d+(<\/accountReference>)), '\1[FILTERED]\2') end private @@ -189,6 +192,10 @@ def fast_fund_credit_request(money, payment_method, options) commit('fast_credit', build_fast_fund_credit_request(money, payment_method, options), :ok, 'PUSH_APPROVED', options) end + def aft_request(money, payment_method, options) + commit('funding_transfer_transaction', build_aft_request(money, payment_method, options), :ok, 'AUTHORISED', options) + end + def store_request(credit_card, options) commit('store', build_store_request(credit_card, options), options) end @@ -260,9 +267,8 @@ def add_level_two_and_three_data(xml, amount, data) xml.invoiceReferenceNumber data[:invoice_reference_number] if data.include?(:invoice_reference_number) xml.customerReference data[:customer_reference] if data.include?(:customer_reference) xml.cardAcceptorTaxId data[:card_acceptor_tax_id] if data.include?(:card_acceptor_tax_id) - { - sales_tax: 'salesTax', + tax_amount: 'salesTax', discount_amount: 'discountAmount', shipping_amount: 'shippingAmount', duty_amount: 'dutyAmount' @@ -270,53 +276,37 @@ def add_level_two_and_three_data(xml, amount, data) next unless data.include?(key) xml.tag! tag do - data_amount = data[key].symbolize_keys - add_amount(xml, data_amount[:amount].to_i, data_amount) + add_amount(xml, data[key].to_i, data) end end - xml.discountName data[:discount_name] if data.include?(:discount_name) - xml.discountCode data[:discount_code] if data.include?(:discount_code) - - add_date_element(xml, 'shippingDate', data[:shipping_date]) if data.include?(:shipping_date) - - if data.include?(:shipping_courier) - xml.shippingCourier( - data[:shipping_courier][:priority], - data[:shipping_courier][:tracking_number], - data[:shipping_courier][:name] - ) - end - add_optional_data_level_two_and_three(xml, data) - if data.include?(:item) && data[:item].kind_of?(Array) - data[:item].each { |item| add_items_into_level_three_data(xml, item.symbolize_keys) } - elsif data.include?(:item) - add_items_into_level_three_data(xml, data[:item].symbolize_keys) - end + data[:line_items].each { |item| add_line_items_into_level_three_data(xml, item.symbolize_keys, data) } if data.include?(:line_items) end - def add_items_into_level_three_data(xml, item) + def add_line_items_into_level_three_data(xml, item, data) xml.item do xml.description item[:description] if item[:description] xml.productCode item[:product_code] if item[:product_code] xml.commodityCode item[:commodity_code] if item[:commodity_code] xml.quantity item[:quantity] if item[:quantity] - - { - unit_cost: 'unitCost', - item_total: 'itemTotal', - item_total_with_tax: 'itemTotalWithTax', - item_discount_amount: 'itemDiscountAmount', - tax_amount: 'taxAmount' - }.each do |key, tag| - next unless item.include?(key) - - xml.tag! tag do - data_amount = item[key].symbolize_keys - add_amount(xml, data_amount[:amount].to_i, data_amount) - end + xml.unitCost do + add_amount(xml, item[:unit_cost], data) + end + xml.unitOfMeasure item[:unit_of_measure] || 'each' + xml.itemTotal do + sub_total_amount = item[:quantity].to_i * (item[:unit_cost].to_i - item[:discount_amount].to_i) + add_amount(xml, sub_total_amount, data) + end + xml.itemTotalWithTax do + add_amount(xml, item[:total_amount], data) + end + xml.itemDiscountAmount do + add_amount(xml, item[:discount_amount], data) + end + xml.taxAmount do + add_amount(xml, item[:tax_amount], data) end end end @@ -326,7 +316,7 @@ def add_optional_data_level_two_and_three(xml, data) xml.destinationPostalCode data[:destination_postal_code] if data.include?(:destination_postal_code) xml.destinationCountryCode data[:destination_country_code] if data.include?(:destination_country_code) add_date_element(xml, 'orderDate', data[:order_date].symbolize_keys) if data.include?(:order_date) - xml.taxExempt data[:tax_exempt] if data.include?(:tax_exempt) + xml.taxExempt data[:tax_amount].to_i > 0 ? 'false' : 'true' end def order_tag_attributes(options) @@ -417,6 +407,66 @@ def build_fast_fund_credit_request(money, payment_method, options) end end + def build_aft_request(money, payment_method, options) + build_request do |xml| + xml.submit do + xml.order order_tag_attributes(options) do + xml.description(options[:description].blank? ? 'Account Funding Transaction' : options[:description]) + add_amount(xml, money, options) + add_order_content(xml, options) + add_payment_method(xml, money, payment_method, options) + add_shopper(xml, options) + add_sub_merchant_data(xml, options[:sub_merchant_data]) if options[:sub_merchant_data] + add_aft_data(xml, payment_method, options) + end + end + end + end + + def add_aft_data(xml, payment_method, options) + xml.fundingTransfer 'type' => options[:aft_type], 'category' => 'PULL_FROM_CARD' do + xml.paymentPurpose options[:aft_payment_purpose] # Must be included for the recipient for following countries, otherwise optional: Argentina, Bangladesh, Chile, Columbia, Jordan, Mexico, Thailand, UAE, India cross-border + xml.fundingParty 'type' => 'sender' do + xml.accountReference options[:aft_sender_account_reference], 'accountType' => options[:aft_sender_account_type] + xml.fullName do + xml.first options.dig(:aft_sender_full_name, :first) + xml.middle options.dig(:aft_sender_full_name, :middle) + xml.last options.dig(:aft_sender_full_name, :last) + end + xml.fundingAddress do + xml.address1 options.dig(:aft_sender_funding_address, :address1) + xml.address2 options.dig(:aft_sender_funding_address, :address2) + xml.postalCode options.dig(:aft_sender_funding_address, :postal_code) + xml.city options.dig(:aft_sender_funding_address, :city) + xml.state options.dig(:aft_sender_funding_address, :state) + xml.countryCode options.dig(:aft_sender_funding_address, :country_code) + end + end + xml.fundingParty 'type' => 'recipient' do + xml.accountReference options[:aft_recipient_account_reference], 'accountType' => options[:aft_recipient_account_type] + xml.fullName do + xml.first options.dig(:aft_recipient_full_name, :first) + xml.middle options.dig(:aft_recipient_full_name, :middle) + xml.last options.dig(:aft_recipient_full_name, :last) + end + xml.fundingAddress do + xml.address1 options.dig(:aft_recipient_funding_address, :address1) + xml.address2 options.dig(:aft_recipient_funding_address, :address2) + xml.postalCode options.dig(:aft_recipient_funding_address, :postal_code) + xml.city options.dig(:aft_recipient_funding_address, :city) + xml.state options.dig(:aft_recipient_funding_address, :state) + xml.countryCode options.dig(:aft_recipient_funding_address, :country_code) + end + if options[:aft_recipient_funding_data] + xml.fundingData do + add_date_element(xml, 'birthDate', options[:aft_recipient_funding_data][:birth_date]) + xml.telephoneNumber options.dig(:aft_recipient_funding_data, :telephone_number) + end + end + end + end + end + def add_payment_details_for_ff_credit(xml, payment_method, options) xml.paymentDetails do xml.tag! 'FF_DISBURSE-SSL' do @@ -562,10 +612,10 @@ def add_date_element(xml, name, date) end def add_amount(xml, money, options) - currency = options[:currency] || currency(money) + currency = options[:currency] || currency(money.to_i) amount_hash = { - :value => localized_amount(money, currency), + :value => localized_amount(money.to_i, currency), 'currencyCode' => currency, 'exponent' => currency_exponent(currency) } @@ -703,7 +753,7 @@ def add_stored_credential_using_normalized_fields(xml, options) stored_credential_params = generate_stored_credential_params(is_initial_transaction, reason) xml.storedCredentials stored_credential_params do - xml.schemeTransactionIdentifier options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id] && !is_initial_transaction + xml.schemeTransactionIdentifier network_transaction_id(options) if network_transaction_id(options) && !is_initial_transaction end end diff --git a/lib/active_merchant/billing/network_tokenization_credit_card.rb b/lib/active_merchant/billing/network_tokenization_credit_card.rb index 51798547d1e..55a2d97cde6 100644 --- a/lib/active_merchant/billing/network_tokenization_credit_card.rb +++ b/lib/active_merchant/billing/network_tokenization_credit_card.rb @@ -31,6 +31,14 @@ def credit_card? true end + def network_token? + source == :network_token + end + + def mobile_wallet? + %i[apple_pay android_pay google_pay].include?(source) + end + def type 'network_tokenization' end diff --git a/lib/active_merchant/connection.rb b/lib/active_merchant/connection.rb index c2669cd4a2e..4e7d357fb77 100644 --- a/lib/active_merchant/connection.rb +++ b/lib/active_merchant/connection.rb @@ -18,7 +18,7 @@ class Connection RETRY_SAFE = false RUBY_184_POST_HEADERS = { 'Content-Type' => 'application/x-www-form-urlencoded' } - attr_accessor :endpoint, :open_timeout, :read_timeout, :verify_peer, :ssl_version, :ca_file, :ca_path, :pem, :pem_password, :logger, :tag, :ignore_http_status, :max_retries, :proxy_address, :proxy_port + attr_accessor :endpoint, :open_timeout, :read_timeout, :verify_peer, :ssl_version, :ca_file, :ca_path, :pem, :pem_password, :logger, :tag, :ignore_http_status, :max_retries, :proxy_address, :proxy_port, :proxy_user, :proxy_password if Net::HTTP.instance_methods.include?(:min_version=) attr_accessor :min_version @@ -44,6 +44,8 @@ def initialize(endpoint) @ssl_connection = {} @proxy_address = :ENV @proxy_port = nil + @proxy_user = nil + @proxy_password = nil end def wiredump_device=(device) @@ -111,7 +113,7 @@ def request(method, body, headers = {}) def http @http ||= begin - http = Net::HTTP.new(endpoint.host, endpoint.port, proxy_address, proxy_port) + http = Net::HTTP.new(endpoint.host, endpoint.port, proxy_address, proxy_port, proxy_user, proxy_password) configure_debugging(http) configure_timeouts(http) configure_ssl(http) diff --git a/lib/active_merchant/posts_data.rb b/lib/active_merchant/posts_data.rb index ded8a8f3a70..61007399aa0 100644 --- a/lib/active_merchant/posts_data.rb +++ b/lib/active_merchant/posts_data.rb @@ -30,6 +30,8 @@ def self.included(base) base.class_attribute :proxy_address base.class_attribute :proxy_port + base.class_attribute :proxy_user + base.class_attribute :proxy_password end def ssl_get(endpoint, headers = {}) @@ -68,8 +70,10 @@ def raw_ssl_request(method, endpoint, data, headers = {}) connection.ignore_http_status = @options[:ignore_http_status] if @options - connection.proxy_address = proxy_address - connection.proxy_port = proxy_port + connection.proxy_address = proxy_address + connection.proxy_port = proxy_port + connection.proxy_user = proxy_user + connection.proxy_password = proxy_password connection.request(method, data, headers) end diff --git a/lib/active_merchant/version.rb b/lib/active_merchant/version.rb index cebd9356dce..0f14bccd30b 100644 --- a/lib/active_merchant/version.rb +++ b/lib/active_merchant/version.rb @@ -1,3 +1,3 @@ module ActiveMerchant - VERSION = '1.135.2' + VERSION = '1.137.0' end diff --git a/test/fixtures.yml b/test/fixtures.yml index d647c5c0bcb..ffedf50d6f4 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -609,7 +609,7 @@ merchant_warrior: # Working credentials, no need to replace mercury: - login: '089716741701445' + login: '62589006=TEST' password: 'xyz' mercury_no_tokenization: @@ -716,6 +716,11 @@ nmi: nmi_secure: security_key: '6457Thfj624V5r7WUwc5v6a68Zsd6YEm' +nuvei: + merchant_id: 'merchantId' + merchant_site_id: 'siteId' + secret_key: 'secretKey' + ogone: login: LOGIN user: USER diff --git a/test/remote/gateways/remote_adyen_test.rb b/test/remote/gateways/remote_adyen_test.rb index 70fea7c9c1a..4e8d1c44bfc 100644 --- a/test/remote/gateways/remote_adyen_test.rb +++ b/test/remote/gateways/remote_adyen_test.rb @@ -287,7 +287,7 @@ def test_successful_authorize_with_3ds2_browser_client_data end def test_successful_authorize_with_network_token - response = @gateway.authorize(@amount, @nt_credit_card, @options) + response = @gateway.authorize(@amount, @nt_credit_card, @options.merge(switch_cryptogram_mapping_nt: true)) assert_success response assert_equal 'Authorised', response.message end @@ -562,7 +562,13 @@ def test_successful_purchase_with_shipping_default_country_code end def test_successful_purchase_with_apple_pay - response = @gateway.purchase(@amount, @apple_pay_card, @options) + response = @gateway.purchase(@amount, @apple_pay_card, @options.merge(switch_cryptogram_mapping_nt: true)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_apple_pay_with_ld_flag_false + response = @gateway.purchase(@amount, @apple_pay_card, @options.merge(switch_cryptogram_mapping_nt: false)) assert_success response assert_equal '[capture-received]', response.message end @@ -580,13 +586,43 @@ def test_succesful_purchase_with_brand_override_with_execute_threed_false end def test_successful_purchase_with_google_pay - response = @gateway.purchase(@amount, @google_pay_card, @options) + response = @gateway.purchase(@amount, @google_pay_card, @options.merge(switch_cryptogram_mapping_nt: true)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay_pan_only + response = @gateway.purchase(@amount, @credit_card, @options.merge(wallet_type: :google_pay)) + assert_success response + assert_equal '[capture-received]', response.message + end + + def test_successful_purchase_with_google_pay_without_billing_address_and_address_override + options = { + reference: '345123', + email: 'john.smith@test.com', + ip: '77.110.174.153', + shopper_reference: 'John Smith', + billing_address: { + address1: '', + address2: '', + country: 'US', + city: 'Beverly Hills', + state: 'CA', + zip: '90210' + }, + order_id: '123', + stored_credential: { reason_type: 'unscheduled' }, + address_override: true + } + + response = @gateway.purchase(@amount, @google_pay_card, options) assert_success response assert_equal '[capture-received]', response.message end def test_successful_purchase_with_google_pay_and_truncate_order_id - response = @gateway.purchase(@amount, @google_pay_card, @options.merge(order_id: @long_order_id)) + response = @gateway.purchase(@amount, @google_pay_card, @options.merge(order_id: @long_order_id, switch_cryptogram_mapping_nt: true)) assert_success response assert_equal '[capture-received]', response.message end @@ -610,7 +646,7 @@ def test_successful_purchase_with_unionpay_card end def test_successful_purchase_with_network_token - response = @gateway.purchase(@amount, @nt_credit_card, @options) + response = @gateway.purchase(@amount, @nt_credit_card, @options.merge(switch_cryptogram_mapping_nt: true)) assert_success response assert_equal '[capture-received]', response.message end @@ -1426,7 +1462,8 @@ def test_purchase_with_skip_mpi_data first_options = options.merge( order_id: generate_unique_id, shopper_interaction: 'Ecommerce', - recurring_processing_model: 'Subscription' + recurring_processing_model: 'Subscription', + switch_cryptogram_mapping_nt: true ) assert auth = @gateway.authorize(@amount, @apple_pay_card, first_options) assert_success auth @@ -1441,7 +1478,8 @@ def test_purchase_with_skip_mpi_data skip_mpi_data: 'Y', shopper_interaction: 'ContAuth', recurring_processing_model: 'Subscription', - network_transaction_id: auth.network_transaction_id + network_transaction_id: auth.network_transaction_id, + switch_cryptogram_mapping_nt: true ) assert purchase = @gateway.purchase(@amount, @apple_pay_card, used_options) @@ -1646,6 +1684,38 @@ def test_succesful_purchase_with_airline_data assert_equal '[capture-received]', response.message end + def test_succesful_purchase_with_airline_data_with_legs + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + flight_date: '2023-09-08', + ticket_issue_address: 'abcqwer', + ticket_number: 'ABCASDF', + travel_agency_code: 'ASDF', + travel_agency_name: 'hopper', + passenger_name: 'Joe Doe', + legs: [{ + carrier_code: 'KL', + class_of_travel: 'F' + }], + passenger: { + first_name: 'Joe', + last_name: 'Doe', + telephone_number: '432211111' + } + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + assert_success response + assert_equal '[capture-received]', response.message + end + def test_succesful_purchase_with_lodging_data lodging_data = { check_in_date: '20230822', diff --git a/test/remote/gateways/remote_blue_snap_test.rb b/test/remote/gateways/remote_blue_snap_test.rb index a984beeeb1c..c5099c4aa04 100644 --- a/test/remote/gateways/remote_blue_snap_test.rb +++ b/test/remote/gateways/remote_blue_snap_test.rb @@ -6,13 +6,13 @@ def setup @amount = 100 @credit_card = credit_card('4263982640269299') - @cabal_card = credit_card('6271701225979642', month: 3, year: 2024) - @naranja_card = credit_card('5895626746595650', month: 11, year: 2024) - @declined_card = credit_card('4917484589897107', month: 1, year: 2023) - @invalid_card = credit_card('4917484589897106', month: 1, year: 2023) - @three_ds_visa_card = credit_card('4000000000001091', month: 1) - @three_ds_master_card = credit_card('5200000000001096', month: 1) - @invalid_cabal_card = credit_card('5896 5700 0000 0000', month: 1, year: 2023) + @cabal_card = credit_card('6271701225979642') + @naranja_card = credit_card('5895626746595650') + @declined_card = credit_card('4917484589897107') + @invalid_card = credit_card('4917484589897106') + @three_ds_visa_card = credit_card('4000000000001091') + @three_ds_master_card = credit_card('5200000000001096') + @invalid_cabal_card = credit_card('5896 5700 0000 0000') # BlueSnap may require support contact to activate fraud checking on sandbox accounts. # Specific merchant-configurable thresholds can be set as follows: @@ -292,7 +292,7 @@ def test_successful_purchase_with_currency end def test_successful_purchase_with_level3_data - l_three_visa = credit_card('4111111111111111', month: 2, year: 2023) + l_three_visa = credit_card('4111111111111111') options = @options.merge({ customer_reference_number: '1234A', sales_tax_amount: 0.6, diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 859dacf1288..459a05c0cd3 100644 --- a/test/remote/gateways/remote_braintree_blue_test.rb +++ b/test/remote/gateways/remote_braintree_blue_test.rb @@ -269,6 +269,54 @@ def test_successful_credit_card_verification assert response = @gateway.verify(card, @options.merge({ allow_card_verification: true, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id] })) assert_success response + assert_match 'OK', response.message + assert_equal 'M', response.cvv_result['code'] + assert_equal 'M', response.avs_result['code'] + end + + def test_successful_credit_card_verification_without_billing_address + options = { + order_ID: '1', + description: 'store purchase' + } + card = credit_card('4111111111111111') + assert response = @gateway.verify(card, options.merge({ allow_card_verification: true, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id] })) + assert_success response + + assert_match 'OK', response.message + assert_equal 'M', response.cvv_result['code'] + assert_equal 'I', response.avs_result['code'] + end + + def test_successful_credit_card_verification_with_only_address + options = { + order_ID: '1', + description: 'store purchase', + billing_address: { + address1: '456 My Street' + } + } + card = credit_card('4111111111111111') + assert response = @gateway.verify(card, options.merge({ allow_card_verification: true, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id] })) + assert_success response + + assert_match 'OK', response.message + assert_equal 'M', response.cvv_result['code'] + assert_equal 'B', response.avs_result['code'] + end + + def test_successful_credit_card_verification_with_only_zip + options = { + order_ID: '1', + description: 'store purchase', + billing_address: { + zip: 'K1C2N6' + } + } + card = credit_card('4111111111111111') + assert response = @gateway.verify(card, options.merge({ allow_card_verification: true, merchant_account_id: fixtures(:braintree_blue)[:merchant_account_id] })) + assert_success response + assert_match 'OK', response.message assert_equal 'M', response.cvv_result['code'] assert_equal 'P', response.avs_result['code'] @@ -1025,7 +1073,7 @@ def test_verify_credentials def test_successful_recurring_first_stored_credential_v2 creds_options = stored_credential_options(:cardholder, :recurring, :initial) - response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) assert_success response assert_equal '1000 Approved', response.message assert_not_nil response.params['braintree_transaction']['network_transaction_id'] @@ -1034,7 +1082,7 @@ def test_successful_recurring_first_stored_credential_v2 def test_successful_follow_on_recurring_first_cit_stored_credential_v2 creds_options = stored_credential_options(:cardholder, :recurring, id: '020190722142652') - response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) assert_success response assert_equal '1000 Approved', response.message assert_not_nil response.params['braintree_transaction']['network_transaction_id'] @@ -1043,7 +1091,7 @@ def test_successful_follow_on_recurring_first_cit_stored_credential_v2 def test_successful_follow_on_recurring_first_mit_stored_credential_v2 creds_options = stored_credential_options(:merchant, :recurring, id: '020190722142652') - response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) assert_success response assert_equal '1000 Approved', response.message assert_not_nil response.params['braintree_transaction']['network_transaction_id'] @@ -1052,7 +1100,7 @@ def test_successful_follow_on_recurring_first_mit_stored_credential_v2 def test_successful_one_time_mit_stored_credential_v2 creds_options = stored_credential_options(:merchant, id: '020190722142652') - response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options, stored_credentials_v2: true)) + response = @gateway.purchase(@amount, credit_card('4111111111111111'), @options.merge(stored_credential: creds_options)) assert_success response assert_equal '1000 Approved', response.message @@ -1282,6 +1330,7 @@ def test_successful_purchase_and_return_paypal_details_object assert_equal '1000 Approved', response.message assert_equal 'paypal_payer_id', response.params['braintree_transaction']['paypal_details']['payer_id'] assert_equal 'payer@example.com', response.params['braintree_transaction']['paypal_details']['payer_email'] + assert_equal nil, response.params['braintree_transaction']['paypal_details']['paypal_payment_token'] end def test_successful_credit_card_purchase_with_prepaid_debit_issuing_bank diff --git a/test/remote/gateways/remote_braintree_token_nonce_test.rb b/test/remote/gateways/remote_braintree_token_nonce_test.rb index cbc8dbc3c24..54e958ad709 100644 --- a/test/remote/gateways/remote_braintree_token_nonce_test.rb +++ b/test/remote/gateways/remote_braintree_token_nonce_test.rb @@ -26,8 +26,25 @@ def setup def test_client_token_generation generator = TokenNonce.new(@braintree_backend) - token = generator.client_token - assert_not_nil token + client_token = generator.client_token + assert_not_nil client_token + assert_not_nil client_token['authorizationFingerprint'] + end + + def test_client_token_generation_with_mid + @options[:merchant_account_id] = '1234' + generator = TokenNonce.new(@braintree_backend, @options) + client_token = generator.client_token + assert_not_nil client_token + assert_equal client_token['merchantAccountId'], '1234' + end + + def test_client_token_generation_with_a_new_mid + @options[:merchant_account_id] = '1234' + generator = TokenNonce.new(@braintree_backend, @options) + client_token = generator.client_token({ merchant_account_id: '5678' }) + assert_not_nil client_token + assert_equal client_token['merchantAccountId'], '5678' end def test_successfully_create_token_nonce_for_bank_account diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index 4347b82842c..142a1d898d6 100644 --- a/test/remote/gateways/remote_checkout_v2_test.rb +++ b/test/remote/gateways/remote_checkout_v2_test.rb @@ -14,6 +14,7 @@ def setup @credit_card_dnh = credit_card('4024007181869214', verification_value: '100', month: '6', year: Time.now.year + 1) @expired_card = credit_card('4242424242424242', verification_value: '100', month: '6', year: '2010') @declined_card = credit_card('42424242424242424', verification_value: '234', month: '6', year: Time.now.year + 1) + @amex_card = credit_card('341829238058580', brand: 'american_express', verification_value: '1234', month: '6', year: Time.now.year + 1) @threeds_card = credit_card('4485040371536584', verification_value: '100', month: '12', year: Time.now.year + 1) @mada_card = credit_card('5043000000000000', brand: 'mada') @@ -227,6 +228,22 @@ def test_successful_purchase assert_equal 'Succeeded', response.message end + def test_successful_inquire + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal 'Succeeded', response.message + + response = @gateway.inquire(response.authorization, {}) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_unsuccessful_inquire + response = @gateway.inquire('123EDSE', {}) + assert_failure response + assert_equal '404: Not Found', response.message + end + def test_successful_purchase_via_oauth response = @gateway_oauth.purchase(@amount, @credit_card, @options) assert_success response @@ -650,6 +667,38 @@ def test_successful_purchase_with_sender_data assert_equal 'Succeeded', response.message end + def test_successful_purchase_with_risk_data_true + options = @options.merge( + risk: { + enabled: 'true', + device_session_id: '12345-abcd' + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_risk_data_false + options = @options.merge( + risk: { + enabled: 'false' + } + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + + def test_successful_purchase_with_empty_risk_data + options = @options.merge( + risk: {} + ) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + assert_equal 'Succeeded', response.message + end + def test_successful_purchase_with_metadata_via_oauth options = @options.merge( metadata: { @@ -1112,4 +1161,27 @@ def test_successful_purchase_with_idempotency_key assert_success response assert_equal 'Succeeded', response.message end + + # checkout states they provide valid amex cards however, they will fail + # a transaction with either CVV mismatch or invalid card error. For + # the purpose of this test, it's to simulate the truncation of reference id + def test_truncate_id_for_amex_transactions + @options[:order_id] = '1111111111111111111111111111112' + + response = @gateway.purchase(@amount, @amex_card, @options) + assert_failure response + assert_equal '111111111111111111111111111111', response.params['reference'] + assert_equal 30, response.params['reference'].length + assert_equal 'American Express', response.params['source']['scheme'] + end + + def test_non_truncate_id_for_non_amex_transactions + @options[:order_id] = '1111111111111111111111111111112' + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_equal '1111111111111111111111111111112', response.params['reference'] + assert_equal 31, response.params['reference'].length + assert_equal 'Visa', response.params['source']['scheme'] + end end diff --git a/test/remote/gateways/remote_clearhaus_test.rb b/test/remote/gateways/remote_clearhaus_test.rb index 844b748aee4..dfe1fd1b07d 100644 --- a/test/remote/gateways/remote_clearhaus_test.rb +++ b/test/remote/gateways/remote_clearhaus_test.rb @@ -44,7 +44,7 @@ def test_unsuccessful_signing_request assert gateway.options[:private_key] assert auth = gateway.authorize(@amount, @credit_card, @options) assert_failure auth - assert_equal 'Neither PUB key nor PRIV key: not enough data', auth.message + assert_equal 'Neither PUB key nor PRIV key: unsupported', auth.message credentials = fixtures(:clearhaus_secure) credentials[:signing_key] = 'foo' diff --git a/test/remote/gateways/remote_commerce_hub_test.rb b/test/remote/gateways/remote_commerce_hub_test.rb index d52c1d93a29..069e3ec099d 100644 --- a/test/remote/gateways/remote_commerce_hub_test.rb +++ b/test/remote/gateways/remote_commerce_hub_test.rb @@ -70,6 +70,49 @@ def test_successful_purchase assert_equal 'Approved', response.message end + def test_successful_purchase_with_payment_name_override + billing_address = { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: billing_address)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'John', response.params['billingAddress']['firstName'] + assert_equal 'Doe', response.params['billingAddress']['lastName'] + end + + def test_successful_purchase_with_name_override_on_alternative_payment_methods + billing_address = { + address1: 'Infinite Loop', + address2: 1, + country: 'US', + city: 'Cupertino', + state: 'CA', + zip: '95014' + } + + response = @gateway.purchase(@amount, @google_pay, @options.merge(billing_address: billing_address)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'DecryptedWallet', response.params['source']['sourceType'] + assert_equal 'Longbob', response.params['billingAddress']['firstName'] + assert_equal 'Longsen', response.params['billingAddress']['lastName'] + end + + def test_successful_purchase_with_billing_name_override + response = @gateway.purchase(@amount, @credit_card, @options.merge(billing_address: address)) + assert_success response + assert_equal 'Approved', response.message + assert_equal 'Jim', response.params['billingAddress']['firstName'] + assert_equal 'Smith', response.params['billingAddress']['lastName'] + end + def test_successful_3ds_purchase @options.merge!(three_d_secure: @three_d_secure) response = @gateway.purchase(@amount, @credit_card, @options) diff --git a/test/remote/gateways/remote_creditcall_test.rb b/test/remote/gateways/remote_creditcall_test.rb index d7ed5a7d2fa..67669780996 100644 --- a/test/remote/gateways/remote_creditcall_test.rb +++ b/test/remote/gateways/remote_creditcall_test.rb @@ -147,7 +147,7 @@ def test_failed_verify @declined_card.number = '' response = @gateway.verify(@declined_card, @options) assert_failure response - assert_match %r{PAN Must be >= 13 Digits}, response.message + assert_match %r{PAN Must be >= 12 Digits}, response.message end def test_invalid_login @@ -155,7 +155,7 @@ def test_invalid_login response = gateway.purchase(@amount, @credit_card, @options) assert_failure response - assert_match %r{Invalid TerminalID - Must be 8 digit number}, response.message + assert_match %r{Invalid terminal details}, response.message end def test_transcript_scrubbing diff --git a/test/remote/gateways/remote_credorax_test.rb b/test/remote/gateways/remote_credorax_test.rb index 30b2dddab3f..0729af56b1c 100644 --- a/test/remote/gateways/remote_credorax_test.rb +++ b/test/remote/gateways/remote_credorax_test.rb @@ -9,11 +9,21 @@ def setup @credit_card = credit_card('4176661000001015', verification_value: '281', month: '12') @fully_auth_card = credit_card('5223450000000007', brand: 'mastercard', verification_value: '090', month: '12') @declined_card = credit_card('4176661000001111', verification_value: '681', month: '12') - @three_ds_card = credit_card('4761739000060016', verification_value: '212', month: '12') + @three_ds_card = credit_card('5455330200000016', verification_value: '737', month: '10', year: Time.now.year + 2) + @address = { + name: 'Jon Smith', + address1: '123 Your Street', + address2: 'Apt 2', + city: 'Toronto', + state: 'ON', + zip: 'K2C3N7', + country: 'CA', + phone_number: '(123)456-7890' + } @options = { order_id: '1', currency: 'EUR', - billing_address: address, + billing_address: @address, description: 'Store Purchase' } @normalized_3ds_2_options = { @@ -21,8 +31,8 @@ def setup shopper_email: 'john.smith@test.com', shopper_ip: '77.110.174.153', shopper_reference: 'John Smith', - billing_address: address(), - shipping_address: address(), + billing_address: @address, + shipping_address: @address, order_id: '123', execute_threed: true, three_ds_version: '2', @@ -348,7 +358,7 @@ def test_failed_capture capture = @gateway.capture(0, auth.authorization) assert_failure capture - assert_equal 'Invalid amount', capture.message + assert_equal 'System malfunction', capture.message end def test_successful_purchase_and_void @@ -482,7 +492,7 @@ def test_successful_credit def test_failed_credit_with_zero_amount response = @gateway.credit(0, @declined_card, @options) assert_failure response - assert_equal 'Invalid amount', response.message + assert_equal 'Transaction not allowed for cardholder', response.message end def test_successful_verify diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index a08307d376a..1e0388fc8d3 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -12,6 +12,7 @@ def setup @master_card = credit_card('2222420000001113', brand: 'master') @discover_card = credit_card('6011111111111117', brand: 'discover') + @carnet_card = credit_card('5062280000000002', brand: 'carnet') @visa_network_token = network_tokenization_credit_card( '4111111111111111', @@ -153,6 +154,15 @@ def test_failure_authorize_with_declined_credit_card assert_equal 'INVALID_ACCOUNT', response.error_code end + def test_successful_authorize_with_carnet_card + response = @gateway.authorize(@amount, @carnet_card, @options) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_equal '002', response.params['paymentInformation']['card']['type'] + refute_empty response.params['_links']['capture'] + end + def test_successful_capture authorize = @gateway.authorize(@amount, @visa_card, @options) response = @gateway.capture(@amount, authorize.authorization, @options) @@ -169,13 +179,13 @@ def test_successful_capture_with_partial_amount assert_equal 'PENDING', response.message end - def test_failure_capture_with_higher_amount - authorize = @gateway.authorize(@amount, @visa_card, @options) - response = @gateway.capture(@amount + 10, authorize.authorization, @options) + # def test_failure_capture_with_higher_amount + # authorize = @gateway.authorize(@amount, @visa_card, @options) + # response = @gateway.capture(@amount + 10, authorize.authorization, @options) - assert_failure response - assert_match(/exceeds/, response.params['message']) - end + # assert_failure response + # assert_match(/exceeds/, response.params['message']) + # end def test_successful_purchase response = @gateway.purchase(@amount, @visa_card, @options) @@ -446,69 +456,65 @@ def stored_credential_options(*args, ntid: nil) def test_purchase_using_stored_credential_initial_mit options = stored_credential_options(:merchant, :internet, :initial) - options[:reason_code] = '4' assert auth = @gateway.authorize(@amount, @visa_card, options) assert_success auth assert purchase = @gateway.purchase(@amount, @visa_card, options) assert_success purchase end - def test_purchase_using_stored_credential_recurring_cit + def test_purchase_using_stored_credential_with_discover options = stored_credential_options(:cardholder, :recurring, :initial) - options[:reason_code] = '4' - assert auth = @gateway.authorize(@amount, @visa_card, options) + assert auth = @gateway.authorize(@amount, @discover_card, options) assert_success auth used_store_credentials = stored_credential_options(:cardholder, :recurring, ntid: auth.network_transaction_id) - used_store_credentials[:reason_code] = '4' - assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) + assert purchase = @gateway.purchase(@amount, @discover_card, used_store_credentials) assert_success purchase end - def test_purchase_using_stored_credential_recurring_mit - options = stored_credential_options(:merchant, :recurring, :initial) - options[:reason_code] = '4' + def test_purchase_using_stored_credential_recurring_non_us + options = stored_credential_options(:cardholder, :recurring, :initial) + options[:billing_address][:country] = 'CA' + options[:billing_address][:state] = 'ON' + options[:billing_address][:city] = 'Ottawa' + options[:billing_address][:zip] = 'K1C2N6' assert auth = @gateway.authorize(@amount, @visa_card, options) assert_success auth used_store_credentials = stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id) - used_store_credentials[:reason_code] = '4' assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) assert_success purchase end - def test_purchase_using_stored_credential_installment_cit - options = stored_credential_options(:cardholder, :installment, :initial) - options[:reason_code] = '4' + def test_purchase_using_stored_credential_recurring_cit + options = stored_credential_options(:cardholder, :recurring, :initial) assert auth = @gateway.authorize(@amount, @visa_card, options) assert_success auth - used_store_credentials = stored_credential_options(:cardholder, :installment, ntid: auth.network_transaction_id) - used_store_credentials[:reason_code] = '4' + used_store_credentials = stored_credential_options(:cardholder, :recurring, ntid: auth.network_transaction_id) assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) assert_success purchase end - def test_purchase_using_stored_credential_installment_mit - options = stored_credential_options(:merchant, :installment, :initial) - options[:reason_code] = '4' + def test_purchase_using_stored_credential_recurring_mit + options = stored_credential_options(:merchant, :recurring, :initial) assert auth = @gateway.authorize(@amount, @visa_card, options) assert_success auth - used_store_credentials = stored_credential_options(:merchant, :installment, ntid: auth.network_transaction_id) - used_store_credentials[:reason_code] = '4' + used_store_credentials = stored_credential_options(:merchant, :recurring, ntid: auth.network_transaction_id) assert purchase = @gateway.purchase(@amount, @visa_card, used_store_credentials) assert_success purchase end - def test_failure_stored_credential_invalid_reason_code - options = stored_credential_options(:cardholder, :internet, :initial) - assert auth = @gateway.authorize(@amount, @master_card, options) - assert_equal(auth.params['status'], 'INVALID_REQUEST') - assert_equal(auth.params['message'], 'Declined - One or more fields in the request contains invalid data') - assert_equal(auth.params['details'].first['field'], 'processingInformation.authorizationOptions.initiator.merchantInitiatedTransaction.reason') + def test_purchase_using_stored_credential_installment + options = stored_credential_options(:cardholder, :installment, :initial) + assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth + used_store_credentials = stored_credential_options(:merchant, :installment, ntid: auth.network_transaction_id) + assert purchase = @gateway.authorize(@amount, @visa_card, options.merge(used_store_credentials)) + assert_success purchase end def test_auth_and_purchase_with_network_txn_id options = stored_credential_options(:merchant, :recurring, :initial) - options[:reason_code] = '4' assert auth = @gateway.authorize(@amount, @visa_card, options) + assert_success auth assert purchase = @gateway.purchase(@amount, @visa_card, options.merge(network_transaction_id: auth.network_transaction_id)) assert_success purchase end @@ -550,16 +556,6 @@ def test_successful_purchase_with_solution_id ActiveMerchant::Billing::CyberSourceGateway.application_id = nil end - def test_successful_purchase_in_australian_dollars - @options[:currency] = 'AUD' - response = @gateway.purchase(@amount, @visa_card, @options) - assert_success response - assert response.test? - assert_equal 'AUTHORIZED', response.message - assert_nil response.params['_links']['capture'] - assert_equal 'AUD', response.params['orderInformation']['amountDetails']['currency'] - end - def test_successful_authorize_with_3ds2_visa @options[:three_d_secure] = { version: '2.2.0', @@ -585,4 +581,49 @@ def test_successful_authorize_with_3ds2_mastercard auth = @gateway.authorize(@amount, @master_card, @options) assert_success auth end + + def test_successful_purchase_with_level_2_data + response = @gateway.purchase(@amount, @visa_card, @options.merge({ purchase_order_number: '13829012412' })) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_successful_purchase_with_level_2_and_3_data + options = { + purchase_order_number: '6789', + discount_amount: '150', + ships_from_postal_code: '90210', + line_items: [ + { + productName: 'Product Name', + kind: 'debit', + quantity: 10, + unitPrice: '9.5000', + totalAmount: '95.00', + taxAmount: '5.00', + discountAmount: '0.00', + productCode: '54321', + commodityCode: '98765' + }, + { + productName: 'Other Product Name', + kind: 'debit', + quantity: 1, + unitPrice: '2.5000', + totalAmount: '90.00', + taxAmount: '2.00', + discountAmount: '1.00', + productCode: '54322', + commodityCode: '98766' + } + ] + } + assert response = @gateway.purchase(@amount, @visa_card, @options.merge(options)) + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end end diff --git a/test/remote/gateways/remote_cyber_source_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 1ff2469ca47..d421d1f21ca 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -76,6 +76,14 @@ def setup source: :network_token ) + @carnet_credit_card = credit_card( + '5062280000000002', + verification_value: '321', + month: '12', + year: (Time.now.year + 2).to_s, + brand: :carnet + ) + @amount = 100 @options = { @@ -96,6 +104,10 @@ def setup ignore_cvv: 'true', commerce_indicator: 'internet', user_po: 'ABC123', + merchant_descriptor_country: 'US', + merchant_descriptor_state: 'NY', + merchant_descriptor_city: 'test123', + submerchant_id: 'AVSBSGDHJMNGFR', taxable: true, sales_slip_number: '456', airline_agent_code: '7Q', @@ -129,6 +141,8 @@ def setup + '1111111115555555222233101abcdefghijkl7777777777777777777777777promotionCde' end + # Scrubbing is working but may fail at the @credit_card.verification_value assertion + # if the the 3 digits are showing up in the Cybersource requestID def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -190,7 +204,7 @@ def test_successful_authorize_with_solution_id_and_stored_creds } @options[:commerce_indicator] = 'internet' - assert response = @gateway.authorize(@amount, @credit_card, @options) + assert response = @gateway.authorize(@amount, @master_credit_card, @options) assert_successful_response(response) assert !response.authorization.blank? ensure @@ -435,6 +449,12 @@ def test_successful_purchase assert_successful_response(response) end + def test_successful_purchase_with_carnet_card + assert response = @gateway.purchase(@amount, @carnet_credit_card, @options) + assert_successful_response(response) + assert_equal '002', response.params['cardType'] + end + def test_successful_purchase_with_bank_account bank_account = check({ account_number: '4100', routing_number: '011000015' }) assert response = @gateway.purchase(10000, bank_account, @options) diff --git a/test/remote/gateways/remote_d_local_test.rb b/test/remote/gateways/remote_d_local_test.rb index 376c8c8c9fc..7a2ff15f34a 100644 --- a/test/remote/gateways/remote_d_local_test.rb +++ b/test/remote/gateways/remote_d_local_test.rb @@ -4,7 +4,7 @@ class RemoteDLocalTest < Test::Unit::TestCase def setup @gateway = DLocalGateway.new(fixtures(:d_local)) - @amount = 200 + @amount = 1000 @credit_card = credit_card('4111111111111111') @credit_card_naranja = credit_card('5895627823453005') @cabal_credit_card = credit_card('5896 5700 0000 0004') @@ -50,6 +50,12 @@ def test_successful_purchase assert_match 'The payment was paid', response.message end + def test_successful_purchase_with_ip_and_phone + response = @gateway.purchase(@amount, @credit_card, @options.merge(ip: '127.0.0.1')) + assert_success response + assert_match 'The payment was paid', response.message + end + def test_successful_purchase_with_save_option response = @gateway.purchase(@amount, @credit_card, @options.merge(save: true)) assert_success response diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb index 43d74f755ed..5415f84e886 100644 --- a/test/remote/gateways/remote_datatrans_test.rb +++ b/test/remote/gateways/remote_datatrans_test.rb @@ -5,7 +5,7 @@ def setup @gateway = DatatransGateway.new(fixtures(:datatrans)) @amount = 756 - @credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025) + @credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: Time.now.year + 1) @bad_amount = 100000 # anything grather than 500 EUR @credit_card_frictionless = credit_card('4000001000000018', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025) @@ -30,6 +30,7 @@ def setup } @billing_address = address + @no_country_billing_address = address(country: nil) @google_pay_card = network_tokenization_credit_card( '4900000000000094', @@ -182,6 +183,48 @@ def test_successful_void assert_equal response.authorization, nil end + def test_succesful_store_transaction + store = @gateway.store(@credit_card, @options) + assert_success store + assert_include store.params, 'overview' + assert_equal store.params['overview'], { 'total' => 1, 'successful' => 1, 'failed' => 0 } + assert store.params['responses'].is_a?(Array) + assert_include store.params['responses'][0], 'alias' + assert_equal store.params['responses'][0]['maskedCC'], '424242xxxxxx4242' + assert_include store.params['responses'][0], 'fingerprint' + end + + def test_successful_unstore + store = @gateway.store(@credit_card, @options) + assert_success store + + unstore = @gateway.unstore(store.authorization, @options) + assert_success unstore + assert_equal unstore.params['response_code'], 204 + end + + def test_successful_store_purchase_unstore_flow + store = @gateway.store(@credit_card, @options) + assert_success store + + purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success purchase + assert_include purchase.params, 'transactionId' + + # second purchase to validate multiple use token + second_purchase = @gateway.purchase(@amount, store.authorization, @options) + assert_success second_purchase + + unstore = @gateway.unstore(store.authorization, @options) + assert_success unstore + + # purchase after unstore to validate deletion + response = @gateway.purchase(@amount, store.authorization, @options) + assert_failure response + assert_equal response.error_code, 'INVALID_ALIAS' + assert_equal response.message, 'authorize.card.alias' + end + def test_failed_void_because_captured_transaction omit("the transaction could take about 20 minutes to pass from settle to transmited, use a previos @@ -195,6 +238,16 @@ def test_failed_void_because_captured_transaction assert_equal 'Action denied : Wrong transaction status', response.message end + def test_successful_verify + verify_response = @gateway.verify(@credit_card, @options) + assert_success verify_response + end + + def test_failed_verify + verify_response = @gateway.verify(@credit_card, @options.merge({ currency: 'DKK' })) + assert_failure verify_response + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.purchase(@amount, @credit_card, @options) @@ -211,6 +264,12 @@ def test_successful_purchase_with_billing_address assert_success response end + def test_successful_purchase_with_no_country_billing_address + response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @no_country_billing_address })) + + assert_success response + end + def test_successful_purchase_with_network_token response = @gateway.purchase(@amount, @nt_credit_card, @options) diff --git a/test/remote/gateways/remote_decidir_plus_test.rb b/test/remote/gateways/remote_decidir_plus_test.rb index 0f36584dab5..5a27ae05fc8 100644 --- a/test/remote/gateways/remote_decidir_plus_test.rb +++ b/test/remote/gateways/remote_decidir_plus_test.rb @@ -160,7 +160,7 @@ def test_successful_verify def test_failed_verify assert response = @gateway_auth.verify(@declined_card, @options) assert_failure response - assert_equal 'missing: fraud_detection', response.message + assert_equal '10734: Fraud Detection Data is required', response.message end def test_successful_store @@ -217,7 +217,7 @@ def test_successful_purchase_with_fraud_detection response = @gateway_purchase.purchase(@amount, payment_reference, options) assert_success response - assert_equal({ 'status' => nil }, response.params['fraud_detection']) + assert_equal({ 'send_to_cs' => false, 'status' => nil }, response.params['fraud_detection']) end def test_successful_purchase_with_card_brand diff --git a/test/remote/gateways/remote_decidir_test.rb b/test/remote/gateways/remote_decidir_test.rb index 6f91f22778c..022c071a668 100644 --- a/test/remote/gateways/remote_decidir_test.rb +++ b/test/remote/gateways/remote_decidir_test.rb @@ -33,9 +33,12 @@ def setup @network_token = network_tokenization_credit_card( '4012001037141112', brand: 'visa', - eci: '05', - payment_cryptogram: '000203016912340000000FA08400317500000000', - name: 'Tesest payway' + eci: '07', + payment_cryptogram: '060103078512340000000FA08400317400000000', + name: 'Tesest payway', + verification_value: '840', + month: '12', + year: '2027' ) @failed_message = ['PEDIR AUTORIZACION | request_authorization_card', 'COMERCIO INVALIDO | invalid_card'] @@ -63,7 +66,7 @@ def test_successful_purchase_with_amex assert response.authorization end - def test_successful_purchase_with_network_token + def test_successful_purchase_with_network_token_visa options = { card_holder_door_number: 1234, card_holder_birthday: '200988', diff --git a/test/remote/gateways/remote_ebanx_test.rb b/test/remote/gateways/remote_ebanx_test.rb index 266c7b4e2ed..5d480ed8632 100644 --- a/test/remote/gateways/remote_ebanx_test.rb +++ b/test/remote/gateways/remote_ebanx_test.rb @@ -344,4 +344,96 @@ def test_successful_purchase_with_long_order_id assert_success response assert_equal 'Accepted', response.message end + + def test_successful_purchase_with_stored_credentials_cardholder_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'recurring', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment', + network_transaction_id: nil + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'installment', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_not_initial + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = @gateway.purchase(@amount, @credit_card, options) + assert_success response + end end diff --git a/test/remote/gateways/remote_elavon_test.rb b/test/remote/gateways/remote_elavon_test.rb index f4c4356b404..6e92ae67aeb 100644 --- a/test/remote/gateways/remote_elavon_test.rb +++ b/test/remote/gateways/remote_elavon_test.rb @@ -8,14 +8,14 @@ def setup @multi_currency_gateway = ElavonGateway.new(fixtures(:elavon_multi_currency)) @credit_card = credit_card('4000000000000002') + @mastercard = credit_card('5121212121212124') @bad_credit_card = credit_card('invalid') @options = { email: 'paul@domain.com', description: 'Test Transaction', billing_address: address, - ip: '203.0.113.0', - merchant_initiated_unscheduled: 'N' + ip: '203.0.113.0' } @shipping_address = { address1: '733 Foster St.', @@ -207,32 +207,181 @@ def test_authorize_and_successful_void assert response.authorization end - def test_successful_auth_and_capture_with_recurring_stored_credential - stored_credential_params = { - initial_transaction: true, - reason_type: 'recurring', - initiator: 'merchant', - network_transaction_id: nil + def test_stored_credentials_with_pass_in_card + # Initial CIT authorize + initial_params = { + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } } - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) - assert_success auth - assert auth.authorization - - assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) - assert_success capture - - @options[:stored_credential] = { - initial_transaction: false, - reason_type: 'recurring', - initiator: 'merchant', - network_transaction_id: auth.network_transaction_id + # X.16 amount invokes par_value and association_token_data in response + assert initial = @gateway.authorize(116, @mastercard, @options.merge(initial_params)) + assert_success initial + approval_code = initial.authorization.split(';').first + ntid = initial.network_transaction_id + par_value = initial.params['par_value'] + association_token_data = initial.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + approval_code: approval_code, + par_value: par_value, + association_token_data: association_token_data, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } } - - assert next_auth = @gateway.authorize(@amount, @credit_card, @options) - assert next_auth.authorization - - assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) - assert_success capture + assert unscheduled = @gateway.purchase(@amount, @mastercard, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + approval_code: approval_code, + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @gateway.purchase(@amount, @mastercard, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + installments: '4', + payment_number: '2', + approval_code: approval_code, + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @gateway.purchase(@amount, @mastercard, @options.merge(installment_params)) + assert_success installment + end + + def test_stored_credentials_with_tokenized_card + # Store card + assert store = @tokenization_gateway.store(@mastercard, @options) + assert_success store + stored_card = store.authorization + + # Initial CIT authorize + initial_params = { + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + } + assert initial = @tokenization_gateway.authorize(116, stored_card, @options.merge(initial_params)) + assert_success initial + ntid = initial.network_transaction_id + par_value = initial.params['par_value'] + association_token_data = initial.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + par_value: par_value, + association_token_data: association_token_data, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert unscheduled = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + installments: '4', + payment_number: '2', + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @tokenization_gateway.purchase(@amount, stored_card, @options.merge(installment_params)) + assert_success installment + end + + def test_stored_credentials_with_manual_token + # Initial CIT get token request + get_token_params = { + add_recurring_token: 'Y', + stored_credential: { + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder', + network_transaction_id: nil + } + } + assert get_token = @tokenization_gateway.authorize(116, @mastercard, @options.merge(get_token_params)) + assert_success get_token + ntid = get_token.network_transaction_id + token = get_token.params['token'] + par_value = get_token.params['par_value'] + association_token_data = get_token.params['association_token_data'] + + # Subsequent unscheduled MIT purchase, with additional data + unscheduled_params = { + ssl_token: token, + par_value: par_value, + association_token_data: association_token_data, + stored_credential: { + reason_type: 'unscheduled', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert unscheduled = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(unscheduled_params)) + assert_success unscheduled + + # Subsequent recurring MIT purchase + recurring_params = { + ssl_token: token, + stored_credential: { + reason_type: 'recurring', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert recurring = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(recurring_params)) + assert_success recurring + + # Subsequent installment MIT purchase + installment_params = { + ssl_token: token, + installments: '4', + payment_number: '2', + stored_credential: { + reason_type: 'installment', + initiator: 'merchant', + network_transaction_id: ntid + } + } + assert installment = @tokenization_gateway.purchase(@amount, @credit_card, @options.merge(installment_params)) + assert_success installment end def test_successful_purchase_with_recurring_token @@ -273,62 +422,6 @@ def test_successful_purchase_with_ssl_token assert_equal 'APPROVAL', purchase.message end - def test_successful_auth_and_capture_with_unscheduled_stored_credential - stored_credential_params = { - initial_transaction: true, - reason_type: 'unscheduled', - initiator: 'merchant', - network_transaction_id: nil - } - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) - assert_success auth - assert auth.authorization - - assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) - assert_success capture - - @options[:stored_credential] = { - initial_transaction: false, - reason_type: 'unscheduled', - initiator: 'merchant', - network_transaction_id: auth.network_transaction_id - } - - assert next_auth = @gateway.authorize(@amount, @credit_card, @options) - assert next_auth.authorization - - assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) - assert_success capture - end - - def test_successful_auth_and_capture_with_installment_stored_credential - stored_credential_params = { - initial_transaction: true, - reason_type: 'installment', - initiator: 'merchant', - network_transaction_id: nil - } - assert auth = @gateway.authorize(@amount, @credit_card, @options.merge({ stored_credential: stored_credential_params })) - assert_success auth - assert auth.authorization - - assert capture = @gateway.capture(@amount, auth.authorization, authorization_validated: true) - assert_success capture - - @options[:stored_credential] = { - initial_transaction: false, - reason_type: 'installment', - initiator: 'merchant', - network_transaction_id: auth.network_transaction_id - } - - assert next_auth = @gateway.authorize(@amount, @credit_card, @options) - assert next_auth.authorization - - assert capture = @gateway.capture(@amount, next_auth.authorization, authorization_validated: true) - assert_success capture - end - def test_successful_store_without_verify assert response = @tokenization_gateway.store(@credit_card, @options) assert_success response @@ -390,6 +483,16 @@ def test_failed_purchase_with_token assert_match %r{invalid}i, response.message end + def test_successful_authorize_with_token + store_response = @tokenization_gateway.store(@credit_card, @options) + token = store_response.params['token'] + assert response = @tokenization_gateway.authorize(@amount, token, @options) + assert_success response + assert response.test? + assert_not_empty response.params['token'] + assert_equal 'APPROVAL', response.message + end + def test_successful_purchase_with_custom_fields assert response = @gateway.purchase(@amount, @credit_card, @options.merge(custom_fields: { my_field: 'a value' })) @@ -401,11 +504,11 @@ def test_successful_purchase_with_custom_fields end def test_failed_purchase_with_multi_currency_terminal_setting_disabled - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'USD', multi_currency: true)) + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(currency: 'ZAR', multi_currency: true)) assert_failure response assert response.test? - assert_equal 'Transaction currency is not allowed for this terminal. Your terminal must be setup with Multi currency', response.message + assert_equal 'The transaction currency sent is not supported', response.message assert response.authorization end @@ -429,7 +532,7 @@ def test_successful_purchase_with_multi_currency_transaction_setting end def test_successful_purchase_with_level_3_fields - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(level_3_data: @level_3_data)) + assert response = @gateway.purchase(500, @credit_card, @options.merge(level_3_data: @level_3_data)) assert_success response assert_equal 'APPROVAL', response.message @@ -445,7 +548,7 @@ def test_successful_purchase_with_shipping_address end def test_successful_purchase_with_shipping_address_and_l3 - assert response = @gateway.purchase(@amount, @credit_card, @options.merge(shipping_address: @shipping_address).merge(level_3_data: @level_3_data)) + assert response = @gateway.purchase(500, @credit_card, @options.merge(shipping_address: @shipping_address).merge(level_3_data: @level_3_data)) assert_success response assert_equal 'APPROVAL', response.message diff --git a/test/remote/gateways/remote_fat_zebra_test.rb b/test/remote/gateways/remote_fat_zebra_test.rb index 17da0b1c94d..8e85b032369 100644 --- a/test/remote/gateways/remote_fat_zebra_test.rb +++ b/test/remote/gateways/remote_fat_zebra_test.rb @@ -233,7 +233,7 @@ def test_successful_purchase_with_3DS version: '2.0', cavv: '3q2+78r+ur7erb7vyv66vv\/\/\/\/8=', eci: '05', - ds_transaction_id: 'ODUzNTYzOTcwODU5NzY3Qw==', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', enrolled: 'true', authentication_response_status: 'Y' } diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index 0b3c6f86782..9c8813ac5e4 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -26,7 +26,8 @@ def setup cvv_result_code: '111', cavv_result_code: '111', timezone_utc_offset: '-5', - billing_address: address.merge(name: 'Cure Tester') + billing_address: address.merge(name: 'Cure Tester'), + extra_data: '' } @cit_options = @options.merge( @@ -114,12 +115,57 @@ def test_successful_purchase_mit assert_equal 'APPROVED', response.message end - def test_failed_purchase + def test_successful_purchase_mit_with_billing_address + set_credentials! + @options[:billing_address] = address.merge(name: 'Jhon Doe', country: 'US') + response = @gateway.purchase(@amount, @credit_card_mit, @options) + assert_success response + assert_equal 'APPROVED', response.message + end + + def test_successful_authorize_cit + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + assert_equal 'CAPTUREREQUIRED', response.message + end + + def test_successful_authorize_and_capture_cit + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + assert_equal 'CAPTUREREQUIRED', response.message + + assert capture = @gateway.capture(@amount, response.authorization) + assert_success capture + end + + def test_failed_purchase_invalid_time + set_credentials! + response = @gateway.purchase(@amount, @credit_card_cit, @options.merge({ mit_expiry_date_utc: '' })) + assert_failure response + assert_equal nil, response.error_code + assert_not_nil response.params['TraceId'] + assert_equal response.message, '{"ExpiryDateUtc":["The field ExpiryDateUtc is invalid."]}' + end + + def test_failed_purchase_required_fields set_credentials! response = @gateway.purchase(@amount, @credit_card_cit, billing_address: address) assert_failure response assert_equal nil, response.error_code assert_not_nil response.params['TraceId'] + error_list = JSON.parse response.message + assert_equal error_list.length, 7 + assert_equal error_list['OrderId'], ["Merchant's orderId is required"] + assert_equal error_list['Transaction.Id'], ['The Id field is required.'] + assert_equal error_list['Transaction.ResponseCode'], ['The ResponseCode field is required.'] + assert_equal error_list['Transaction.AvsResultCode'], ['The AvsResultCode field is required.'] + assert_equal error_list['Transaction.CvvResultCode'], ['The CvvResultCode field is required.'] + assert_equal error_list['Transaction.CavvResultCode'], ['The CavvResultCode field is required.'] + assert_equal error_list['Transaction.ResponseCodeSource'], ['The ResponseCodeSource field is required.'] end def test_failed_cit_declined_purchase @@ -139,6 +185,16 @@ def test_successful_refund assert_equal 'DECLINED', refund.message end + def test_successful_void + @cit_options[:phone] = '998888' + set_credentials! + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + + assert void = @gateway.void(response.authorization) + assert_success void + end + def test_partial_refund omit('Partial refunds requires to raise some limits on merchant account') set_credentials! @@ -174,9 +230,15 @@ def test_successful_purchase_with_token end def test_successful_inquire_request + @cit_options[:phone] = '998888' set_credentials! - response = @gateway.inquire('f8da8dc7-17de-4b5e-858d-4bdc47cd5dbf', {}) + + response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) + assert_success response + + response = @gateway.inquire(response.authorization, {}) assert_success response + assert_equal 'CAPTUREREQUIRED', response.message end def test_unsuccessful_inquire_request diff --git a/test/remote/gateways/remote_hps_test.rb b/test/remote/gateways/remote_hps_test.rb index 9f7a0e08c24..d5343cf751e 100644 --- a/test/remote/gateways/remote_hps_test.rb +++ b/test/remote/gateways/remote_hps_test.rb @@ -362,7 +362,6 @@ def test_transcript_scrubbing_with_cryptogram credit_card = network_tokenization_credit_card( '4242424242424242', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, eci: '05', source: :apple_pay ) @@ -435,56 +434,6 @@ def test_successful_auth_with_apple_pay_raw_cryptogram_without_eci assert_equal 'Success', response.message end - def test_successful_purchase_with_android_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - eci: '05', - source: :android_pay - ) - assert response = @gateway.purchase(@amount, credit_card, @options) - assert_success response - assert_equal 'Success', response.message - end - - def test_successful_purchase_with_android_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - source: :android_pay - ) - assert response = @gateway.purchase(@amount, credit_card, @options) - assert_success response - assert_equal 'Success', response.message - end - - def test_successful_auth_with_android_pay_raw_cryptogram_with_eci - credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - eci: '05', - source: :android_pay - ) - assert response = @gateway.authorize(@amount, credit_card, @options) - assert_success response - assert_equal 'Success', response.message - end - - def test_successful_auth_with_android_pay_raw_cryptogram_without_eci - credit_card = network_tokenization_credit_card( - '4242424242424242', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - verification_value: nil, - source: :android_pay - ) - assert response = @gateway.authorize(@amount, credit_card, @options) - assert_success response - assert_equal 'Success', response.message - end - def test_successful_purchase_with_google_pay_raw_cryptogram_with_eci credit_card = network_tokenization_credit_card( '4242424242424242', diff --git a/test/remote/gateways/remote_iveri_test.rb b/test/remote/gateways/remote_iveri_test.rb index 0ced8b40be3..d68147c2edd 100644 --- a/test/remote/gateways/remote_iveri_test.rb +++ b/test/remote/gateways/remote_iveri_test.rb @@ -133,17 +133,21 @@ def test_failed_void def test_successful_verify response = @gateway.verify(@credit_card, @options) + # authorization portion is successful since we use that as the main response assert_success response assert_equal 'Authorisation', response.responses[0].params['transaction_command'] assert_equal '0', response.responses[0].params['result_status'] - assert_equal 'Void', response.responses[1].params['transaction_command'] + # authorizationreversal portion is successful + assert_success response.responses.last + assert_equal 'AuthorisationReversal', response.responses[1].params['transaction_command'] assert_equal '0', response.responses[1].params['result_status'] assert_equal 'Succeeded', response.message end def test_failed_verify response = @gateway.verify(@bad_card, @options) - assert_failure response + assert_failure response # assert failure of authorization portion + assert_failure response.responses.last # assert failure of authorisationvoid portion assert_includes ['Denied', 'Hot card', 'Please call'], response.message end diff --git a/test/remote/gateways/remote_litle_test.rb b/test/remote/gateways/remote_litle_test.rb index c16c628ee2f..0dcecc08266 100644 --- a/test/remote/gateways/remote_litle_test.rb +++ b/test/remote/gateways/remote_litle_test.rb @@ -76,6 +76,18 @@ def setup payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' } ) + + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + source: :network_token, + month: '02', + year: '2050', + brand: 'master', + number: '5112010000000000', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) + @check = check( name: 'Tom Black', routing_number: '011075150', @@ -260,6 +272,12 @@ def test_successful_purchase_with_google_pay assert_equal 'Approved', response.message end + def test_successful_purchase_with_network_token + assert response = @gateway.purchase(10100, @decrypted_network_token) + assert_success response + assert_equal 'Approved', response.message + end + def test_successful_purchase_with_level_two_data_visa options = @options.merge( level_2_data: { @@ -597,6 +615,12 @@ def test_authorize_and_capture_with_stored_credential_cit_card_on_file assert_equal 'Approved', capture.message end + def test_authorize_with_network_token + assert response = @gateway.authorize(10100, @decrypted_network_token) + assert_success response + assert_equal 'Approved', response.message + end + def test_purchase_with_stored_credential_cit_card_on_file_non_ecommerce credit_card = CreditCard.new(@credit_card_hash.merge( number: '4457000800000002', @@ -872,4 +896,15 @@ def test_echeck_scrubbing assert_scrubbed(@gateway.options[:login], transcript) assert_scrubbed(@gateway.options[:password], transcript) end + + def test_network_token_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.purchase(10010, @decrypted_network_token, @options) + end + transcript = @gateway.scrub(transcript) + assert_scrubbed(@decrypted_network_token.number, transcript) + assert_scrubbed(@decrypted_network_token.payment_cryptogram, transcript) + assert_scrubbed(@gateway.options[:login], transcript) + assert_scrubbed(@gateway.options[:password], transcript) + end end diff --git a/test/remote/gateways/remote_mercado_pago_test.rb b/test/remote/gateways/remote_mercado_pago_test.rb index 9aab14911f3..3cdf17ef28c 100644 --- a/test/remote/gateways/remote_mercado_pago_test.rb +++ b/test/remote/gateways/remote_mercado_pago_test.rb @@ -38,7 +38,7 @@ def setup @options = { billing_address: address, shipping_address: address, - email: 'user+br@example.com', + email: 'test_user_1390220683@testuser.com', description: 'Store Purchase' } @processing_options = { @@ -125,6 +125,12 @@ def test_successful_purchase_with_notification_url assert_equal 'https://www.spreedly.com/', response.params['notification_url'] end + def test_successful_purchase_with_idempotency_key + response = @gateway.purchase(@amount, @credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_equal 'accredited', response.message + end + def test_successful_purchase_with_payer response = @gateway.purchase(@amount, @credit_card, @options.merge({ payer: @payer })) assert_success response @@ -157,6 +163,12 @@ def test_successful_authorize_and_capture assert_equal 'accredited', capture.message end + def test_successful_authorize_with_idempotency_key + response = @gateway.authorize(@amount, @credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_equal 'accredited', response.message + end + def test_successful_authorize_and_capture_with_elo auth = @gateway.authorize(@amount, @elo_credit_card, @options) assert_success auth @@ -312,6 +324,12 @@ def test_successful_verify assert_match %r{pending_capture}, response.message end + def test_successful_verify_with_idempotency_key + response = @gateway.verify(@credit_card, @options.merge(idempotency_key: '0d5020ed-1af6-469c-ae06-c3bec19954bb')) + assert_success response + assert_match %r{pending_capture}, response.message + end + def test_successful_verify_with_amount @options[:amount] = 200 response = @gateway.verify(@credit_card, @options) @@ -363,4 +381,31 @@ def test_transcript_scrubbing assert_scrubbed(@credit_card.verification_value, transcript) assert_scrubbed(@gateway.options[:access_token], transcript) end + + def test_successful_purchase_with_3ds + three_ds_cc = credit_card('5483928164574623', verification_value: '123', month: 11, year: 2025) + @options[:execute_threed] = true + + response = @gateway.purchase(290, three_ds_cc, @options) + + assert_success response + assert_equal 'pending_challenge', response.message + assert_include response.params, 'three_ds_info' + assert_equal response.params['three_ds_info']['external_resource_url'], 'https://api.mercadopago.com/cardholder_authenticator/v2/prod/browser-challenges' + assert_include response.params['three_ds_info'], 'creq' + end + + def test_successful_purchase_with_3ds_mandatory + three_ds_cc = credit_card('5031755734530604', verification_value: '123', month: 11, year: 2025) + @options[:execute_threed] = true + @options[:three_ds_mode] = 'mandatory' + + response = @gateway.purchase(290, three_ds_cc, @options) + + assert_success response + assert_equal 'pending_challenge', response.message + assert_include response.params, 'three_ds_info' + assert_equal response.params['three_ds_info']['external_resource_url'], 'https://api.mercadopago.com/cardholder_authenticator/v2/prod/browser-challenges' + assert_include response.params['three_ds_info'], 'creq' + end end diff --git a/test/remote/gateways/remote_merchant_warrior_test.rb b/test/remote/gateways/remote_merchant_warrior_test.rb index 284dd8ab890..852e79ce380 100644 --- a/test/remote/gateways/remote_merchant_warrior_test.rb +++ b/test/remote/gateways/remote_merchant_warrior_test.rb @@ -60,7 +60,7 @@ def test_successful_purchase def test_failed_purchase assert purchase = @gateway.purchase(@success_amount, @expired_card, @options) - assert_match 'Card has expired', purchase.message + assert_match 'Transaction declined', purchase.message assert_failure purchase assert_not_nil purchase.params['transaction_id'] assert_equal purchase.params['transaction_id'], purchase.authorization diff --git a/test/remote/gateways/remote_mercury_test.rb b/test/remote/gateways/remote_mercury_test.rb index 37079eaf852..60bbdd01f3c 100644 --- a/test/remote/gateways/remote_mercury_test.rb +++ b/test/remote/gateways/remote_mercury_test.rb @@ -8,11 +8,12 @@ def setup @gateway = MercuryGateway.new(fixtures(:mercury)) @amount = 100 + @decline_amount = 257 - @credit_card = credit_card('4003000123456781', brand: 'visa', month: '12', year: '18') + @credit_card = credit_card('4895281000000006', brand: 'visa', month: '12', year: Time.now.year) - @track_1_data = '%B4003000123456781^LONGSEN/L. ^18121200000000000000**123******?*' - @track_2_data = ';5413330089010608=2512101097750213?' + @track_1_data = "%B#{@credit_card.number}^LONGSEN/L. ^18121200000000000000**111******?*" + @track_2_data = ";#{@credit_card.number}=18121200000000000000?" @options = { order_id: 'c111111111.1', @@ -21,8 +22,8 @@ def setup @options_with_billing = @options.merge( merchant: '999', billing_address: { - address1: '4 Corporate SQ', - zip: '30329' + address1: '123 Main Street', + zip: '45209' } ) @full_options = @options_with_billing.merge( @@ -46,13 +47,13 @@ def test_successful_authorize_and_capture end def test_failed_authorize - response = @gateway.authorize(1100, @credit_card, @options) + response = @gateway.authorize(@decline_amount, @credit_card, @options) assert_failure response assert_equal 'DECLINE', response.message end def test_purchase_and_void - response = @gateway.purchase(102, @credit_card, @options) + response = @gateway.purchase(@amount, @credit_card, @options) assert_success response void = @gateway.void(response.authorization) @@ -82,13 +83,14 @@ def test_credit end def test_failed_purchase - response = @gateway.purchase(1100, @credit_card, @options) + response = @gateway.purchase(@decline_amount, @credit_card, @options) assert_failure response assert_equal 'DECLINE', response.message assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code end def test_avs_and_cvv_results + @credit_card.verification_value = '222' response = @gateway.authorize(333, @credit_card, @options_with_billing) assert_success response @@ -136,9 +138,7 @@ def test_partial_capture end def test_authorize_with_bad_expiration_date - @credit_card.month = 13 - @credit_card.year = 2001 - response = @gateway.authorize(575, @credit_card, @options_with_billing) + response = @gateway.authorize(267, @credit_card, @options_with_billing) assert_failure response assert_equal 'INVLD EXP DATE', response.message end @@ -213,7 +213,7 @@ def test_authorize_and_capture_without_tokenization def test_successful_authorize_and_capture_with_track_1_data @credit_card.track_data = @track_1_data - response = @gateway.authorize(100, @credit_card, @options) + response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_equal '1.00', response.params['authorize'] diff --git a/test/remote/gateways/remote_nmi_test.rb b/test/remote/gateways/remote_nmi_test.rb index bd77940790b..8fd9b211d31 100644 --- a/test/remote/gateways/remote_nmi_test.rb +++ b/test/remote/gateways/remote_nmi_test.rb @@ -10,15 +10,26 @@ def setup routing_number: '123123123', account_number: '123123123' ) - @apple_pay_card = network_tokenization_credit_card( + @apple_pay = network_tokenization_credit_card( '4111111111111111', payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', month: '01', - year: '2024', + year: Time.new.year + 2, source: :apple_pay, eci: '5', transaction_id: '123456789' ) + + @google_pay = network_tokenization_credit_card( + '4111111111111111', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + month: '01', + year: Time.new.year + 2, + source: :google_pay, + transaction_id: '123456789', + eci: '05' + ) + @options = { order_id: generate_unique_id, billing_address: address, @@ -99,6 +110,34 @@ def test_successful_purchase assert response.authorization end + def test_successful_purchase_with_customer_vault_data + vault_id = SecureRandom.hex(16) + + options = { + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase', + customer_vault: 'add_customer' + } + + assert response = @gateway.purchase(@amount, @credit_card, options.merge(customer_vault_id: vault_id)) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert_equal vault_id, response.params['customer_vault_id'] + assert response.authorization + end + + def test_successful_purchase_with_customer_vault_and_auto_generate_customer_vault_id + assert response = @gateway.purchase(@amount, @credit_card, @options.merge(customer_vault: 'add_customer')) + assert_success response + assert response.test? + + assert_equal 'Succeeded', response.message + assert response.params.include?('customer_vault_id') + assert response.authorization + end + def test_successful_purchase_sans_cvv @credit_card.verification_value = nil assert response = @gateway.purchase(@amount, @credit_card, @options) @@ -130,17 +169,55 @@ def test_failed_purchase_with_echeck assert_equal 'FAILED', response.message end - def test_successful_purchase_with_apple_pay_card - assert @gateway.supports_network_tokenization? - assert response = @gateway.purchase(@amount, @apple_pay_card, @options) + def test_successful_purchase_with_apple_pay + assert @gateway_secure.supports_network_tokenization? + assert response = @gateway_secure.purchase(@amount, @apple_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_with_google_pay + assert @gateway_secure.supports_network_tokenization? + assert response = @gateway_secure.purchase(@amount, @google_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_google_pay_without_billing_address + assert @gateway_secure.supports_network_tokenization? + @options.delete(:billing_address) + + assert response = @gateway_secure.purchase(@amount, @google_pay, @options) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert response.authorization + end + + def test_successful_purchase_apple_pay_without_billing_address + assert @gateway_secure.supports_network_tokenization? + @options.delete(:billing_address) + + assert response = @gateway_secure.purchase(@amount, @apple_pay, @options) assert_success response assert response.test? assert_equal 'Succeeded', response.message assert response.authorization end - def test_failed_purchase_with_apple_pay_card - assert response = @gateway.purchase(99, @apple_pay_card, @options) + def test_failed_purchase_with_apple_pay + assert response = @gateway_secure.purchase(1, @apple_pay, @options) + assert_failure response + assert response.test? + assert_equal 'DECLINE', response.message + end + + def test_failed_purchase_with_google_pay + assert response = @gateway_secure.purchase(1, @google_pay, @options) assert_failure response assert response.test? assert_equal 'DECLINE', response.message @@ -304,6 +381,34 @@ def test_successful_verify assert_match 'Succeeded', response.message end + def test_successful_verify_with_customer_vault_data + vault_id = SecureRandom.hex(16) + + options = { + order_id: generate_unique_id, + billing_address: address, + description: 'Store purchase', + customer_vault: 'add_customer' + } + + assert response = @gateway.verify(@credit_card, options.merge(customer_vault_id: vault_id)) + assert_success response + assert response.test? + assert_equal 'Succeeded', response.message + assert_equal vault_id, response.params['customer_vault_id'] + assert response.authorization + end + + def test_successful_verify_with_customer_vault_and_auto_generate_customer_vault_id + assert response = @gateway.verify(@credit_card, @options.merge(customer_vault: 'add_customer')) + assert_success response + assert response.test? + + assert_equal 'Succeeded', response.message + assert response.params.include?('customer_vault_id') + assert response.authorization + end + def test_failed_verify card = credit_card(year: 2010) response = @gateway.verify(card, @options) @@ -482,12 +587,23 @@ def test_check_transcript_scrubbing def test_network_tokenization_transcript_scrubbing transcript = capture_transcript(@gateway) do - @gateway.purchase(@amount, @apple_pay_card, @options) + @gateway.purchase(@amount, @apple_pay, @options) end clean_transcript = @gateway.scrub(transcript) - assert_scrubbed(@apple_pay_card.number, clean_transcript) - assert_scrubbed(@apple_pay_card.payment_cryptogram, clean_transcript) + assert_scrubbed(@apple_pay.number, clean_transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, clean_transcript) + assert_password_scrubbed(clean_transcript) + end + + def test_transcript_scrubbing_with_google_pay + transcript = capture_transcript(@gateway) do + @gateway.purchase(@amount, @google_pay, @options) + end + + clean_transcript = @gateway.scrub(transcript) + assert_scrubbed(@apple_pay.number, clean_transcript) + assert_scrubbed(@apple_pay.payment_cryptogram, clean_transcript) assert_password_scrubbed(clean_transcript) end diff --git a/test/remote/gateways/remote_nuvei_test.rb b/test/remote/gateways/remote_nuvei_test.rb new file mode 100644 index 00000000000..51f1ceae5cb --- /dev/null +++ b/test/remote/gateways/remote_nuvei_test.rb @@ -0,0 +1,145 @@ +require 'test_helper' +require 'timecop' + +class RemoteNuveiTest < Test::Unit::TestCase + def setup + @gateway = NuveiGateway.new(fixtures(:nuvei)) + + @amount = 100 + @credit_card = credit_card('4761344136141390', verification_value: '999', first_name: 'Cure', last_name: 'Tester') + @declined_card = credit_card('4000128449498204') + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip: '127.0.0.1' + } + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + + @gateway.scrub(transcript) + end + + def test_successful_session_token_generation + response = @gateway.send(:fetch_session_token, @options) + assert_success response + assert_not_nil response.params[:sessionToken] + end + + def test_failed_session_token_generation + @gateway.options[:merchant_site_id] = 123 + response = @gateway.send(:fetch_session_token, {}) + assert_failure response + assert_match 'Invalid merchant site id', response.message + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + capture_response = @gateway.capture(@amount, response.authorization) + + assert_success capture_response + assert_match 'APPROVED', capture_response.message + end + + def test_successful_zero_auth + response = @gateway.authorize(0, @credit_card, @options) + assert_success response + assert_match 'APPROVED', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + assert_match 'SUCCESS', response.params['status'] + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_failed_purchase_with_invalid_cvv + @credit_card.verification_value = nil + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'ERROR', response.params['transactionStatus'] + assert_match 'Invalid CVV2', response.message + end + + def test_failed_capture_invalid_transaction_id + response = @gateway.capture(@amount, '123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + void_response = @gateway.void(response.authorization) + assert_success void_response + assert_match 'SUCCESS', void_response.params['status'] + assert_match 'APPROVED', void_response.message + end + + def test_failed_void_invalid_transaction_id + response = @gateway.void('123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + refund_response = @gateway.refund(@amount, response.authorization) + assert_success refund_response + assert_match 'SUCCESS', refund_response.params['status'] + assert_match 'APPROVED', refund_response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_general_credit + credit_response = @gateway.credit(@amount, @credit_card, @options.merge!(user_token_id: '123')) + assert_success credit_response + assert_match 'SUCCESS', credit_response.params['status'] + assert_match 'APPROVED', credit_response.message + end + + def test_failed_general_credit + credit_response = @gateway.credit(@amount, @declined_card, @options) + assert_failure credit_response + assert_match 'ERROR', credit_response.params['status'] + assert_match 'Invalid user token', credit_response.message + end +end diff --git a/test/remote/gateways/remote_orbital_test.rb b/test/remote/gateways/remote_orbital_test.rb index b923774c987..27cf8139e66 100644 --- a/test/remote/gateways/remote_orbital_test.rb +++ b/test/remote/gateways/remote_orbital_test.rb @@ -672,13 +672,13 @@ def test_authorize_sends_with_payment_delivery def test_default_payment_delivery_with_no_payment_delivery_sent transcript = capture_transcript(@echeck_gateway) do - @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '4')) + response = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '4')) + assert_equal '1', response.params['approval_status'] + assert_equal '00', response.params['resp_code'] end assert_match(/B/, transcript) assert_match(/A/, transcript) - assert_match(/1/, transcript) - assert_match(/00/, transcript) end def test_sending_echeck_adds_ecp_details_for_refund @@ -692,12 +692,12 @@ def test_sending_echeck_adds_ecp_details_for_refund transcript = capture_transcript(@echeck_gateway) do refund = @echeck_gateway.refund(@amount, capture.authorization, @options.merge(payment_method: @echeck, action_code: 'W6', auth_method: 'I')) assert_success refund + assert_equal '1', refund.params['approval_status'] end assert_match(/W6/, transcript) assert_match(/I/, transcript) assert_match(/R/, transcript) - assert_match(/1/, transcript) end def test_sending_credit_card_performs_correct_refund @@ -714,43 +714,40 @@ def test_sending_credit_card_performs_correct_refund def test_echeck_purchase_with_address_responds_with_name transcript = capture_transcript(@echeck_gateway) do - @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + response = @echeck_gateway.authorize(@amount, @echeck, @options.merge(order_id: '2')) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] end assert_match(/Jim Smith/, transcript) - assert_match(/00/, transcript) - assert_match(/atusMsg>ApprovedTest McTest/, transcript) - assert_match(/00/, transcript) - assert_match(/atusMsg>ApprovedLongbob Longsen/, transcript) - assert_match(/00/, transcript) - assert_match(/Approved/, transcript) end def test_credit_purchase_with_no_address_responds_with_no_name - transcript = capture_transcript(@gateway) do - @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) - end - - assert_match(/00/, transcript) - assert_match(/Approved/, transcript) + response = @gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] end # == Certification Tests @@ -1586,21 +1583,18 @@ def test_failed_capture def test_credit_purchase_with_address_responds_with_name transcript = capture_transcript(@tandem_gateway) do - @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + response = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2')) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] end assert_match(/Longbob Longsen/, transcript) - assert_match(/00/, transcript) - assert_match(/Approved/, transcript) end def test_credit_purchase_with_no_address_responds_with_no_name - transcript = capture_transcript(@tandem_gateway) do - @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) - end - - assert_match(/00/, transcript) - assert_match(/Approved/, transcript) + response = @tandem_gateway.authorize(@amount, @credit_card, @options.merge(order_id: '2', address: nil, billing_address: nil)) + assert_equal '00', response.params['resp_code'] + assert_equal 'Approved', response.params['status_msg'] end def test_void_transactions diff --git a/test/remote/gateways/remote_pay_trace_test.rb b/test/remote/gateways/remote_pay_trace_test.rb index 611fec465a3..cea698408c3 100644 --- a/test/remote/gateways/remote_pay_trace_test.rb +++ b/test/remote/gateways/remote_pay_trace_test.rb @@ -263,7 +263,8 @@ def test_successful_authorize_and_capture_with_level_3_data assert_success capture transaction_id = auth.authorization - assert_equal "Visa/MasterCard enhanced data was successfully added to Transaction ID #{transaction_id}. 2 line item records were created.", capture.message + assert_equal capture.authorization, transaction_id + assert_equal 'Your transaction was successfully captured.', capture.message end def test_failed_authorize diff --git a/test/remote/gateways/remote_paypal_test.rb b/test/remote/gateways/remote_paypal_test.rb index eee5d7aba1a..90dfae72e46 100644 --- a/test/remote/gateways/remote_paypal_test.rb +++ b/test/remote/gateways/remote_paypal_test.rb @@ -172,6 +172,16 @@ def test_successful_voiding assert_success response end + def test_purchase_and_inquire + purchase_response = @gateway.purchase(@amount, @credit_card, @params) + assert_success purchase_response + assert purchase_response.params['transaction_id'] + + response = @gateway.inquire(purchase_response.authorization, {}) + assert_success response + assert_equal 'Success', response.message + end + def test_purchase_and_full_credit purchase = @gateway.purchase(@amount, @credit_card, @params) assert_success purchase diff --git a/test/remote/gateways/remote_pin_test.rb b/test/remote/gateways/remote_pin_test.rb index eaae9661ffa..a228294a6da 100644 --- a/test/remote/gateways/remote_pin_test.rb +++ b/test/remote/gateways/remote_pin_test.rb @@ -17,7 +17,7 @@ def setup description: "Store Purchase #{DateTime.now.to_i}" } - @additional_options_3ds = @options.merge( + @additional_options_3ds_passthrough = @options.merge( three_d_secure: { version: '1.0.2', eci: '06', @@ -25,6 +25,14 @@ def setup xid: 'MDAwMDAwMDAwMDAwMDAwMzIyNzY=' } ) + + @additional_options_3ds = @options.merge( + three_d_secure: { + enabled: true, + fallback_ok: true, + callback_url: 'https://yoursite.com/authentication_complete' + } + ) end def test_successful_purchase @@ -77,6 +85,16 @@ def test_successful_authorize_and_capture end def test_successful_authorize_and_capture_with_passthrough_3ds + authorization = @gateway.authorize(@amount, @credit_card, @additional_options_3ds_passthrough) + assert_success authorization + assert_equal false, authorization.params['response']['captured'] + + response = @gateway.capture(@amount, authorization.authorization, @options) + assert_success response + assert_equal true, response.params['response']['captured'] + end + + def test_successful_authorize_and_capture_with_3ds authorization = @gateway.authorize(@amount, @credit_card, @additional_options_3ds) assert_success authorization assert_equal false, authorization.params['response']['captured'] diff --git a/test/remote/gateways/remote_plexo_test.rb b/test/remote/gateways/remote_plexo_test.rb index e64082b0d82..e856d61e1e4 100644 --- a/test/remote/gateways/remote_plexo_test.rb +++ b/test/remote/gateways/remote_plexo_test.rb @@ -24,13 +24,42 @@ def setup }, identification_type: '1', identification_value: '123456', - billing_address: address + billing_address: address, + invoice_number: '12345abcde' } @cancel_options = { description: 'Test desc', reason: 'requested by client' } + + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + first_name: 'Santiago', last_name: 'Navatta', + brand: 'Mastercard', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '5555555555554444', + source: :network_token, + month: '12', + year: Time.now.year + }) + + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + first_name: 'Joe', last_name: 'Doe', + brand: 'visa', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '5555555555554444', + source: :network_token, + month: '12', + year: Time.now.year + } + ) + end + + def test_successful_purchase_with_network_token + response = @gateway.purchase(@amount, @decrypted_network_token, @options.merge({ invoice_number: '12345abcde' })) + assert_success response + assert_equal 'You have been mocked.', response.message end def test_successful_purchase @@ -153,6 +182,9 @@ def test_failed_void assert_equal 'The selected payment state is not valid.', response.message end + # for verify tests: sometimes those fails but re-running after + # few seconds they can works + def test_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response diff --git a/test/remote/gateways/remote_rapyd_test.rb b/test/remote/gateways/remote_rapyd_test.rb index 40e9564cc17..e90ab2de357 100644 --- a/test/remote/gateways/remote_rapyd_test.rb +++ b/test/remote/gateways/remote_rapyd_test.rb @@ -145,6 +145,14 @@ def test_successful_purchase_with_reccurence_type assert_equal 'SUCCESS', response.message end + def test_successful_purchase_with_save_payment_method + @options[:pm_type] = 'gb_visa_mo_card' + response = @gateway.purchase(@amount, @credit_card, @options.merge(save_payment_method: true)) + assert_success response + assert_equal 'SUCCESS', response.message + assert_equal true, response.params['data']['save_payment_method'] + end + def test_successful_purchase_with_address billing_address = address(name: 'Henry Winkler', address1: '123 Happy Days Lane') @@ -182,7 +190,7 @@ def test_successful_purchase_with_options def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response - assert_equal 'Do Not Honor', response.message + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', response.message end def test_successful_authorize_and_capture @@ -197,7 +205,7 @@ def test_successful_authorize_and_capture def test_failed_authorize response = @gateway.authorize(@amount, @declined_card, @options) assert_failure response - assert_equal 'Do Not Honor', response.message + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', response.message end def test_partial_capture @@ -275,7 +283,7 @@ def test_failed_purchase_with_zero_amount def test_failed_void response = @gateway.void('') assert_failure response - assert_equal 'NOT_FOUND', response.message + assert_equal 'UNAUTHORIZED_API_CALL', response.message end def test_successful_verify @@ -295,7 +303,7 @@ def test_successful_verify_with_peso def test_failed_verify response = @gateway.verify(@declined_card, @options) assert_failure response - assert_equal 'Do Not Honor', response.message + assert_equal 'The request attempted an operation that requires a card number, but the number was not recognized. The request was rejected. Corrective action: Use the card number of a valid card.', response.message end def test_successful_store_and_purchase @@ -334,7 +342,7 @@ def test_failed_unstore unstore = @gateway.unstore('') assert_failure unstore - assert_equal 'NOT_FOUND', unstore.message + assert_equal 'UNAUTHORIZED_API_CALL', unstore.message end def test_invalid_login diff --git a/test/remote/gateways/remote_redsys_rest_test.rb b/test/remote/gateways/remote_redsys_rest_test.rb index 6c4f2361e59..4bb2827f7ba 100644 --- a/test/remote/gateways/remote_redsys_rest_test.rb +++ b/test/remote/gateways/remote_redsys_rest_test.rb @@ -4,7 +4,7 @@ class RemoteRedsysRestTest < Test::Unit::TestCase def setup @gateway = RedsysRestGateway.new(fixtures(:redsys_rest)) @amount = 100 - @credit_card = credit_card('4548812049400004') + @credit_card = credit_card('4548810000000011', verification_value: '123', month: '12', year: '34') @credit_card_no_cvv = credit_card('4548812049400004', verification_value: nil) @declined_card = credit_card @threeds2_credit_card = credit_card('4918019199883839') @@ -183,28 +183,83 @@ def test_successful_purchase_3ds # end # Pending 3DS support - # def test_successful_3ds_authorize_with_exemption - # options = @options.merge(execute_threed: true, terminal: 12) - # response = @gateway.authorize(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) - # assert_success response - # assert response.params['ds_emv3ds'] - # assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] - # assert_equal 'CardConfiguration', response.message - # end + def test_successful_3ds_authorize_with_exemption + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.authorize(@amount, @credit_card, options.merge(three_ds_exemption_type: 'low_value')) + assert_success response + assert response.params['ds_emv3ds'] + assert_equal '2.2.0', response.params['ds_emv3ds']['protocolVersion'] + assert_equal 'CardConfiguration', response.message + end # Pending 3DS support - # def test_successful_3ds_purchase_with_exemption - # options = @options.merge(execute_threed: true, terminal: 12) - # response = @gateway.purchase(@amount, @credit_card, options.merge(sca_exemption: 'LWV')) - # assert_success response - # assert response.params['ds_emv3ds'] - # assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion'] - # assert_equal 'CardConfiguration', response.message - # end + def test_successful_3ds_purchase_with_exemption + options = @options.merge(execute_threed: true, terminal: 12) + response = @gateway.purchase(@amount, @credit_card, options.merge(three_ds_exemption_type: 'low_value')) + assert_success response + assert response.params['ds_emv3ds'] + assert_equal '2.2.0', response.params['ds_emv3ds']['protocolVersion'] + assert_equal 'CardConfiguration', response.message + end + + def test_successful_purchase_using_stored_credential_recurring_cit + initial_options = stored_credential_options(:cardholder, :recurring, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_recurring_mit + initial_options = stored_credential_options(:merchant, :recurring, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_installment_cit + initial_options = stored_credential_options(:cardholder, :installment, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:recurring, :cardholder, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_installment_mit + initial_options = stored_credential_options(:merchant, :installment, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:merchant, :recurring, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end + + def test_successful_purchase_using_stored_credential_unscheduled_cit + initial_options = stored_credential_options(:cardholder, :unscheduled, :initial) + assert initial_purchase = @gateway.purchase(@amount, @credit_card, initial_options) + assert_success initial_purchase + assert network_transaction_id = initial_purchase.params['ds_merchant_cof_txnid'] + used_options = stored_credential_options(:cardholder, :unscheduled, id: network_transaction_id) + assert purchase = @gateway.purchase(@amount, @credit_card, used_options) + assert_success purchase + end private def generate_order_id (Time.now.to_f * 100).to_i.to_s end + + def stored_credential_options(*args, id: nil) + @options.merge(order_id: generate_unique_id, + stored_credential: stored_credential(*args, id: id)) + end end diff --git a/test/remote/gateways/remote_safe_charge_test.rb b/test/remote/gateways/remote_safe_charge_test.rb index 5c9d81fc6b6..8aebfa61f38 100644 --- a/test/remote/gateways/remote_safe_charge_test.rb +++ b/test/remote/gateways/remote_safe_charge_test.rb @@ -63,6 +63,13 @@ def test_successful_purchase assert_equal 'Success', response.message end + def test_successful_purchase_with_card_holder_verification + response = @gateway.purchase(@amount, @credit_card, @options.merge(middle_name: 'middle', card_holder_verification: 1)) + assert_success response + assert_equal 'Success', response.message + assert_equal '', response.params['cardholdernameverification'] + end + def test_successful_purchase_with_token response = @gateway.purchase(@amount, @credit_card, @options) assert_success response diff --git a/test/remote/gateways/remote_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb index fe2769e5115..0c945acc346 100644 --- a/test/remote/gateways/remote_stripe_payment_intents_test.rb +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -126,6 +126,15 @@ def test_successful_purchase assert purchase.params.dig('charges', 'data')[0]['balance_transaction'] end + def test_successful_purchase_google_pay_fpan + options = { + currency: 'GBP', + customer: @customer + } + assert purchase = @gateway.purchase(@amount, @visa_payment_method, options.merge(wallet_type: :non_tokenized_google_pay)) + assert_equal 'succeeded', purchase.params['status'] + end + def test_successful_purchase_with_card_brand options = { currency: 'USD', @@ -305,6 +314,57 @@ def test_successful_purchase_with_apple_pay_when_sending_the_billing_address assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) end + def test_successful_purchase_with_apple_pay_and_cit + options = { + currency: 'GBP', + new_ap_gp_route: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + } + + purchase = @gateway.purchase(@amount, @apple_pay, options) + assert purchase.success? + assert_match('apple_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type']) + end + + def test_succeeds_apple_pay_ntid_and_passes_it_to_mit + options = { + currency: 'GBP', + new_ap_gp_route: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'unscheduled', + initial_transaction: true + } + } + + cit_purchase = @gateway.purchase(@amount, @apple_pay, options) + assert cit_purchase.success? + + assert purchase = @gateway.purchase(@amount, @apple_pay, { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: cit_purchase.params.dig('charges', 'data', 0, 'payment_method_details', 'card', 'network_transaction_id'), + off_session: 'true' + } + }) + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + def test_purchases_with_same_idempotency_key options = { currency: 'GBP', @@ -380,6 +440,17 @@ def test_unsuccessful_purchase refute purchase.params.dig('error', 'payment_intent', 'charges', 'data')[0]['captured'] end + def test_unsuccessful_purchase_returns_header_response + options = { + currency: 'GBP', + customer: @customer + } + assert purchase = @gateway.purchase(@amount, @declined_payment_method, options) + + assert_equal 'Your card was declined.', purchase.message + assert_not_nil purchase.params['response_headers']['stripe_should_retry'] + end + def test_successful_purchase_with_external_auth_data_3ds_1 options = { currency: 'GBP', @@ -670,6 +741,30 @@ def test_create_setup_intent_with_setup_future_usage_and_card_brand assert_not_empty si_response.params.dig('latest_attempt', 'payment_method_details', 'card') end + def test_create_setup_intent_with_setup_future_usage_and_moto_exemption + response = @gateway.create_setup_intent(@visa_card_brand_choice, { + address: { + email: 'test@example.com', + name: 'John Doe', + line1: '1 Test Ln', + city: 'Durham', + tracking_number: '123456789' + }, + currency: 'USD', + confirm: true, + moto: true, + return_url: 'https://example.com' + }) + + assert_equal 'succeeded', response.params['status'] + # since we cannot "click" the stripe hooks URL to confirm the authorization + # we will at least confirm we can retrieve the created setup_intent and it contains the structure we expect + setup_intent_id = response.params['id'] + assert si_response = @gateway.retrieve_setup_intent(setup_intent_id) + assert_equal 'succeeded', si_response.params['status'] + assert_not_empty si_response.params.dig('latest_attempt', 'payment_method_details', 'card') + end + def test_create_setup_intent_with_connected_account [@three_ds_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| assert authorize_response = @gateway.create_setup_intent(card_to_use, { @@ -885,6 +980,25 @@ def test_succeeds_with_initial_cit_3ds_required assert_equal 'requires_action', purchase.params['status'] end + def test_succeeds_with_subsequent_cit_3ds_required + assert purchase = @gateway.purchase(@amount, @visa_card, { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: '1098510912210968' + } + }) + assert_success purchase + assert_equal 'succeeded', purchase.params['status'] + assert purchase.params.dig('charges', 'data')[0]['captured'] + assert purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_transaction_id'] + end + def test_succeeds_with_mit assert purchase = @gateway.purchase(@amount, @visa_card, { currency: 'USD', @@ -1241,10 +1355,15 @@ def test_create_a_payment_intent_and_void capture_method: 'manual', confirm: true } + + void_options = { + cancellation_reason: 'requested_by_customer', + order_id: '123445abcde' + } assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) intent_id = create_response.params['id'] - assert cancel_response = @gateway.void(intent_id, cancellation_reason: 'requested_by_customer') + assert cancel_response = @gateway.void(intent_id, void_options) assert_equal @amount, cancel_response.params.dig('charges', 'data')[0].dig('amount_refunded') assert_equal 'canceled', cancel_response.params['status'] assert_equal 'requested_by_customer', cancel_response.params['cancellation_reason'] @@ -1301,14 +1420,20 @@ def test_refund_a_payment_intent capture_method: 'manual', confirm: true } + + refund_options = { + order_id: '123445abcde' + } + assert create_response = @gateway.create_intent(@amount, @visa_payment_method, options) intent_id = create_response.params['id'] assert @gateway.capture(@amount, intent_id, options) - assert refund = @gateway.refund(@amount - 20, intent_id) + assert refund = @gateway.refund(@amount - 20, intent_id, refund_options) assert_equal @amount - 20, refund.params['charge']['amount_refunded'] assert_equal true, refund.params['charge']['captured'] + assert_equal '123445abcde', refund.params['metadata']['order_id'] refund_id = refund.params['id'] assert_equal refund.authorization, refund_id end @@ -1485,6 +1610,13 @@ def test_request_three_d_secure assert purchase = @gateway.purchase(@amount, @three_ds_not_required_card, options) assert_equal 'requires_action', purchase.params['status'] + options = { + currency: 'GBP', + request_three_d_secure: 'challenge' + } + assert purchase = @gateway.purchase(@amount, @three_ds_not_required_card, options) + assert_equal 'requires_action', purchase.params['status'] + options = { currency: 'GBP' } diff --git a/test/remote/gateways/remote_stripe_test.rb b/test/remote/gateways/remote_stripe_test.rb index 85417f3c583..490c218e2bd 100644 --- a/test/remote/gateways/remote_stripe_test.rb +++ b/test/remote/gateways/remote_stripe_test.rb @@ -207,6 +207,14 @@ def test_unsuccessful_purchase assert_match(/ch_[a-zA-Z\d]+/, response.authorization) end + def test_unsuccessful_purchase_returns_response_headers + assert response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match %r{Your card was declined}, response.message + assert_match Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + assert_not_nil response.params['response_headers']['stripe_should_retry'] + end + def test_unsuccessful_purchase_with_destination_and_amount destination = fixtures(:stripe_destination)[:stripe_user_id] custom_options = @options.merge(destination: destination, destination_amount: @amount + 20) @@ -326,10 +334,18 @@ def test_successful_void_with_metadata assert_success response assert response.authorization - assert void = @gateway.void(response.authorization, metadata: { test_metadata: 123 }) + void_options = { + metadata: { + test_metadata: 123 + }, + order_id: '123445abcde' + } + + assert void = @gateway.void(response.authorization, void_options) assert void.test? assert_success void assert_equal '123', void.params['metadata']['test_metadata'] + assert_equal '123445abcde', void.params['metadata']['order_id'] end def test_successful_void_with_reason @@ -386,6 +402,27 @@ def test_successful_refund_with_reason assert_equal 'fraudulent', refund.params['reason'] end + def test_successful_refund_with_metada_and_order_id + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert response.authorization + + refund_options = { + metadata: { + test_metadata: 123 + }, + order_id: '123445abcde' + } + + assert refund = @gateway.refund(@amount - 20, response.authorization, refund_options) + assert refund.test? + refund_id = refund.params['id'] + assert_equal refund.authorization, refund_id + assert_success refund + assert_equal '123445abcde', refund.params['metadata']['order_id'] + assert_equal '123', refund.params['metadata']['test_metadata'] + end + def test_successful_refund_on_verified_bank_account customer_id = @verified_bank_account[:customer_id] bank_account_id = @verified_bank_account[:bank_account_id] diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb index adc9f4551d4..f74e0372ea1 100644 --- a/test/remote/gateways/remote_worldpay_test.rb +++ b/test/remote/gateways/remote_worldpay_test.rb @@ -59,11 +59,8 @@ def setup invoice_reference_number: 'INV12233565', customer_reference: 'CUST00000101', card_acceptor_tax_id: 'VAT1999292', - sales_tax: { - amount: '20', - exponent: '2', - currency: 'USD' - } + tax_amount: '20', + ship_from_postal_code: '43245' } } @@ -71,58 +68,32 @@ def setup level_3_data: { customer_reference: 'CUST00000102', card_acceptor_tax_id: 'VAT1999285', - sales_tax: { - amount: '20', - exponent: '2', - currency: 'USD' - }, - discount_amount: { - amount: '1', - exponent: '2', - currency: 'USD' - }, - shipping_amount: { - amount: '50', - exponent: '2', - currency: 'USD' - }, - duty_amount: { - amount: '20', - exponent: '2', - currency: 'USD' - }, - item: { + tax_amount: '20', + discount_amount: '1', + shipping_amount: '50', + duty_amount: '20', + line_items: [{ description: 'Laptop 14', product_code: 'LP00125', commodity_code: 'COM00125', quantity: '2', - unit_cost: { - amount: '1500', - exponent: '2', - currency: 'USD' - }, + unit_cost: '1500', unit_of_measure: 'each', - item_total: { - amount: '3000', - exponent: '2', - currency: 'USD' - }, - item_total_with_tax: { - amount: '3500', - exponent: '2', - currency: 'USD' - }, - item_discount_amount: { - amount: '200', - exponent: '2', - currency: 'USD' - }, - tax_amount: { - amount: '500', - exponent: '2', - currency: 'USD' - } - } + discount_amount: '200', + tax_amount: '500', + total_amount: '3300' + }, + { + description: 'Laptop 15', + product_code: 'LP00125', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: '1500', + unit_of_measure: 'each', + discount_amount: '200', + tax_amount: '500', + total_amount: '3300' + }] } } @@ -176,6 +147,50 @@ def setup transaction_id: '123456789', eci: '05' ) + + @aft_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + middle: 'Middle', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + middle: 'Middle', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' + } + } + } end def test_successful_purchase @@ -705,7 +720,7 @@ def test_successful_purchase_with_level_two_fields end def test_successful_purchase_with_level_two_fields_and_sales_tax_zero - @level_two_data[:level_2_data][:sales_tax][:amount] = 0 + @level_two_data[:level_2_data][:tax_amount] = 0 assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_two_data)) assert_success response assert_equal true, response.params['ok'] @@ -720,12 +735,13 @@ def test_successful_purchase_with_level_three_fields end def test_unsuccessful_purchase_level_three_data_without_item_mastercard - @level_three_data[:level_3_data][:item] = {} + @level_three_data[:level_3_data][:line_items] = [{ + }] @credit_card.brand = 'master' assert response = @gateway.purchase(@amount, @credit_card, @options.merge(@level_three_data)) assert_failure response assert_equal response.error_code, '2' - assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item is incomplete, it must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,taxAmount?,categories?,pageURL?,imageURL?).' + assert_equal response.params['error'].gsub(/\"+/, ''), 'The content of element type item must match (description,productCode?,commodityCode?,quantity?,unitCost?,unitOfMeasure?,itemTotal?,itemTotalWithTax?,itemDiscountAmount?,itemTaxRate?,lineDiscountIndicator?,itemLocalTaxRate?,itemLocalTaxAmount?,taxAmount?,categories?,pageURL?,imageURL?).' end def test_successful_purchase_with_level_two_and_three_fields @@ -949,6 +965,34 @@ def test_successful_mastercard_credit_on_cft_gateway assert_equal 'SUCCESS', credit.message end + def test_successful_visa_account_funding_transfer + credit = @gateway.credit(@amount, @credit_card, @options.merge(@aft_options)) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_successful_visa_account_funding_transfer_via_token + assert store = @gateway.store(@credit_card, @store_options) + assert_success store + + credit = @gateway.credit(@amount, store.authorization, @options.merge(@aft_options)) + assert_success credit + assert_equal 'SUCCESS', credit.message + end + + def test_failed_visa_account_funding_transfer + credit = @gateway.credit(@amount, credit_card('4111111111111111', name: 'REFUSED'), @options.merge(@aft_options)) + assert_failure credit + assert_equal 'REFUSED', credit.message + end + + def test_failed_visa_account_funding_transfer_acquirer_error + credit = @gateway.credit(@amount, credit_card('4111111111111111', name: 'ACQERROR'), @options.merge(@aft_options)) + assert_failure credit + assert_equal 'ACQUIRER ERROR', credit.message + assert_equal '20', credit.error_code + end + # These three fast_fund_credit tests are currently failing with the message: Disbursement transaction not supported # It seems that the current sandbox setup does not support testing this. diff --git a/test/unit/connection_test.rb b/test/unit/connection_test.rb index 338718a99b4..c6bdc1d72e4 100644 --- a/test/unit/connection_test.rb +++ b/test/unit/connection_test.rb @@ -27,7 +27,7 @@ def test_connection_endpoint_raises_uri_error def test_connection_passes_env_proxy_by_default spy = Net::HTTP.new('example.com', 443) - Net::HTTP.expects(:new).with('example.com', 443, :ENV, nil).returns(spy) + Net::HTTP.expects(:new).with('example.com', 443, :ENV, nil, nil, nil).returns(spy) spy.expects(:start).returns(true) spy.expects(:get).with('/tx.php', { 'connection' => 'close' }).returns(@ok) @connection.request(:get, nil, {}) @@ -36,8 +36,10 @@ def test_connection_passes_env_proxy_by_default def test_connection_does_pass_requested_proxy @connection.proxy_address = 'proxy.example.com' @connection.proxy_port = 8080 + @connection.proxy_user = 'user' + @connection.proxy_password = 'password' spy = Net::HTTP.new('example.com', 443) - Net::HTTP.expects(:new).with('example.com', 443, 'proxy.example.com', 8080).returns(spy) + Net::HTTP.expects(:new).with('example.com', 443, 'proxy.example.com', 8080, 'user', 'password').returns(spy) spy.expects(:start).returns(true) spy.expects(:get).with('/tx.php', { 'connection' => 'close' }).returns(@ok) @connection.request(:get, nil, {}) diff --git a/test/unit/credit_card_methods_test.rb b/test/unit/credit_card_methods_test.rb index c5eaeb293e0..cc9ffa9753f 100644 --- a/test/unit/credit_card_methods_test.rb +++ b/test/unit/credit_card_methods_test.rb @@ -29,9 +29,10 @@ def maestro_bins %w[500032 500057 501015 501016 501018 501020 501021 501023 501024 501025 501026 501027 501028 501029 501038 501039 501040 501041 501043 501045 501047 501049 501051 501053 501054 501055 501056 501057 501058 501060 501061 501062 501063 501066 501067 501072 501075 501083 501087 501623 - 501800 501089 501091 501092 501095 501104 501105 501107 501108 501500 501879 + 501800 501089 501091 501092 501095 501104 501105 501107 501108 501109 501500 501879 502000 502113 502301 503175 503645 503800 503670 504310 504338 504363 504533 504587 504620 504639 504656 504738 504781 504910 + 505616 507001 507002 507004 507082 507090 560014 560565 561033 572402 572610 572626 576904 578614 585274 585697 586509 588729 588792 589244 589300 589407 589471 589605 589633 589647 589671 590043 590206 590263 590265 @@ -361,6 +362,8 @@ def test_should_detect_naranja_card assert_equal 'naranja', CreditCard.brand?('5895627823453005') assert_equal 'naranja', CreditCard.brand?('5895620000000002') assert_equal 'naranja', CreditCard.brand?('5895626746595650') + assert_equal 'naranja', CreditCard.brand?('5895628637412581') + assert_equal 'naranja', CreditCard.brand?('5895627087232438') end # Alelo BINs beginning with the digit 4 overlap with Visa's range of valid card numbers. @@ -444,9 +447,10 @@ def test_matching_invalid_card end def test_matching_valid_naranja - number = '5895627823453005' - assert_equal 'naranja', CreditCard.brand?(number) - assert CreditCard.valid_number?(number) + %w[5895627823453005 5895627087232438 5895628637412581].each do |number| + assert_equal 'naranja', CreditCard.brand?(number) + assert CreditCard.valid_number?(number) + end end def test_matching_valid_creditel @@ -568,6 +572,27 @@ def test_should_validate_tuya_card assert_false CreditCard.valid_number?('5888_0000_0000_0030') end + def test_should_detect_uatp_card_brand + assert_equal 'uatp', CreditCard.brand?('117500000000000') + assert_equal 'uatp', CreditCard.brand?('117515279008103') + assert_equal 'uatp', CreditCard.brand?('129001000000000') + end + + def test_should_validate_uatp_card + assert_true CreditCard.valid_number?('117515279008103') + assert_true CreditCard.valid_number?('116901000000000') + assert_true CreditCard.valid_number?('195724000000000') + assert_true CreditCard.valid_number?('192004000000000') + assert_true CreditCard.valid_number?('135410014004955') + end + + def test_should_detect_invalid_uatp_card + assert_false CreditCard.valid_number?('117515279008104') + assert_false CreditCard.valid_number?('116901000000001') + assert_false CreditCard.valid_number?('195724000000001') + assert_false CreditCard.valid_number?('192004000000001') + end + def test_credit_card? assert credit_card.credit_card? end diff --git a/test/unit/gateways/adyen_test.rb b/test/unit/gateways/adyen_test.rb index 28a766f6ca6..48b6d717c30 100644 --- a/test/unit/gateways/adyen_test.rb +++ b/test/unit/gateways/adyen_test.rb @@ -63,6 +63,15 @@ def setup verification_value: nil ) + @google_pay_card = network_tokenization_credit_card( + '4761209980011439', + payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', + month: '11', + year: '2022', + source: :google_pay, + verification_value: nil + ) + @nt_credit_card = network_tokenization_credit_card( '4895370015293175', brand: 'visa', @@ -262,6 +271,16 @@ def test_failed_authorize assert_failure response end + def test_failure_authorize_with_transient_error + @gateway.instance_variable_set(:@response_headers, { 'transient-error' => 'error_will_robinson' }) + @gateway.expects(:ssl_post).returns(failed_authorize_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert response.params['response_headers']['transient_error'], 'error_will_robinson' + assert response.test? + end + def test_standard_error_code_mapping @gateway.expects(:ssl_post).returns(failed_billing_field_response) @@ -350,24 +369,26 @@ def test_failed_authorise_visa response = @gateway.send(:commit, 'authorise', {}, {}) assert_equal 'Refused | 01: Refer to card issuer', response.message + assert_equal '01', response.error_code assert_failure response end - def test_failed_authorise_mastercard - @gateway.expects(:ssl_post).returns(failed_authorize_mastercard_response) + def test_failed_fraud_raw_refusal + @gateway.expects(:ssl_post).returns(failed_fraud_visa_response) response = @gateway.send(:commit, 'authorise', {}, {}) - assert_equal 'Refused | 01 : New account information available', response.message + assert_equal 'N7', response.error_code assert_failure response end - def test_failed_authorise_mastercard_raw_error_message + def test_failed_authorise_mastercard @gateway.expects(:ssl_post).returns(failed_authorize_mastercard_response) - response = @gateway.send(:commit, 'authorise', {}, { raw_error_message: true }) + response = @gateway.send(:commit, 'authorise', {}, {}) - assert_equal 'Refused | 01: Refer to card issuer', response.message + assert_equal 'Refused | 01 : New account information available', response.message + assert_equal '01', response.error_code assert_failure response end @@ -521,6 +542,24 @@ def test_splits_sent end.respond_with(successful_authorize_response) end + def test_splits_sent_without_amount + split_data = [{ + 'type' => 'MarketPlace', + 'account' => '163298747', + 'reference' => 'QXhlbFN0b2x0ZW5iZXJnCg' + }, { + 'type' => 'Commission', + 'reference' => 'THVjYXNCbGVkc29lCg' + }] + + options = @options.merge({ splits: split_data }) + stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_equal split_data, JSON.parse(data)['splits'] + end.respond_with(successful_authorize_response) + end + def test_execute_threed_false_with_additional_data stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge({ execute_threed: false, overwrite_brand: true, selected_brand: 'maestro' })) @@ -647,7 +686,7 @@ def test_stored_credential_recurring_mit_initial response = stub_comms do @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| - assert_match(/"shopperInteraction":"ContAuth"/, data) + assert_match(/"shopperInteraction":"Ecommerce"/, data) assert_match(/"recurringProcessingModel":"Subscription"/, data) end.respond_with(successful_authorize_response) @@ -697,7 +736,7 @@ def test_stored_credential_unscheduled_mit_initial response = stub_comms do @gateway.authorize(@amount, @credit_card, options) end.check_request do |_endpoint, data, _headers| - assert_match(/"shopperInteraction":"ContAuth"/, data) + assert_match(/"shopperInteraction":"Ecommerce"/, data) assert_match(/"recurringProcessingModel":"UnscheduledCardOnFile"/, data) end.respond_with(successful_authorize_response) @@ -1211,7 +1250,45 @@ def test_authorize_with_network_tokenization_credit_card_no_name def test_authorize_with_network_tokenization_credit_card response = stub_comms do - @gateway.authorize(@amount, @apple_pay_card, @options) + @gateway.authorize(@amount, @apple_pay_card, @options.merge(switch_cryptogram_mapping_nt: false)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', parsed['mpiData']['cavv'] + assert_equal '07', parsed['mpiData']['eci'] + assert_equal 'applepay', parsed['additionalData']['paymentdatasource.type'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_google_pay + response = stub_comms do + @gateway.authorize(@amount, @google_pay_card, @options.merge(selected_brand: 'visa')) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal @google_pay_card.payment_cryptogram, parsed['mpiData']['cavv'] + assert_equal '07', parsed['mpiData']['eci'] + assert_equal 'googlepay', parsed['additionalData']['paymentdatasource.type'] + assert_equal 'googlepay', parsed['selectedBrand'] + assert_equal 'true', parsed['additionalData']['paymentdatasource.tokenized'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_google_pay_pan_only + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(wallet_type: :google_pay)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'googlepay', parsed['additionalData']['paymentdatasource.type'] + assert_equal 'googlepay', parsed['selectedBrand'] + assert_equal 'false', parsed['additionalData']['paymentdatasource.tokenized'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_using_ld_option + response = stub_comms do + @gateway.authorize(@amount, @apple_pay_card, @options.merge(switch_cryptogram_mapping_nt: true)) end.check_request do |_endpoint, data, _headers| parsed = JSON.parse(data) assert_equal 'YwAAAAAABaYcCMX/OhNRQAAAAAA=', parsed['mpiData']['cavv'] @@ -1221,6 +1298,32 @@ def test_authorize_with_network_tokenization_credit_card assert_success response end + def test_authorize_with_network_tokenization_credit_card_no_apple_no_google + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge(switch_cryptogram_mapping_nt: true)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', parsed['mpiData']['tokenAuthenticationVerificationValue'] + assert_equal '07', parsed['mpiData']['eci'] + assert_nil parsed['additionalData']['paymentdatasource.type'] + assert_equal 'VISATOKENSERVICE', parsed['recurring']['tokenService'] + assert_equal 'EXTERNAL', parsed['recurring']['contract'] + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_authorize_with_network_tokenization_credit_card_and_stored_credentials + stored_credential = stored_credential(:merchant, :recurring) + response = stub_comms do + @gateway.authorize(@amount, @nt_credit_card, @options.merge(switch_cryptogram_mapping_nt: true, stored_credential: stored_credential)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + assert_equal 'ContAuth', parsed['shopperInteraction'] + assert_nil parsed['mpiData'] + end.respond_with(successful_authorize_response) + assert_success response + end + def test_authorize_and_capture_with_network_transaction_id auth = stub_comms do @gateway.authorize(@amount, @credit_card, @options) @@ -1496,6 +1599,62 @@ def test_succesful_additional_airline_data assert_success response end + def test_succesful_additional_airline_data_with_legs + airline_data = { + agency_invoice_number: 'BAC123', + agency_plan_name: 'plan name', + airline_code: '434234', + airline_designator_code: '1234', + boarding_fee: '100', + computerized_reservation_system: 'abcd', + customer_reference_number: 'asdf1234', + document_type: 'cc', + legs: [ + { + carrier_code: 'KL', + date_of_travel: '2024-10-10' + }, + { + carrier_code: 'KL', + date_of_travel: '2024-10-11' + } + ], + passenger: { + first_name: 'Joe', + last_name: 'Doe' + } + } + + response = stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(additional_data_airline: airline_data)) + end.check_request do |_endpoint, data, _headers| + parsed = JSON.parse(data) + additional_data = parsed['additionalData'] + assert_equal additional_data['airline.agency_invoice_number'], airline_data[:agency_invoice_number] + assert_equal additional_data['airline.agency_plan_name'], airline_data[:agency_plan_name] + assert_equal additional_data['airline.airline_code'], airline_data[:airline_code] + assert_equal additional_data['airline.airline_designator_code'], airline_data[:airline_designator_code] + assert_equal additional_data['airline.boarding_fee'], airline_data[:boarding_fee] + assert_equal additional_data['airline.computerized_reservation_system'], airline_data[:computerized_reservation_system] + assert_equal additional_data['airline.customer_reference_number'], airline_data[:customer_reference_number] + assert_equal additional_data['airline.document_type'], airline_data[:document_type] + assert_equal additional_data['airline.flight_date'], airline_data[:flight_date] + assert_equal additional_data['airline.ticket_issue_address'], airline_data[:abcqwer] + assert_equal additional_data['airline.ticket_number'], airline_data[:ticket_number] + assert_equal additional_data['airline.travel_agency_code'], airline_data[:travel_agency_code] + assert_equal additional_data['airline.travel_agency_name'], airline_data[:travel_agency_name] + assert_equal additional_data['airline.passenger_name'], airline_data[:passenger_name] + assert_equal additional_data['airline.leg1.carrier_code'], airline_data[:legs][0][:carrier_code] + assert_equal additional_data['airline.leg1.date_of_travel'], airline_data[:legs][0][:date_of_travel] + assert_equal additional_data['airline.leg2.carrier_code'], airline_data[:legs][1][:carrier_code] + assert_equal additional_data['airline.leg2.date_of_travel'], airline_data[:legs][1][:date_of_travel] + assert_equal additional_data['airline.passenger.first_name'], airline_data[:passenger][:first_name] + assert_equal additional_data['airline.passenger.last_name'], airline_data[:passenger][:last_name] + assert_equal additional_data['airline.passenger.telephone_number'], airline_data[:passenger][:telephone_number] + end.respond_with(successful_authorize_response) + assert_success response + end + def test_additional_data_lodging lodging_data = { check_in_date: '20230822', @@ -1916,6 +2075,20 @@ def failed_authorize_visa_response RESPONSE end + def failed_fraud_visa_response + <<-RESPONSE + { + "additionalData": + { + "refusalReasonRaw": "N7 : FRAUD" + }, + "refusalReason": "Refused", + "pspReference":"8514775559925128", + "resultCode":"Refused" + } + RESPONSE + end + def failed_without_raw_refusal_reason <<-RESPONSE { diff --git a/test/unit/gateways/braintree_blue_test.rb b/test/unit/gateways/braintree_blue_test.rb index b948d11cf6f..d0c00eb57e7 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -254,6 +254,24 @@ def test_customer_has_default_payment_method @gateway.authorize(100, 'fake-paypal-future-nonce', options) end + def test_not_adding_default_payment_method_to_customer + options = { + prevent_default_payment_method: true, + payment_method_nonce: 'fake-paypal-future-nonce', + store: true, + device_data: 'device_data', + paypal: { + paypal_flow_type: 'checkout_with_vault' + } + } + + Braintree::TransactionGateway.any_instance.expects(:sale).returns(braintree_result(paypal: { implicitly_vaulted_payment_method_token: 'abc123' })) + + Braintree::CustomerGateway.any_instance.expects(:update).with(nil, { default_payment_method_token: 'abc123' }).never + + @gateway.authorize(100, 'fake-paypal-future-nonce', options) + end + def test_risk_data_can_be_specified risk_data = { customer_browser: 'User-Agent Header', @@ -922,7 +940,7 @@ def test_that_setting_a_wiredump_device_on_the_gateway_sets_the_braintree_logger end end - def test_solution_id_is_added_to_create_transaction_parameters + def test_channel_is_added_to_create_transaction_parameters assert_nil @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {})[:channel] ActiveMerchant::Billing::BraintreeBlueGateway.application_id = 'ABC123' assert_equal @gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {})[:channel], 'ABC123' @@ -933,6 +951,15 @@ def test_solution_id_is_added_to_create_transaction_parameters ActiveMerchant::Billing::BraintreeBlueGateway.application_id = nil end + def test_override_application_id_is_sent_to_channel + gateway = BraintreeBlueGateway.new(merchant_id: 'test', public_key: 'test', private_key: 'test', channel: 'overidden-channel') + gateway_response = gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), {}) + assert_equal gateway_response[:channel], 'overidden-channel' + + gateway_response = gateway.send(:create_transaction_parameters, 100, credit_card('41111111111111111111'), { override_application_id: 'override-application-id' }) + assert_equal gateway_response[:channel], 'override-application-id' + end + def test_successful_purchase_with_descriptor Braintree::TransactionGateway.any_instance.expects(:sale).with do |params| (params[:descriptor][:name] == 'wow*productname') && @@ -1166,7 +1193,7 @@ def test_stored_credential_recurring_cit_initial external_vault: { status: 'will_vault' }, - transaction_source: '' + transaction_source: 'recurring_first' } ) ).returns(braintree_result) @@ -1182,7 +1209,7 @@ def test_stored_credential_recurring_cit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: '' + transaction_source: 'recurring' } ) ).returns(braintree_result) @@ -1190,6 +1217,22 @@ def test_stored_credential_recurring_cit_used @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :recurring, id: '123ABC') }) end + def test_stored_credential_prefers_options_for_ntid + Braintree::TransactionGateway.any_instance.expects(:sale).with( + standard_purchase_params.merge( + { + external_vault: { + status: 'vaulted', + previous_network_transaction_id: '321XYZ' + }, + transaction_source: 'recurring' + } + ) + ).returns(braintree_result) + + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', network_transaction_id: '321XYZ', stored_credential: stored_credential(:cardholder, :recurring, id: '123ABC') }) + end + def test_stored_credential_recurring_mit_initial Braintree::TransactionGateway.any_instance.expects(:sale).with( standard_purchase_params.merge( @@ -1197,7 +1240,7 @@ def test_stored_credential_recurring_mit_initial external_vault: { status: 'will_vault' }, - transaction_source: 'recurring' + transaction_source: 'recurring_first' } ) ).returns(braintree_result) @@ -1228,7 +1271,7 @@ def test_stored_credential_installment_cit_initial external_vault: { status: 'will_vault' }, - transaction_source: '' + transaction_source: 'installment_first' } ) ).returns(braintree_result) @@ -1244,7 +1287,7 @@ def test_stored_credential_installment_cit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: '' + transaction_source: 'installment' } ) ).returns(braintree_result) @@ -1259,7 +1302,7 @@ def test_stored_credential_installment_mit_initial external_vault: { status: 'will_vault' }, - transaction_source: 'recurring' + transaction_source: 'installment_first' } ) ).returns(braintree_result) @@ -1275,7 +1318,7 @@ def test_stored_credential_installment_mit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: 'recurring' + transaction_source: 'installment' } ) ).returns(braintree_result) @@ -1372,7 +1415,7 @@ def test_stored_credential_v2_recurring_first_cit_initial ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: { initiator: 'merchant', reason_type: 'recurring_first', initial_transaction: true } }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: { initiator: 'merchant', reason_type: 'recurring_first', initial_transaction: true } }) end def test_stored_credential_moto_cit_initial @@ -1402,7 +1445,7 @@ def test_stored_credential_v2_recurring_first ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :recurring, :initial) }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :recurring, :initial) }) end def test_stored_credential_v2_follow_on_recurring_first @@ -1418,7 +1461,7 @@ def test_stored_credential_v2_follow_on_recurring_first ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :recurring, id: '123ABC') }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :recurring, id: '123ABC') }) end def test_stored_credential_v2_installment_first @@ -1433,7 +1476,7 @@ def test_stored_credential_v2_installment_first ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :installment, :initial) }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :installment, :initial) }) end def test_stored_credential_v2_follow_on_installment_first @@ -1449,7 +1492,7 @@ def test_stored_credential_v2_follow_on_installment_first ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :installment, id: '123ABC') }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :installment, id: '123ABC') }) end def test_stored_credential_v2_unscheduled_cit_initial @@ -1464,7 +1507,7 @@ def test_stored_credential_v2_unscheduled_cit_initial ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:cardholder, :unscheduled, :initial) }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:cardholder, :unscheduled, :initial) }) end def test_stored_credential_v2_unscheduled_mit_initial @@ -1479,7 +1522,7 @@ def test_stored_credential_v2_unscheduled_mit_initial ) ).returns(braintree_result) - @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credentials_v2: true, stored_credential: stored_credential(:merchant, :unscheduled, :initial) }) + @gateway.purchase(100, credit_card('41111111111111111111'), { test: true, order_id: '1', stored_credential: stored_credential(:merchant, :unscheduled, :initial) }) end def test_raises_exeption_when_adding_bank_account_to_customer_without_billing_address @@ -1526,6 +1569,10 @@ def test_scrub_sensitive_data assert_equal filtered_success_token_nonce, @gateway.scrub(success_create_token_nonce) end + def test_transcript_scrubbing_network_token + assert_equal @gateway.scrub(pre_scrub_network_token), post_scrub_network_token + end + def test_setup_purchase Braintree::ClientTokenGateway.any_instance.expects(:generate).with do |params| (params[:merchant_account_id] == 'merchant_account_id') @@ -1709,4 +1756,674 @@ def filtered_success_token_nonce [Braintree] RESPONSE end + + def pre_scrub_network_token + <<-RESPONSE + [Braintree] + [Braintree] 47.70 + [Braintree] 111111 + [Braintree] + [Braintree] + [Braintree] test_transaction@gmail.com + [Braintree] 123341 + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] + [Braintree] false + [Braintree] true + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] 111111 + [Braintree] 11111122233 + [Braintree] checkout-flow + [Braintree] 0 + [Braintree] + [Braintree] Account-12344 + [Braintree] + [Braintree] 41111111111111 + [Braintree] 02 + [Braintree] 2028 + [Braintree] John Smith + [Braintree] + [Braintree] /wBBBBBBBPZWYOv4AmbmrruuUDDDD= + [Braintree] 07 + [Braintree] + [Braintree] + [Braintree] + [Braintree] vaulted + [Braintree] 312343241232 + [Braintree] + [Braintree] recurring + [Braintree] + [Braintree] 251 Test STree + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] CHANNEL_BT + [Braintree] sale + [Braintree] + + I, [2024-08-16T16:36:13.440224 #2217917] INFO -- : [Braintree] [16/Aug/2024 16:36:13 UTC] POST /merchants/js7myvkvrjt5khpb/transactions 201 + D, [2024-08-16T16:36:13.440275 #2217917] DEBUG -- : [Braintree] [16/Aug/2024 16:36:13 UTC] 201 + D, [2024-08-16T16:36:13.440973 #2217917] DEBUG -- : [Braintree] + [Braintree] + [Braintree] ftq5rn1j + [Braintree] submitted_for_settlement + [Braintree] sale + [Braintree] USD + [Braintree] 47.70 + [Braintree] 47.70 + [Braintree] CHANNEL + [Braintree] + [Braintree] + [Braintree] 114475310 + [Braintree] 2024-08-16T16:36:12Z + [Braintree] 2024-08-16T16:36:13Z + [Braintree] + [Braintree] + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] test_email@gmail.com + [Braintree] + [Braintree] 8765432432 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 5773 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Anna Smith + [Braintree] CA + [Braintree] 32343 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455 + [Braintree] 12356432 + [Braintree] tbyb-second + [Braintree] 0 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] M + [Braintree] M + [Braintree] I + [Braintree] + [Braintree] 796973 + [Braintree] 1000 + [Braintree] Approved + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] https://assets.braintreegateway.com/payment_method_logo/unknown.png?environment=production + [Braintree] false + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 41111 + [Braintree] 111 + [Braintree] Visa + [Braintree] 02 + [Braintree] 2028 + [Braintree] US + [Braintree] John Smith + [Braintree] https://assets.braintreegateway.com/paymenn + [Braintree] true + [Braintree] No + [Braintree] No + [Braintree] Yes + [Braintree] Yes + [Braintree] Unknown + [Braintree] No + [Braintree] Test Bank Account + [Braintree] USA + [Braintree] F + [Braintree] + [Braintree] credit + [Braintree] + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] authorized + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] submitted_for_settlement + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] true + [Braintree] CHANNEL_BT + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] network_token + [Braintree] + [Braintree] + [Braintree] 00 + [Braintree] Successful approval/completion or V.I.P. PIN verification is successful + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455667786 + [Braintree] approved + [Braintree] 2024-08-17T16:36:13Z + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] ddetwte3DG43GDR + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 112233445566 + [Braintree] + [Braintree] CHANNEL_MERCHANT + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-453-46223 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] fqq5tm1j + [Braintree] dHJhbnNhY3RpE3Gppse33o + [Braintree] 47.70 + [Braintree] USD + [Braintree] 1000 + [Braintree] Approved + [Braintree] 755332 + [Braintree] TEST-STORE + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-733-45235 + [Braintree] + [Braintree] 122334553 + [Braintree] + [Braintree] sale + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + RESPONSE + end + + def post_scrub_network_token + <<-RESPONSE + [Braintree] + [Braintree] 47.70 + [Braintree] 111111 + [Braintree] + [Braintree] + [Braintree] test_transaction@gmail.com + [Braintree] 123341 + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] + [Braintree] false + [Braintree] true + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] 111111 + [Braintree] 11111122233 + [Braintree] checkout-flow + [Braintree] 0 + [Braintree] + [Braintree] Account-12344 + [Braintree] + [Braintree] [FILTERED] + [Braintree] 02 + [Braintree] 2028 + [Braintree] John Smith + [Braintree] + [Braintree] [FILTERED] + [Braintree] 07 + [Braintree] + [Braintree] + [Braintree] + [Braintree] vaulted + [Braintree] 312343241232 + [Braintree] + [Braintree] recurring + [Braintree] + [Braintree] 251 Test STree + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 57753 + [Braintree] US + [Braintree] USA + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] CHANNEL_BT + [Braintree] sale + [Braintree] + + I, [2024-08-16T16:36:13.440224 #2217917] INFO -- : [Braintree] [16/Aug/2024 16:36:13 UTC] POST /merchants/js7myvkvrjt5khpb/transactions 201 + D, [2024-08-16T16:36:13.440275 #2217917] DEBUG -- : [Braintree] [16/Aug/2024 16:36:13 UTC] 201 + D, [2024-08-16T16:36:13.440973 #2217917] DEBUG -- : [Braintree] + [Braintree] + [Braintree] ftq5rn1j + [Braintree] submitted_for_settlement + [Braintree] sale + [Braintree] USD + [Braintree] 47.70 + [Braintree] 47.70 + [Braintree] CHANNEL + [Braintree] + [Braintree] + [Braintree] 114475310 + [Braintree] 2024-08-16T16:36:12Z + [Braintree] 2024-08-16T16:36:13Z + [Braintree] + [Braintree] + [Braintree] John + [Braintree] Smith + [Braintree] + [Braintree] test_email@gmail.com + [Braintree] + [Braintree] 8765432432 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Los Angeles + [Braintree] CA + [Braintree] 5773 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 251 Test Street + [Braintree] + [Braintree] Anna Smith + [Braintree] CA + [Braintree] 32343 + [Braintree] United States of America + [Braintree] US + [Braintree] USA + [Braintree] 840 + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455 + [Braintree] 12356432 + [Braintree] tbyb-second + [Braintree] 0 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] M + [Braintree] M + [Braintree] I + [Braintree] + [Braintree] 796973 + [Braintree] 1000 + [Braintree] Approved + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] true + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] https://assets.braintreegateway.com/payment_method_logo/unknown.png?environment=production + [Braintree] false + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] Unknown + [Braintree] + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 41111 + [Braintree] 111 + [Braintree] Visa + [Braintree] 02 + [Braintree] 2028 + [Braintree] US + [Braintree] John Smith + [Braintree] https://assets.braintreegateway.com/paymenn + [Braintree] true + [Braintree] No + [Braintree] No + [Braintree] Yes + [Braintree] Yes + [Braintree] Unknown + [Braintree] No + [Braintree] Test Bank Account + [Braintree] USA + [Braintree] F + [Braintree] + [Braintree] credit + [Braintree] + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] authorized + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] 2024-08-16T16:36:13Z + [Braintree] submitted_for_settlement + [Braintree] 47.70 + [Braintree] testemail@gmail.com + [Braintree] api + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] true + [Braintree] CHANNEL_BT + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] network_token + [Braintree] + [Braintree] + [Braintree] 00 + [Braintree] Successful approval/completion or V.I.P. PIN verification is successful + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 1122334455667786 + [Braintree] approved + [Braintree] 2024-08-17T16:36:13Z + [Braintree] + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] ddetwte3DG43GDR + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] 112233445566 + [Braintree] + [Braintree] CHANNEL_MERCHANT + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-453-46223 + [Braintree] + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] fqq5tm1j + [Braintree] dHJhbnNhY3RpE3Gppse33o + [Braintree] 47.70 + [Braintree] USD + [Braintree] 1000 + [Braintree] Approved + [Braintree] 755332 + [Braintree] TEST-STORE + [Braintree] + [Braintree] + [Braintree] New York + [Braintree] NY + [Braintree] 10012 + [Braintree] 551-733-45235 + [Braintree] + [Braintree] 122334553 + [Braintree] + [Braintree] sale + [Braintree] false + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + [Braintree] + RESPONSE + end end diff --git a/test/unit/gateways/cecabank_test.rb b/test/unit/gateways/cecabank_test.rb index e2bfe3c749e..b2218845379 100644 --- a/test/unit/gateways/cecabank_test.rb +++ b/test/unit/gateways/cecabank_test.rb @@ -37,7 +37,7 @@ def test_invalid_xml_response_handling assert_instance_of Response, response assert_failure response assert_match(/Unable to parse the response/, response.message) - assert_match(/No close tag for/, response.params['error_message']) + # assert_match(/No close tag for/, response.params['error_message']) end def test_expiration_date_sent_correctly diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index 1fcc42989e2..4b2af977630 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -1,5 +1,4 @@ require 'test_helper' - class CheckoutV2Test < Test::Unit::TestCase include CommStub @@ -86,6 +85,21 @@ def test_successful_passing_processing_channel_id end.respond_with(successful_purchase_response) end + def test_successful_passing_risk_data + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, { + risk: { + enabled: 'true', + device_session_id: '12345-abcd' + } + }) + end.check_request do |_method, _endpoint, data, _headers| + request = JSON.parse(data)['risk'] + assert_equal request['enabled'], true + assert_equal request['device_session_id'], '12345-abcd' + end.respond_with(successful_purchase_response) + end + def test_successful_passing_incremental_authorization response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card, { incremental_authorization: 'abcd1234' }) @@ -416,23 +430,13 @@ def test_failed_purchase assert_equal Gateway::STANDARD_ERROR_CODE[:invalid_number], response.error_code end - def test_failed_purchase_3ds_with_threeds_response_message - response = stub_comms(@gateway, :ssl_request) do - @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing', threeds_response_message: true }) - end.respond_with(failed_purchase_3ds_response) - - assert_failure response - assert_equal 'Insufficient Funds', response.message - assert_equal nil, response.error_code - end - - def test_failed_purchase_3ds_without_threeds_response_message + def test_failed_purchase_3ds response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, { execute_threed: true, exemption: 'no_preference', challenge_indicator: 'trusted_listing' }) end.respond_with(failed_purchase_3ds_response) assert_failure response - assert_equal 'Declined', response.message + assert_equal 'Insufficient Funds', response.message assert_equal nil, response.error_code end @@ -488,7 +492,7 @@ def test_successful_purchase_with_stored_credentials } @gateway.purchase(@amount, @credit_card, initial_options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(%r{"payment_type":"Recurring"}, data) + assert_match(%r{"payment_type":"Installment"}, data) assert_match(%r{"merchant_initiated":false}, data) end.respond_with(successful_purchase_initial_stored_credential_response) diff --git a/test/unit/gateways/credorax_test.rb b/test/unit/gateways/credorax_test.rb index 625953f57f0..3fe4084588d 100644 --- a/test/unit/gateways/credorax_test.rb +++ b/test/unit/gateways/credorax_test.rb @@ -505,7 +505,7 @@ def test_adds_3ds2_fields_via_normalized_hash @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) end.check_request do |_endpoint, data, _headers| assert_match(/i8=#{eci}%3A#{cavv}%3Anone/, data) - assert_match(/3ds_version=2.0/, data) + assert_match(/3ds_version=2/, data) assert_match(/3ds_dstrxid=#{ds_transaction_id}/, data) end.respond_with(successful_purchase_response) end @@ -526,7 +526,7 @@ def test_adds_default_cavv_when_omitted_from_normalized_hash @gateway.purchase(@amount, @credit_card, options_with_normalized_3ds) end.check_request do |_endpoint, data, _headers| assert_match(/i8=#{eci}%3Anone%3Anone/, data) - assert_match(/3ds_version=2.0/, data) + assert_match(/3ds_version=2.2.0/, data) assert_match(/3ds_dstrxid=#{ds_transaction_id}/, data) end.respond_with(successful_purchase_response) end diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb index 8105b78c8ec..b5f1b4fbdf6 100644 --- a/test/unit/gateways/cyber_source_rest_test.rb +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -17,6 +17,7 @@ def setup year: 2031 ) @master_card = credit_card('2222420000001113', brand: 'master') + @carnet_card = credit_card('5062280000000000', brand: 'carnet') @visa_network_token = network_tokenization_credit_card( '4111111111111111', @@ -76,6 +77,7 @@ def setup }, email: 'test@cybs.com' } + @discover_card = credit_card('6011111111111117', brand: 'discover') @gmt_time = Time.now.httpdate @digest = 'SHA-256=gXWufV4Zc7VkN9Wkv9jh/JuAVclqDusx3vkyo3uJFWU=' @resource = '/pts/v2/payments/' @@ -218,6 +220,51 @@ def test_authorize_network_token_visa end.respond_with(successful_purchase_response) end + def test_authorize_network_token_visa_recurring + @options[:stored_credential] = stored_credential(:cardholder, :recurring) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'recurring', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_visa_installment + @options[:stored_credential] = stored_credential(:cardholder, :installment) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'install', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_visa_unscheduled + @options[:stored_credential] = stored_credential(:cardholder, :unscheduled) + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + def test_authorize_network_token_mastercard stub_comms do @gateway.authorize(100, @mastercard_network_token, @options) @@ -302,7 +349,6 @@ def test_stored_credential_recurring_cit request = JSON.parse(data) assert_equal 'recurring', request['processingInformation']['commerceIndicator'] assert_equal 'customer', request.dig('processingInformation', 'authorizationOptions', 'initiator', 'type') - assert_equal true, request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'storedCredentialUsed') end.respond_with(successful_purchase_response) assert_success response @@ -316,7 +362,8 @@ def test_stored_credential_recurring_mit_ntid request = JSON.parse(data) assert_equal 'recurring', request['processingInformation']['commerceIndicator'] assert_equal 'merchant', request.dig('processingInformation', 'authorizationOptions', 'initiator', 'type') - assert_equal true, request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'storedCredentialUsed') + assert_equal true, request.dig('processingInformation', 'authorizationOptions', 'initiator', 'storedCredentialUsed') + assert_nil request.dig('processingInformation', 'authorizationOptions', 'initiator', 'merchantInitiatedTransaction', 'originalAuthorizedAmount') end.respond_with(successful_purchase_response) assert_success response @@ -491,6 +538,64 @@ def test_adds_application_id_as_partner_solution_id CyberSourceRestGateway.application_id = nil end + def test_purchase_with_level_2_data + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge({ purchase_order_number: '13829012412' })) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '13829012412', request['orderInformation']['invoiceDetails']['purchaseOrderNumber'] + end.respond_with(successful_purchase_response) + end + + def test_purchase_with_level_3_data + options = { + purchase_order_number: '6789', + discount_amount: '150', + ships_from_postal_code: '90210', + line_items: [ + { + productName: 'Product Name', + kind: 'debit', + quantity: 10, + unitPrice: '9.5000', + totalAmount: '95.00', + taxAmount: '5.00', + discountAmount: '0.00', + productCode: '54321', + commodityCode: '98765' + }, + { + productName: 'Other Product Name', + kind: 'debit', + quantity: 1, + unitPrice: '2.5000', + totalAmount: '90.00', + taxAmount: '2.00', + discountAmount: '1.00', + productCode: '54322', + commodityCode: '98766' + } + ] + } + stub_comms do + @gateway.authorize(100, @credit_card, @options.merge(options)) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '3', request['processingInformation']['purchaseLevel'] + assert_equal '150', request['orderInformation']['amountDetails']['discountAmount'] + assert_equal '90210', request['orderInformation']['shipping_details']['shipFromPostalCode'] + end.respond_with(successful_purchase_response) + end + + def test_accurate_card_type_and_code_for_carnet + stub_comms do + @gateway.purchase(100, @carnet_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '002', request['paymentInformation']['card']['type'] + end.respond_with(successful_purchase_response) + end + private def parse_signature(signature) diff --git a/test/unit/gateways/cyber_source_test.rb b/test/unit/gateways/cyber_source_test.rb index 4b6fb1c914a..a54be77a4a5 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -18,6 +18,7 @@ def setup @master_credit_card = credit_card('4111111111111111', brand: 'master') @elo_credit_card = credit_card('5067310000000010', brand: 'elo') @declined_card = credit_card('801111111111111', brand: 'visa') + @carnet_card = credit_card('5062280000000000', brand: 'carnet') @network_token = network_tokenization_credit_card('4111111111111111', brand: 'visa', transaction_id: '123', @@ -41,6 +42,12 @@ def setup eci: '05', payment_cryptogram: '111111111100cryptogram', source: :apple_pay) + @apple_pay_discover = network_tokenization_credit_card('6011111111111117', + brand: 'discover', + transaction_id: '123', + eci: '05', + payment_cryptogram: '111111111100cryptogram', + source: :apple_pay) @google_pay = network_tokenization_credit_card('4242424242424242', source: :google_pay) @check = check() @@ -248,11 +255,15 @@ def test_uses_names_from_the_payment_method def test_purchase_includes_invoice_header stub_comms do - @gateway.purchase(100, @credit_card, merchant_descriptor: 'Spreedly', reference_data_code: '3A', invoice_number: '1234567') + @gateway.purchase(100, @credit_card, merchant_descriptor: 'Spreedly', reference_data_code: '3A', invoice_number: '1234567', merchant_descriptor_city: 'test123', submerchant_id: 'AVSBSGDHJMNGFR', merchant_descriptor_country: 'US', merchant_descriptor_state: 'NY') end.check_request do |_endpoint, data, _headers| assert_match(/Spreedly<\/merchantDescriptor>/, data) assert_match(/3A<\/referenceDataCode>/, data) assert_match(/1234567<\/invoiceNumber>/, data) + assert_match(/test123<\/merchantDescriptorCity>/, data) + assert_match(/AVSBSGDHJMNGFR<\/submerchantID>/, data) + assert_match(/US<\/merchantDescriptorCountry>/, data) + assert_match(/NY<\/merchantDescriptorState>/, data) end.respond_with(successful_purchase_response) end @@ -558,6 +569,19 @@ def test_successful_apple_pay_purchase_subsequent_auth_mastercard assert_success response end + def test_successful_apple_pay_purchase_subsequent_auth_discover + @gateway.expects(:ssl_post).with do |_host, request_body| + assert_match %r'', request_body + assert_match %r'internet', request_body + true + end.returns(successful_purchase_response) + + options = @options.merge(enable_cybs_discover_apple_pay: true) + + assert response = @gateway.purchase(@amount, @apple_pay_discover, options) + assert_success response + end + def test_successful_reference_purchase @gateway.stubs(:ssl_post).returns(successful_create_subscription_response, successful_purchase_response) @@ -1112,7 +1136,7 @@ def test_cof_cit_auth response = stub_comms do @gateway.authorize(@amount, @credit_card, @options) end.check_request do |_endpoint, data, _headers| - assert_not_match(/\/, data) + assert_match(/\/, data) assert_match(/\/, data) assert_not_match(/\/, data) assert_not_match(/\/, data) @@ -1293,7 +1317,7 @@ def test_subsequent_cit_unscheduled_network_token assert_match(/\111111111100cryptogram/, data) assert_match(/\015/, data) assert_match(/\3/, data) - assert_not_match(/\true/, data) + assert_match(/\true/, data) assert_match(/\true/, data) assert_not_match(/\true/, data) assert_not_match(/\016150703802094/, data) @@ -1316,7 +1340,7 @@ def test_cit_installment_network_token assert_match(/\015/, data) assert_match(/\3/, data) assert_match(/\true/, data) - assert_match(/\internet/, data) + assert_match(/\install/, data) assert_not_match(/\/, data) assert_not_match(/\true/, data) assert_not_match(/\016150703802094/, data) @@ -1342,7 +1366,7 @@ def test_mit_installment_network_token assert_not_match(/\true/, data) assert_match(/\true/, data) assert_match(/\016150703802094/, data) - assert_match(/\internet/, data) + assert_match(/\install/, data) end.respond_with(successful_authorization_response) assert response.success? end @@ -1361,11 +1385,11 @@ def test_subsequent_cit_installment_network_token assert_match(/\111111111100cryptogram/, data) assert_match(/\015/, data) assert_match(/\3/, data) - assert_not_match(/\/, data) + assert_match(/\/, data) assert_match(/\true/, data) assert_not_match(/\true/, data) assert_not_match(/\016150703802094/, data) - assert_match(/\internet/, data) + assert_match(/\install/, data) end.respond_with(successful_authorization_response) assert response.success? end @@ -1384,7 +1408,7 @@ def test_cit_recurring_network_token assert_match(/\015/, data) assert_match(/\3/, data) assert_match(/\true/, data) - assert_match(/\internet/, data) + assert_match(/\recurring/, data) assert_not_match(/\/, data) assert_not_match(/\true/, data) assert_not_match(/\016150703802094/, data) @@ -1410,7 +1434,7 @@ def test_mit_recurring_network_token assert_not_match(/\true/, data) assert_match(/\true/, data) assert_match(/\016150703802094/, data) - assert_match(/\internet/, data) + assert_match(/\recurring/, data) end.respond_with(successful_authorization_response) assert response.success? end @@ -1429,11 +1453,11 @@ def test_subsequent_cit_recurring_network_token assert_match(/\111111111100cryptogram/, data) assert_match(/\015/, data) assert_match(/\3/, data) - assert_not_match(/\/, data) + assert_match(/\/, data) assert_match(/\true/, data) assert_not_match(/\true/, data) assert_not_match(/\016150703802094/, data) - assert_match(/\internet/, data) + assert_match(/\recurring/, data) end.respond_with(successful_authorization_response) assert response.success? end @@ -1989,6 +2013,14 @@ def test_routing_number_formatting_with_canadian_routing_number_and_padding assert_equal @gateway.send(:format_routing_number, '012345678', { currency: 'CAD' }), '12345678' end + def test_accurate_card_type_and_code_for_carnet + stub_comms do + @gateway.purchase(100, @carnet_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/002<\/cardType>/, data) + end.respond_with(successful_purchase_response) + end + private def options_with_normalized_3ds( diff --git a/test/unit/gateways/d_local_test.rb b/test/unit/gateways/d_local_test.rb index a1bf4c354ff..3d48225d102 100644 --- a/test/unit/gateways/d_local_test.rb +++ b/test/unit/gateways/d_local_test.rb @@ -44,6 +44,15 @@ def test_purchase_with_save end.respond_with(successful_purchase_response) end + def test_purchase_with_ip_and_phone + stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(ip: '127.0.0.1')) + end.check_request do |_endpoint, data, _headers| + assert_equal '127.0.0.1', JSON.parse(data)['payer']['ip'] + assert_equal '(555)555-5555', JSON.parse(data)['payer']['phone'] + end.respond_with(successful_purchase_response) + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb index 532cea6b645..0c249bf6777 100644 --- a/test/unit/gateways/datatrans_test.rb +++ b/test/unit/gateways/datatrans_test.rb @@ -27,9 +27,10 @@ def setup } }) - @transaction_reference = '240214093712238757|093712' + @transaction_reference = '240214093712238757|093712|123alias_token_id123|05|25' @billing_address = address + @no_country_billing_address = address(country: nil) @nt_credit_card = network_tokenization_credit_card( '4111111111111111', @@ -84,6 +85,19 @@ def test_authorize_with_credit_card_and_billing_address assert_success response end + def test_authorize_with_credit_card_and_no_country_billing_address + response = stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options.merge({ billing_address: @no_country_billing_address })) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + billing = parsed_data['billing'] + assert_nil billing['country'] + end.respond_with(successful_authorize_response) + + assert_success response + end + def test_purchase_with_credit_card response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) @@ -98,6 +112,17 @@ def test_purchase_with_credit_card assert_success response end + def test_verify_with_credit_card + response = stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) unless parsed_data.empty? + end.respond_with(successful_authorize_response, successful_void_response) + + assert_success response + end + def test_purchase_with_network_token response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @nt_credit_card, @options) @@ -194,6 +219,43 @@ def test_voids assert_success response end + def test_store + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.check_request do |_action, endpoint, data, _headers| + assert_match('aliases/tokenize', endpoint) + parsed_data = JSON.parse(data) + request = parsed_data['requests'][0] + assert_equal('CARD', request['type']) + assert_equal(@credit_card.number, request['pan']) + end.respond_with(successful_store_response) + + assert_success response + end + + def test_unstore + response = stub_comms(@gateway, :ssl_request) do + @gateway.unstore(@transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + assert_match('aliases/123alias_token_id123', endpoint) + assert_equal data, '{}' + end.respond_with(successful_unstore_response) + + assert_success response + end + + def test_purchase_with_tpv + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @transaction_reference, @options) + end.check_request do |_action, endpoint, data, _headers| + parsed_data = JSON.parse(data) + common_assertions_authorize_purchase(endpoint, parsed_data) + assert_equal(@transaction_reference.split('|')[2], parsed_data['card']['alias']) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_required_merchant_id_and_password error = assert_raises ArgumentError do DatatransGateway.new @@ -249,7 +311,7 @@ def test_get_response_message_from_message_user def test_url_generation_from_action action = 'test' - assert_equal "#{@gateway.test_url}#{action}", @gateway.send(:url, action) + assert_equal "#{@gateway.test_url}transactions/#{action}", @gateway.send(:url, action) end def test_scrub @@ -258,10 +320,14 @@ def test_scrub end def test_authorization_from - assert_equal '1234|9248', @gateway.send(:authorization_from, { 'transactionId' => '1234', 'acquirerAuthorizationCode' => '9248' }) - assert_equal '1234|', @gateway.send(:authorization_from, { 'transactionId' => '1234' }) - assert_equal '|9248', @gateway.send(:authorization_from, { 'acquirerAuthorizationCode' => '9248' }) - assert_equal nil, @gateway.send(:authorization_from, {}) + assert_equal '1234|9248|', @gateway.send(:authorization_from, { 'transactionId' => '1234', 'acquirerAuthorizationCode' => '9248' }, '', {}) + assert_equal '1234||', @gateway.send(:authorization_from, { 'transactionId' => '1234' }, '', {}) + assert_equal '|9248|', @gateway.send(:authorization_from, { 'acquirerAuthorizationCode' => '9248' }, '', {}) + assert_equal nil, @gateway.send(:authorization_from, {}, '', {}) + # tes for store + assert_equal '||any_alias|any_month|any_year', @gateway.send(:authorization_from, { 'responses' => [{ 'alias' => 'any_alias' }] }, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' }) + # handle nil responses or missing keys + assert_equal '|||any_month|any_year', @gateway.send(:authorization_from, {}, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' }) end def test_parse @@ -289,6 +355,19 @@ def successful_capture_response '{"response_code": 204}' end + def successful_store_response + '{ + "overview":{"total":1, "successful":1, "failed":0}, + "responses": + [{ + "type":"CARD", + "alias":"7LHXscqwAAEAAAGQvYQBwc5zIs52AGRs", + "maskedCC":"424242xxxxxx4242", + "fingerprint":"F-dSjBoCMOYxomP49vzhdOYE" + }] + }' + end + def common_assertions_authorize_purchase(endpoint, parsed_data) assert_match('authorize', endpoint) assert_equal(@options[:order_id], parsed_data['refno']) @@ -299,6 +378,7 @@ def common_assertions_authorize_purchase(endpoint, parsed_data) alias successful_purchase_response successful_authorize_response alias successful_refund_response successful_authorize_response alias successful_void_response successful_capture_response + alias successful_unstore_response successful_capture_response def pre_scrubbed <<~PRE_SCRUBBED diff --git a/test/unit/gateways/decidir_plus_test.rb b/test/unit/gateways/decidir_plus_test.rb index 26a783a2984..0d05c967e7b 100644 --- a/test/unit/gateways/decidir_plus_test.rb +++ b/test/unit/gateways/decidir_plus_test.rb @@ -77,6 +77,15 @@ def test_successful_capture end.respond_with(successful_purchase_response) end + def test_failed_refund_for_validation_errors + response = stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '12420186') + end.respond_with(failed_credit_message_response) + + assert_failure response + assert_equal('invalid_status_error - status: refunded', response.message) + end + def test_failed_authorize response = stub_comms(@gateway, :ssl_request) do @gateway.authorize(@amount, @credit_card, @options) @@ -336,6 +345,12 @@ def failed_purchase_message_response } end + def failed_credit_message_response + %{ + {\"error_type\":\"invalid_status_error\",\"validation_errors\":{\"status\":\"refunded\"}} + } + end + def successful_refund_response %{ {\"id\":417921,\"amount\":100,\"sub_payments\":null,\"error\":null,\"status\":\"approved\",\"status_details\":{\"ticket\":\"4589\",\"card_authorization_code\":\"173711\",\"address_validation_code\":\"VTE0011\",\"error\":null}} diff --git a/test/unit/gateways/decidir_test.rb b/test/unit/gateways/decidir_test.rb index be2c78a3f96..3f3e5e693dc 100644 --- a/test/unit/gateways/decidir_test.rb +++ b/test/unit/gateways/decidir_test.rb @@ -2,6 +2,7 @@ class DecidirTest < Test::Unit::TestCase include CommStub + include ActiveMerchant::Billing::CreditCardFormatting def setup @gateway_for_purchase = DecidirGateway.new(api_key: 'api_key') @@ -43,7 +44,8 @@ def setup '4012001037141112', brand: 'visa', eci: '05', - payment_cryptogram: '000203016912340000000FA08400317500000000' + payment_cryptogram: '000203016912340000000FA08400317500000000', + verification_value: '123' ) end @@ -407,8 +409,36 @@ def test_network_token_payment_method card_holder_identification_number: '44444444', last_4: @credit_card.last_digits } - @gateway_for_auth.expects(:ssl_request).returns(successful_network_token_response) - response = @gateway_for_auth.authorize(100, @network_token, options) + + response = stub_comms(@gateway_for_auth, :ssl_request) do + @gateway_for_auth.authorize(100, @network_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cryptogram\":\"#{@network_token.payment_cryptogram}\"/, data) + assert_match(/"security_code\":\"#{@network_token.verification_value}\"/, data) + assert_match(/"expiration_month\":\"#{format(@network_token.month, :two_digits)}\"/, data) + assert_match(/"expiration_year\":\"#{format(@network_token.year, :two_digits)}\"/, data) + end.respond_with(successful_network_token_response) + + assert_success response + assert_equal 49120515, response.authorization + end + + def test_network_token_payment_method_without_cvv + options = { + card_holder_name: 'Tesest payway', + card_holder_door_number: 1234, + card_holder_birthday: '200988', + card_holder_identification_type: 'DNI', + card_holder_identification_number: '44444444', + last_4: @credit_card.last_digits + } + @network_token.verification_value = nil + response = stub_comms(@gateway_for_auth, :ssl_request) do + @gateway_for_auth.authorize(100, @network_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"cryptogram\":\"#{@network_token.payment_cryptogram}\"/, data) + assert_not_match(/"security_code\":\"#{@network_token.verification_value}\"/, data) + end.respond_with(successful_network_token_response) assert_success response assert_equal 49120515, response.authorization @@ -419,6 +449,10 @@ def test_scrub assert_equal @gateway_for_purchase.scrub(pre_scrubbed), post_scrubbed end + def test_transcript_scrubbing_network_token + assert_equal @gateway_for_purchase.scrub(pre_scrubbed_network_token), post_scrubbed_network_token + end + def test_payment_method_id_with_visa post = {} @gateway_for_purchase.send(:add_auth_purchase_params, post, @amount, @credit_card, @options) @@ -556,6 +590,64 @@ def post_scrubbed ) end + def pre_scrubbed_network_token + %( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /api/v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nApikey: 5df6b5764c3f4822aecdc82d56f26b9d\\r\\nCache-Control: no-cache\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nHost: developers.decidir.com\\r\\nContent-Length: 505\\r\\n\\r\\n\" + <- "{\\\"payment_method_id\\\":1,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"bin\\\":\\\"401200\\\",\\\"payment_type\\\":\\\"single\\\",\\\"installments\\\":1,\\\"description\\\":\\\"Store Purchase\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ARS\\\",\\\"card_data\\\":{\\\"card_holder_identification\\\":{},\\\"card_holder_name\\\":\\\"Tesest payway\\\",\\\"last_four_digits\\\":null},\\\"is_tokenized_payment\\\":true,\\\"fraud_detection\\\":{\\\"sent_to_cs\\\":false},\\\"token_card_data\\\":{\\\"expiration_month\\\":\\\"09\\\",\\\"expiration_year\\\":\\\"25\\\",\\\"token\\\":\\\"4012001037141112\\\",\\\"eci\\\":\\\"05\\\",\\\"cryptogram\\\":\\\"/wBBBBBCd4HzpGYAmbmgguoBBBB=\\\"},\\\"sub_payments\\\":[]}\" + -> "HTTP/1.1 402 Payment Required\\r\\n\" + -> "Content-Type: application/json; charset=utf-8\\r\\n\" + -> "Content-Length: 826\\r\\n\" + -> "Connection: close\\r\\n\" + -> "date: Wed, 21 Aug 2024 16:35:34 GMT\\r\\n\" + -> "ETag: W/\\\"33a-JHilnlQgDvDXNEdqUzzsVialMcw\\\"\\r\\n\" + -> "vary: Origin\\r\\n\" + -> "Access-Control-Allow-Origin: *\\r\\n\" + -> "Access-Control-Expose-Headers: Accept,Accept-Version,Content-Length,Content-MD5,Content-Type,Date,X-Auth-Token,Access-Control-Allow-Origin,apikey,Set-Cookie,x-consumer-username\\r\\n\" + -> "X-Kong-Upstream-Latency: 325\\r\\n\" + -> "X-Kong-Proxy-Latency: 1\\r\\n\" + -> "Via: kong/2.0.5\\r\\n\" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\\r\\n\" + -> "Set-Cookie: TS017a11a6=012e46d8ee27033640500a291b59a9176ef91d5ef14fa722c67ee9909e85848e261382cc63bbfa0cb5d092944db41533293bbb0e26; Path=/; Domain=.developers.decidir.com\\r\\n\" + -> "\\r\\n\"\nreading 826 bytes... + -> "{\\\"id\\\":1945684101,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"payment_method_id\\\":1,\\\"card_brand\\\":\\\"Visa\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ars\\\",\\\"status\\\":\\\"rejected\\\",\\\"status_details\\\":{\\\"ticket\\\":\\\"4922\\\",\\\"card_authorization_code\\\":\\\"\\\",\\\"address_validation_code\\\":\\\"VTE2222\\\",\\\"error\\\":{\\\"type\\\":\\\"insufficient_amount\\\",\\\"reason\\\":{\\\"id\\\":13,\\\"description\\\":\\\"MONTO INVALIDO\\\",\\\"additional_description\\\":\\\"\\\"}}},\\\"date\\\":\\\"2024-08-21T13:35Z\\\",\\\"payment_mode\\\":null,\\\"customer\\\":null,\\\"bin\\\":\\\"401200\\\",\\\"installments\\\":1,\\\"first_installment_expiration_date\\\":null,\\\"payment_type\\\":\\\"single\\\",\\\"sub_payments\\\":[],\\\"site_id\\\":\\\"99999999\\\",\\\"fraud_detection\\\":null,\\\"aggregate_data\\\":null,\\\"establishment_name\\\":null,\\\"spv\\\":null,\\\"confirmed\\\":null,\\\"pan\\\":null,\\\"customer_token\\\":null,\\\"card_data\\\":\\\"/tokens/1945684101\\\",\\\"token\\\":\\\"4a08b19a-fbe2-45b2-8ef6-f3f12d4aa6ed\\\",\\\"authenticated_token\\\":false}\" + read 826 bytes + Conn close + ) + end + + def post_scrubbed_network_token + %( + opening connection to developers.decidir.com:443... + opened + starting SSL for developers.decidir.com:443... + SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384 + <- "POST /api/v2/payments HTTP/1.1\\r\\nContent-Type: application/json\\r\\nApikey: [FILTERED]\\r\\nCache-Control: no-cache\\r\\nConnection: close\\r\\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\\r\\nAccept: */*\\r\\nUser-Agent: Ruby\\r\\nHost: developers.decidir.com\\r\\nContent-Length: 505\\r\\n\\r\\n\" + <- "{\\\"payment_method_id\\\":1,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"bin\\\":\\\"401200\\\",\\\"payment_type\\\":\\\"single\\\",\\\"installments\\\":1,\\\"description\\\":\\\"Store Purchase\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ARS\\\",\\\"card_data\\\":{\\\"card_holder_identification\\\":{},\\\"card_holder_name\\\":\\\"Tesest payway\\\",\\\"last_four_digits\\\":null},\\\"is_tokenized_payment\\\":true,\\\"fraud_detection\\\":{\\\"sent_to_cs\\\":false},\\\"token_card_data\\\":{\\\"expiration_month\\\":\\\"09\\\",\\\"expiration_year\\\":\\\"25\\\",\\\"token\\\":\\\"[FILTERED]\\\",\\\"eci\\\":\\\"05\\\",\\\"cryptogram\\\":\\\"/[FILTERED]=\\\"},\\\"sub_payments\\\":[]}\" + -> "HTTP/1.1 402 Payment Required\\r\\n\" + -> "Content-Type: application/json; charset=utf-8\\r\\n\" + -> "Content-Length: 826\\r\\n\" + -> "Connection: close\\r\\n\" + -> "date: Wed, 21 Aug 2024 16:35:34 GMT\\r\\n\" + -> "ETag: W/\\\"33a-JHilnlQgDvDXNEdqUzzsVialMcw\\\"\\r\\n\" + -> "vary: Origin\\r\\n\" + -> "Access-Control-Allow-Origin: *\\r\\n\" + -> "Access-Control-Expose-Headers: Accept,Accept-Version,Content-Length,Content-MD5,Content-Type,Date,X-Auth-Token,Access-Control-Allow-Origin,apikey,Set-Cookie,x-consumer-username\\r\\n\" + -> "X-Kong-Upstream-Latency: 325\\r\\n\" + -> "X-Kong-Proxy-Latency: 1\\r\\n\" + -> "Via: kong/2.0.5\\r\\n\" + -> "Strict-Transport-Security: max-age=16070400; includeSubDomains\\r\\n\" + -> "Set-Cookie: TS017a11a6=012e46d8ee27033640500a291b59a9176ef91d5ef14fa722c67ee9909e85848e261382cc63bbfa0cb5d092944db41533293bbb0e26; Path=/; Domain=.developers.decidir.com\\r\\n\" + -> "\\r\\n\"\nreading 826 bytes... + -> "{\\\"id\\\":1945684101,\\\"site_transaction_id\\\":\\\"59239287-c211-4d72-97b0-70fd701126a6\\\",\\\"payment_method_id\\\":1,\\\"card_brand\\\":\\\"Visa\\\",\\\"amount\\\":100,\\\"currency\\\":\\\"ars\\\",\\\"status\\\":\\\"rejected\\\",\\\"status_details\\\":{\\\"ticket\\\":\\\"4922\\\",\\\"card_authorization_code\\\":\\\"\\\",\\\"address_validation_code\\\":\\\"VTE2222\\\",\\\"error\\\":{\\\"type\\\":\\\"insufficient_amount\\\",\\\"reason\\\":{\\\"id\\\":13,\\\"description\\\":\\\"MONTO INVALIDO\\\",\\\"additional_description\\\":\\\"\\\"}}},\\\"date\\\":\\\"2024-08-21T13:35Z\\\",\\\"payment_mode\\\":null,\\\"customer\\\":null,\\\"bin\\\":\\\"401200\\\",\\\"installments\\\":1,\\\"first_installment_expiration_date\\\":null,\\\"payment_type\\\":\\\"single\\\",\\\"sub_payments\\\":[],\\\"site_id\\\":\\\"99999999\\\",\\\"fraud_detection\\\":null,\\\"aggregate_data\\\":null,\\\"establishment_name\\\":null,\\\"spv\\\":null,\\\"confirmed\\\":null,\\\"pan\\\":null,\\\"customer_token\\\":null,\\\"card_data\\\":\\\"/tokens/1945684101\\\",\\\"token\\\":\\\"4a08b19a-fbe2-45b2-8ef6-f3f12d4aa6ed\\\",\\\"authenticated_token\\\":false}\" + read 826 bytes + Conn close + ) + end + def successful_purchase_response %( {"id":7719132,"site_transaction_id":"ebcb2db7-7aab-4f33-a7d1-6617a5749fce","payment_method_id":1,"card_brand":"Visa","amount":100,"currency":"ars","status":"approved","status_details":{"ticket":"7156","card_authorization_code":"174838","address_validation_code":"VTE0011","error":null},"date":"2019-06-21T17:48Z","customer":null,"bin":"450799","installments":1,"establishment_name":"Heavenly Buffaloes","first_installment_expiration_date":null,"payment_type":"single","sub_payments":[],"site_id":"99999999","fraud_detection":{"status":null},"aggregate_data":null,"establishment_name":null,"spv":null,"confirmed":null,"pan":"345425f15b2c7c4584e0044357b6394d7e","customer_token":null,"card_data":"/tokens/7719132"} diff --git a/test/unit/gateways/ebanx_test.rb b/test/unit/gateways/ebanx_test.rb index 423a1c0f83f..f6b11253705 100644 --- a/test/unit/gateways/ebanx_test.rb +++ b/test/unit/gateways/ebanx_test.rb @@ -46,6 +46,153 @@ def test_successful_purchase_with_soft_descriptor assert_success response end + def test_successful_purchase_with_stored_credentials_cardholder_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'recurring', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"SCHEDULED_RECURRING\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"CUSTOMER_COF\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: true, + initiator: 'cardholder', + reason_type: 'installment', + network_transaction_id: nil + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"initial\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"INSTALLMENT\"}, data + assert_not_match %r{"mandate_id\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_installment + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'installment', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"INSTALLMENT\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_unscheduled + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"MERCHANT_COF\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_merchant_recurring + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'merchant', + reason_type: 'recurring', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"MIT\"}, data + assert_match %r{"trans_type\":\"SCHEDULED_RECURRING\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_stored_credentials_cardholder_not_initial + options = @options.merge!({ + stored_credential: { + initial_transaction: false, + initiator: 'cardholder', + reason_type: 'unscheduled', + network_transaction_id: '1234' + } + }) + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match %r{"cof_type\":\"stored\"}, data + assert_match %r{"initiator\":\"CIT\"}, data + assert_match %r{"trans_type\":\"CUSTOMER_COF\"}, data + assert_match %r{"mandate_id\":\"1234\"}, data + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_failed_purchase @gateway.expects(:ssl_request).returns(failed_purchase_response) diff --git a/test/unit/gateways/elavon_test.rb b/test/unit/gateways/elavon_test.rb index da97607f95a..3fb341c87bc 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -26,18 +26,32 @@ def setup @credit_card = credit_card @amount = 100 + @stored_card = '4421912014039990' @options = { order_id: '1', billing_address: address, description: 'Store Purchase' } + + @google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :google_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) + + @apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + source: :apple_pay, + payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}" + }) end def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/123<\/ssl_cvv2cvc2>/, data) + end.respond_with(successful_purchase_response) - assert response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal '093840;180820AD3-27AEE6EF-8CA7-4811-8D1F-E420C3B5041E', response.authorization assert response.test? @@ -145,6 +159,22 @@ def test_successful_purchase_with_unscheduled end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_apple_pay + stub_comms do + @gateway.purchase(@amount, @apple_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_applepay_web>/, data) + end.respond_with(successful_purchase_response) + end + + def test_successful_purchase_with_google_pay + stub_comms do + @gateway.purchase(@amount, @google_pay, @options) + end.check_request do |_endpoint, data, _headers| + assert_match(/%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_google_pay>/, data) + end.respond_with(successful_purchase_response) + end + def test_sends_ssl_add_token_field response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(add_recurring_token: 'Y')) @@ -156,13 +186,23 @@ def test_sends_ssl_add_token_field end def test_sends_ssl_token_field - response = stub_comms do + purchase_response = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(ssl_token: '8675309')) end.check_request do |_endpoint, data, _headers| assert_match(/8675309<\/ssl_token>/, data) + refute_match(/8675309<\/ssl_token>/, data) + refute_match(/#{oar_data}<\/ssl_oar_data>/, data) assert_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) @@ -360,10 +400,265 @@ def test_oar_only_network_transaction_id ps2000_data = nil network_transaction_id = "#{oar_data}|#{ps2000_data}" stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { network_transaction_id: network_transaction_id })) + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring', network_transaction_id: network_transaction_id })) end.check_request do |_endpoint, data, _headers| assert_match(/#{oar_data}<\/ssl_oar_data>/, data) - refute_match(//, data) + refute_match(/123<\/ssl_cvv2cvc2>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/A7540295892588345510A<\/ssl_ps2000_data>/, data) + assert_match(/010012130901291622040000047554200000000000155836402916121309<\/ssl_oar_data>/, data) + assert_match(/1234566<\/ssl_approval_code>/, data) + assert_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/12<\/ssl_entry_mode>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + assert_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + refute_match(/1<\/ssl_recurring_flag>/, data) + refute_match(/2<\/ssl_recurring_flag>/, data) + assert_match(/2<\/ssl_payment_number>/, data) + assert_match(/4<\/ssl_payment_count>/, data) + refute_match(/Y<\/ssl_merchant_initiated_unscheduled>/, data) + assert_match(/1234567890<\/ssl_par_value>/, data) + assert_match(/1<\/ssl_association_token_data>/, data) + refute_match(//, data) + refute_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) end.respond_with(successful_purchase_response) end @@ -382,19 +677,19 @@ def test_ps2000_only_network_transaction_id def test_oar_transaction_id_without_pipe oar_data = '010012318808182231420000047554200000000000093840023122123188' stub_comms do - @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { network_transaction_id: oar_data })) + @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential: { initiator: 'merchant', reason_type: 'recurring', network_transaction_id: oar_data })) end.check_request do |_endpoint, data, _headers| assert_match(/#{oar_data}<\/ssl_oar_data>/, data) - refute_match(//, data) + refute_match(//, data) + refute_match(/#{ps2000_data}<\/ssl_ps2000_data>/, data) end.respond_with(successful_purchase_response) end diff --git a/test/unit/gateways/fat_zebra_test.rb b/test/unit/gateways/fat_zebra_test.rb index 3caf94852dc..dfb33d78355 100644 --- a/test/unit/gateways/fat_zebra_test.rb +++ b/test/unit/gateways/fat_zebra_test.rb @@ -25,6 +25,7 @@ def setup eci: '05', xid: 'ODUzNTYzOTcwODU5NzY3Qw==', enrolled: 'true', + ds_transaction_id: 'f25084f0-5b16-4c0a-ae5d-b24808a95e4b', authentication_response_status: 'Y' } end @@ -235,7 +236,7 @@ def test_three_ds_v2_object_construction assert_equal ds_options[:cavv], ds_data[:cavv] assert_equal ds_options[:eci], ds_data[:sli] assert_equal ds_options[:xid], ds_data[:xid] - assert_equal ds_options[:ds_transaction_id], ds_data[:ds_transaction_id] + assert_equal ds_options[:ds_transaction_id], ds_data[:directory_server_txn_id] assert_equal 'Y', ds_data[:ver] assert_equal ds_options[:authentication_response_status], ds_data[:par] end diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 752f03734c1..aed92d414d8 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -24,7 +24,10 @@ def setup cvv_result_code: '111', cavv_result_code: '111', timezone_utc_offset: '-5', - billing_address: address.merge(name: 'Cure Tester') + billing_address: address.merge(name: 'Cure Tester'), + shipping_address: address.merge(name: 'Jhon Doe', country: 'US'), + sense_key: 'abc123', + extra_data: { hello: 'world' }.to_json } @cit_options = { @@ -90,9 +93,9 @@ def test_invalid_instance end def test_successful_purchase - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) - end.check_request do |endpoint, data, headers| + end.check_request do |_method, endpoint, data, headers| request = JSON.parse(data) if /token/.match?(endpoint) assert_equal request['AppKey'], @gateway.options[:app_key] @@ -106,6 +109,9 @@ def test_successful_purchase assert_equal request['isDeclined'], @options[:is_declined] assert_equal request['orderId'], @options[:order_id] assert_equal request['idempotencyKey'], @options[:idempotency_key] + assert_equal request['senseKey'], 'abc123' + assert_equal request['Source'], 'Spreedly' + assert_equal request['ExtraData'], { hello: 'world' }.to_json assert_equal request['transaction']['timezoneUtcOffset'], @options[:timezone_utc_offset] assert_equal request['transaction']['amount'], @amount assert_equal request['transaction']['responseCode'], @options[:response_code] @@ -113,30 +119,45 @@ def test_successful_purchase assert_equal request['transaction']['avsResultCode'], @options[:avs_result_code] assert_equal request['transaction']['cvvResultCode'], @options[:cvv_result_code] assert_equal request['transaction']['cavvResultCode'], @options[:cavv_result_code] + assert_equal request['transactionType'], 'Purchase' assert_equal request['payer']['email'], @options[:email] assert_equal request['description'], @options[:description] + + assert_equal request['billingInformation']['firstName'], 'Cure' + assert_equal request['billingInformation']['country'], 'CA' + assert_equal request['shippingInformation']['firstName'], 'Jhon' + assert_equal request['shippingInformation']['country'], 'US' end end.respond_with(successful_access_token_response, successful_purchase_response) assert_success response - assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5', response.authorization + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5#USD', response.authorization assert response.test? end + def test_successful_authorization + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['transactionType'], 'Authorization' if /evaluate/.match?(endpoint) + end.respond_with(successful_access_token_response, successful_purchase_response) + end + def test_successful_purchase_three_ds_global - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @three_d_secure_options) end.respond_with(successful_access_token_response, successful_purchase_response) assert_success response - assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5', response.authorization + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5#USD', response.authorization assert response.test? end def test_succeful_request_with_three_ds_global - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @three_d_secure_options) - end.check_request do |endpoint, data, _headers| + end.check_request do |_method, endpoint, data, _headers| if /evaluate/.match?(endpoint) request = JSON.parse(data) assert_equal request['threeDSecure']['EcommerceIndicator'], @three_d_secure_options[:three_d_secure][:eci] @@ -153,7 +174,7 @@ def test_succeful_request_with_three_ds_global end def test_failed_purchase - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, @options) end.respond_with(successful_access_token_response, failed_purchase_response) @@ -162,10 +183,31 @@ def test_failed_purchase assert_equal '400', response.message end + def test_purchase_using_card_with_no_number + credit_card_with_no_number = credit_card + credit_card_with_no_number.number = nil + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, credit_card_with_no_number, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + end + + def test_successful_purchase_with_token + payment = 'bb114473-43fc-46c4-9082-ea3dfb490509' + + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, payment, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + end + def test_failed_refund - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, 'reference', @options) - end.check_request do |endpoint, data, _headers| + end.check_request do |_method, endpoint, data, _headers| request = JSON.parse(data) if /token/.match?(endpoint) @@ -180,27 +222,55 @@ def test_failed_refund assert response.test? end + def test_failed_purchase_idempotency_key + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, missed_idempotency_key_field) + + assert_failure response + assert_nil response.error_code + assert_equal '{"IdempotencyKey":["The IdempotencyKey field is required."]}', response.message + end + + def test_failed_purchase_expiry_date + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, invalid_expiry_date_utc) + + assert_failure response + assert_nil response.error_code + assert_equal '{"ExpiryDateUtc":["The field ExpiryDateUtc is invalid."]}', response.message + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end def test_address_names_from_address - names = @gateway.send(:address_names, @options[:billing_address][:name], @credit_card) + names = @gateway.send(:names_from_address, @options[:billing_address], @credit_card) assert_equal 'Cure', names.first assert_equal 'Tester', names.last end def test_address_names_from_credit_card - names = @gateway.send(:address_names, 'Doe', @credit_card) + @options.delete(:billing_address) + names = @gateway.send(:names_from_address, {}, @credit_card) assert_equal 'Longbob', names.first - assert_equal 'Doe', names.last + assert_equal 'Longsen', names.last + end + + def test_address_names_when_passing_string_token + names = @gateway.send(:names_from_address, @options[:billing_address], SecureRandom.uuid) + + assert_equal 'Cure', names.first + assert_equal 'Tester', names.last end def test_successful_store - response = stub_comms do + response = stub_comms(@gateway, :ssl_request) do @gateway.store(@credit_card, @options) end.respond_with(successful_access_token_response, successful_store_response) @@ -210,14 +280,52 @@ def test_successful_store def test_successful_inquire_request session_id = 'f8da8dc7-17de-4b5e-858d-4bdc47cd5dbf' - stub_comms do + stub_comms(@gateway, :ssl_request) do @gateway.inquire(session_id, {}) - end.check_request do |endpoint, data, _headers| + end.check_request do |_method, endpoint, data, _headers| request = JSON.parse(data) assert_equal request['orderSessionKey'], session_id if /outcome/.match?(endpoint) end.respond_with(successful_access_token_response, successful_purchase_response) end + def test_address_when_billing_address_provided + address = @gateway.send(:address, @options) + assert_equal 'CA', address[:country] + end + + def test_address_when_address_is_provided_in_options + @options.delete(:billing_address) + @options[:address] = { country: 'US' } + address = @gateway.send(:address, @options) + assert_equal 'US', address[:country] + end + + def test_authorization_from_on_store + response = stub_comms(@gateway, :ssl_request) do + @gateway.store(@credit_card, @options) + end.respond_with(successful_access_token_response, successful_store_response) + + assert_success response + assert_equal 'd3e10716-6aac-4eb8-a74d-c1a3027f1d96', response.authorization + end + + def test_authorization_from_on_purchase + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.respond_with(successful_access_token_response, successful_purchase_response) + + assert_success response + assert_equal 'ca7bb327-a750-412d-a9c3-050d72b3f0c5#USD', response.authorization + end + + def test_add_base_data_without_idempotency_key + @options.delete(:idempotency_key) + post = {} + @gateway.send(:add_base_data, post, @options) + + assert_equal 5, post[:idempotencyKey].split('-').size + end + private def pre_scrubbed @@ -535,4 +643,26 @@ def failed_refund_response } RESPONSE end + + def missed_idempotency_key_field + <<~RESPONSE + { + "TraceId": ["00-bf5a1XXXTRACEXXX174b8a-f58XXXIDXXX32-01"], + "IdempotencyKey": ["The IdempotencyKey field is required."], + "access_token": "SomeAccessTokenXXXX1ZWE5ZmY0LTM4MjUtNDc0ZC04ZDhhLTk2OGZjM2NlYTA5ZCIsImlhdCI6IjE3MjI1Mjc1ODI1MjIiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MjI1Mjc1ODIsImV4cCI6MTcyMjUyODE4MiwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.Q7b5CViX4x3Qmna-JmLS2pQD8kWbrI5-GLLT1Ki9t3o", + "token_expires": 1722528182522 + } + RESPONSE + end + + def invalid_expiry_date_utc + <<~RESPONSE + { + "TraceId": ["00-bf5a1XXXTRACEXXX174b8a-f58XXXIDXXX32-01"], + "ExpiryDateUtc":["The field ExpiryDateUtc is invalid."], + "access_token": "SomeAccessTokenXXXX1ZWE5ZmY0LTM4MjUtNDc0ZC04ZDhhLTk2OGZjM2NlYTA5ZCIsImlhdCI6IjE3MjI1Mjc1ODI1MjIiLCJhdWQiOlsicGF5bWVudHMiLCJvcmRlcnMiLCJtZXJjaGFudHMiLCJlbGlnaWJpbGl0eS1zZnRwIiwiZWxpZ2liaWxpdHkiLCJjb250YWN0Il0sImN1c3RvbTptaWQiOiJkOWQwYjVmZC05NDMzLTQ0ZDMtODA1MS02M2ZlZTI4NzY4ZTgiLCJuYmYiOjE3MjI1Mjc1ODIsImV4cCI6MTcyMjUyODE4MiwiaXNzIjoiQXBpLUNsaWVudC1TZXJ2aWNlIn0.Q7b5CViX4x3Qmna-JmLS2pQD8kWbrI5-GLLT1Ki9t3o", + "token_expires": 1722528182522 + } + RESPONSE + end end diff --git a/test/unit/gateways/hps_test.rb b/test/unit/gateways/hps_test.rb index 1f8832e7ab7..ad8ac0552fd 100644 --- a/test/unit/gateways/hps_test.rb +++ b/test/unit/gateways/hps_test.rb @@ -639,21 +639,21 @@ def test_three_d_secure_visa options = { three_d_secure: { + version: '2.2.0', cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', - xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' } } response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(/(.*)<\/hps:SecureECommerce>/, data) - assert_match(/Visa 3DSecure<\/hps:PaymentDataSource>/, data) - assert_match(/3DSecure<\/hps:TypeOfPaymentData>/, data) - assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:PaymentData>/, data) - assert_match(/5<\/hps:ECommerceIndicator>/, data) - assert_match(/#{options[:three_d_secure][:xid]}<\/hps:XID>/, data) + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:version]}<\/hps:Version>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) end.respond_with(successful_charge_response) assert_success response @@ -666,21 +666,21 @@ def test_three_d_secure_mastercard options = { three_d_secure: { + version: '2.2.0', cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', - xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' } } response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(/(.*)<\/hps:SecureECommerce>/, data) - assert_match(/MasterCard 3DSecure<\/hps:PaymentDataSource>/, data) - assert_match(/3DSecure<\/hps:TypeOfPaymentData>/, data) - assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:PaymentData>/, data) - assert_match(/5<\/hps:ECommerceIndicator>/, data) - assert_match(/#{options[:three_d_secure][:xid]}<\/hps:XID>/, data) + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:version]}<\/hps:Version>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) end.respond_with(successful_charge_response) assert_success response @@ -695,19 +695,17 @@ def test_three_d_secure_discover three_d_secure: { cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '5', - xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' } } response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(/(.*)<\/hps:SecureECommerce>/, data) - assert_match(/Discover 3DSecure<\/hps:PaymentDataSource>/, data) - assert_match(/3DSecure<\/hps:TypeOfPaymentData>/, data) - assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:PaymentData>/, data) - assert_match(/5<\/hps:ECommerceIndicator>/, data) - assert_match(/#{options[:three_d_secure][:xid]}<\/hps:XID>/, data) + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) end.respond_with(successful_charge_response) assert_success response @@ -722,19 +720,17 @@ def test_three_d_secure_amex three_d_secure: { cavv: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', eci: '05', - xid: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' + ds_transaction_id: 'TTBCSkVTa1ZpbDI1bjRxbGk5ODE=' } } response = stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @credit_card, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(/(.*)<\/hps:SecureECommerce>/, data) - assert_match(/AMEX 3DSecure<\/hps:PaymentDataSource>/, data) - assert_match(/3DSecure<\/hps:TypeOfPaymentData>/, data) - assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:PaymentData>/, data) - assert_match(/5<\/hps:ECommerceIndicator>/, data) - assert_match(/#{options[:three_d_secure][:xid]}<\/hps:XID>/, data) + assert_match(/(.*)<\/hps:Secure3D>/, data) + assert_match(/#{options[:three_d_secure][:cavv]}<\/hps:AuthenticationValue>/, data) + assert_match(/5<\/hps:ECI>/, data) + assert_match(/#{options[:three_d_secure][:ds_transaction_id]}<\/hps:DirectoryServerTxnId>/, data) end.respond_with(successful_charge_response) assert_success response diff --git a/test/unit/gateways/litle_test.rb b/test/unit/gateways/litle_test.rb index 4397c01f679..b79516737f7 100644 --- a/test/unit/gateways/litle_test.rb +++ b/test/unit/gateways/litle_test.rb @@ -42,6 +42,16 @@ def setup payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' } ) + @decrypted_network_token = NetworkTokenizationCreditCard.new( + { + source: :network_token, + month: '02', + year: '2050', + brand: 'master', + number: '5112010000000000', + payment_cryptogram: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=' + } + ) @amount = 100 @options = {} @check = check( @@ -82,6 +92,26 @@ def test_successful_purchase assert response.test? end + def test_successful_purchase_prepaid_card_141 + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_for_prepaid_cards_141) + + assert_success response + assert_equal 'Consumer non-reloadable prepaid card, Approved', response.message + assert_equal '141', response.params['response'] + end + + def test_successful_purchase_prepaid_card_142 + response = stub_comms do + @gateway.purchase(@amount, @credit_card) + end.respond_with(successful_purchase_for_prepaid_cards_142) + + assert_success response + assert_equal 'Consumer single-use virtual card number, Approved', response.message + assert_equal '142', response.params['response'] + end + def test_successful_purchase_with_010_response response = stub_comms do @gateway.purchase(@amount, @credit_card) @@ -344,6 +374,14 @@ def test_add_google_pay_order_source end.respond_with(successful_purchase_response) end + def test_add_network_token_order_source + stub_comms do + @gateway.purchase(@amount, @decrypted_network_token) + end.check_request do |_endpoint, data, _headers| + assert_match 'ecommerce', data + end.respond_with(successful_purchase_response) + end + def test_successful_authorize_and_capture response = stub_comms do @gateway.authorize(@amount, @credit_card) @@ -830,6 +868,48 @@ def successful_purchase_with_echeck_response ) end + def successful_purchase_for_prepaid_cards_141 + %( + + + 456342657452 + 123456 + 141 + 2024-04-09T19:50:30 + 2024-04-09 + Consumer non-reloadable prepaid card, Approved + 382410 + + 01 + M + + MPMMPMPMPMPU + + + ) + end + + def successful_purchase_for_prepaid_cards_142 + %( + + + 456342657452 + 123456 + 142 + 2024-04-09T19:50:30 + 2024-04-09 + Consumer single-use virtual card number, Approved + 382410 + + 01 + M + + MPMMPMPMPMPU + + + ) + end + def successful_authorize_stored_credentials %( diff --git a/test/unit/gateways/mercado_pago_test.rb b/test/unit/gateways/mercado_pago_test.rb index a25401c202d..f62a8944c06 100644 --- a/test/unit/gateways/mercado_pago_test.rb +++ b/test/unit/gateways/mercado_pago_test.rb @@ -40,9 +40,13 @@ def setup end def test_successful_purchase - @gateway.expects(:ssl_post).at_most(2).returns(successful_purchase_response) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal true, request['binary_mode'] if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) - response = @gateway.purchase(@amount, @credit_card, @options) assert_success response assert_equal '4141491|1.0', response.authorization @@ -362,6 +366,23 @@ def test_includes_deviceid_header assert_success response end + def test_includes_idempotency_key_header + @options[:idempotency_key] = '12345' + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json' }).returns(successful_purchase_response) + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json', 'X-Idempotency-Key' => '12345' }).returns(successful_purchase_response) + + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + end + + def test_includes_idempotency_key_header_for_refund + @options[:idempotency_key] = '12345' + @gateway.expects(:ssl_post).with(anything, anything, { 'Content-Type' => 'application/json', 'X-Idempotency-Key' => '12345' }).returns(successful_refund_response) + + response = @gateway.refund(@amount, 'authorization|1.0', @options) + assert_success response + end + def test_includes_additional_data @options[:additional_info] = { 'foo' => 'bar', 'baz' => 'quux' } response = stub_comms do @@ -499,6 +520,37 @@ def test_invalid_taxes_shape end end + def test_set_binary_mode_to_nil_when_request_is_3ds + stub_comms do + @gateway.authorize(@amount, @credit_card, @options.merge(execute_threed: true)) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_nil request['binary_mode'] if /payments/.match?(endpoint) + end.respond_with(successful_authorize_response) + end + + def test_should_not_include_sponsor_id_when_test_mode_is_enabled + @options[:sponsor_id] = '1234' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + assert_not_match(%r("sponsor_id":), data) if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) + end + + def test_should_include_sponsor_id_when_test_mode_is_disabled + @gateway.stubs(test?: false) + @options[:sponsor_id] = '1234' + + stub_comms do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '1234', request['sponsor_id'] if /payments/.match?(endpoint) + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed diff --git a/test/unit/gateways/merchant_warrior_test.rb b/test/unit/gateways/merchant_warrior_test.rb index b06fdb8320b..d5522180613 100644 --- a/test/unit/gateways/merchant_warrior_test.rb +++ b/test/unit/gateways/merchant_warrior_test.rb @@ -17,7 +17,10 @@ def setup @options = { address: address, - transaction_product: 'TestProduct' + transaction_product: 'TestProduct', + email: 'user@aol.com', + ip: '1.2.3.4', + store_id: 'My Store' } @three_ds_secure = { version: '2.2.0', @@ -145,9 +148,7 @@ def test_address state: 'NY', country: 'US', zip: '11111', - phone: '555-1212', - email: 'user@aol.com', - ip: '1.2.3.4' + phone: '555-1212' } stub_comms do @@ -162,6 +163,40 @@ def test_address assert_match(/customerIP=1.2.3.4/, data) assert_match(/customerPhone=555-1212/, data) assert_match(/customerEmail=user%40aol.com/, data) + assert_match(/storeID=My\+Store/, data) + end.respond_with(successful_purchase_response) + end + + def test_address_with_phone_number + options = { + address: { + name: 'Bat Man', + address1: '123 Main', + city: 'Brooklyn', + state: 'NY', + country: 'US', + zip: '11111', + phone_number: '555-1212' + }, + transaction_product: 'TestProduct', + email: 'user@aol.com', + ip: '1.2.3.4', + store_id: 'My Store' + } + + stub_comms do + @gateway.purchase(@success_amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/customerName=Bat\+Man/, data) + assert_match(/customerCountry=US/, data) + assert_match(/customerState=NY/, data) + assert_match(/customerCity=Brooklyn/, data) + assert_match(/customerAddress=123\+Main/, data) + assert_match(/customerPostCode=11111/, data) + assert_match(/customerIP=1.2.3.4/, data) + assert_match(/customerPhone=555-1212/, data) + assert_match(/customerEmail=user%40aol.com/, data) + assert_match(/storeID=My\+Store/, data) end.respond_with(successful_purchase_response) end diff --git a/test/unit/gateways/mercury_test.rb b/test/unit/gateways/mercury_test.rb index d9b2e8d9cd0..9dd62ea082b 100644 --- a/test/unit/gateways/mercury_test.rb +++ b/test/unit/gateways/mercury_test.rb @@ -126,38 +126,7 @@ def test_transcript_scrubbing def successful_purchase_response <<~RESPONSE - - - - Processor - 000000 - Approved - AP* - - - - 595901 - 5499990123456781 - 0813 - M/C - Sale - 000011 - Captured - 0194 - 1 - Y - M - 999 - LM Integration (Ruby) - - 1.00 - 1.00 - - KbMCC0742510421 - |17|410100700000 - - - + \r\n\r\n\t\r\n\t\tProcessor\r\n\t\t000000\r\n\t\tApproved\r\n\t\tAP\r\n\t\t\r\n\t\r\n\t\r\n\t\t595901\r\n\t\t5499990123456781\r\n\t\t0813\r\n\t\tM/C\r\n\t\tSale\r\n\t\t000011\r\n\t\tCaptured\r\n\t\t0194\r\n\t\t1\r\n\t\tY\r\n\t\tM\r\n\t\t999\r\n\t\tLM Integration (Ruby)\r\n\t\t\r\n\t\t\t1.00\r\n\t\t\t1.00\r\n\t\t\r\n\t\tKbMCC0742510421 \r\n\t\t|17|410100700000\r\n\t\r\n\r\n RESPONSE end diff --git a/test/unit/gateways/moneris_test.rb b/test/unit/gateways/moneris_test.rb index feefeace8c6..cd8e3da72ba 100644 --- a/test/unit/gateways/moneris_test.rb +++ b/test/unit/gateways/moneris_test.rb @@ -15,7 +15,7 @@ def setup @credit_card = credit_card('4242424242424242') # https://developer.moneris.com/livedemo/3ds2/reference/guide/php - @fully_authenticated_eci = 5 + @fully_authenticated_eci = '02' @no_liability_shift_eci = 7 @options = { order_id: '1', customer: '1', billing_address: address } @@ -86,6 +86,7 @@ def test_failed_mpi_cavv_purchase assert_match(/12345<\/ds_trans_id>/, data) assert_match(/d0f461f8-960f-40c9-a323-4e43a4e16aaa<\/threeds_server_trans_id>/, data) assert_match(/2<\/threeds_version>/, data) + assert_match(/2<\/crypt_type>/, data) end.respond_with(failed_cavv_purchase_response) assert_failure response diff --git a/test/unit/gateways/nmi_test.rb b/test/unit/gateways/nmi_test.rb index f2817a6a38e..5e011a07f81 100644 --- a/test/unit/gateways/nmi_test.rb +++ b/test/unit/gateways/nmi_test.rb @@ -29,6 +29,56 @@ def setup descriptor_merchant_id: '120', descriptor_url: 'url' } + + @google_pay = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :google_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + + @apple_pay = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + month: '02', + year: '2035', + source: :apple_pay, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + transaction_id: '13456789' + ) + end + + def test_successful_apple_pay_purchase + stub_comms do + @gateway.purchase(@amount, @apple_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/type=sale/, data) + assert_match(/ccnumber=4111111111111111/, data) + assert_match(/ccexp=#{sprintf("%.2i", @apple_pay.month)}#{@apple_pay.year.to_s[-2..-1]}/, data) + assert_match(/cavv=EHuWW9PiBkWvqE5juRwDzAUFBAk%3D/, data) + assert_match(/eci=05/, data) + assert_match(/decrypted_applepay_data/, data) + assert_nil data['decrypted_googlepay_data'] + end.respond_with(successful_purchase_response) + end + + def test_successful_google_pay_purchase + stub_comms do + @gateway.purchase(@amount, @google_pay) + end.check_request do |_endpoint, data, _headers| + assert_match(/type=sale/, data) + assert_match(/ccnumber=4111111111111111/, data) + assert_match(/ccexp=#{sprintf("%.2i", @google_pay.month)}#{@google_pay.year.to_s[-2..-1]}/, data) + assert_match(/cavv=EHuWW9PiBkWvqE5juRwDzAUFBAk%3D/, data) + assert_match(/eci=05/, data) + assert_match(/decrypted_googlepay_data/, data) + assert_nil data['decrypted_applepay_data'] + end.respond_with(successful_purchase_response) end def test_successful_authorize_and_capture_using_security_key @@ -160,6 +210,23 @@ def test_purchase_with_shipping_fields assert_success response end + def test_purchase_with_customer_vault_options + options = { + description: 'Store purchase', + customer_vault: 'add_customer', + customer_vault_id: '12345abcde' + } + + response = stub_comms do + @gateway.purchase(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(/customer_vault=add_customer/, data) + assert_match(/customer_vault_id=12345abcde/, data) + end.respond_with(successful_purchase_response) + + assert_success response + end + def test_purchase_with_shipping_fields_omits_blank_name options = @transaction_options.merge({ shipping_address: shipping_address(name: nil) }) diff --git a/test/unit/gateways/nuvei_test.rb b/test/unit/gateways/nuvei_test.rb new file mode 100644 index 00000000000..d19d19981ef --- /dev/null +++ b/test/unit/gateways/nuvei_test.rb @@ -0,0 +1,187 @@ +require 'test_helper' + +class NuveiTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = NuveiGateway.new( + merchant_id: 'SOMECREDENTIAL', + merchant_site_id: 'SOMECREDENTIAL', + secret_key: 'SOMECREDENTIAL', + session_token: 'fdda0126-674f-4f8c-ad24-31ac846654ab', + token_expires: Time.now.utc.to_i + 900 + ) + @credit_card = credit_card + @amount = 100 + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip_address: '127.0.0.1' + } + + @post = { + merchantId: 'test_merchant_id', + merchantSiteId: 'test_merchant_site_id', + clientRequestId: 'test_client_request_id', + clientUniqueId: 'test_client_unique_id', + amount: '100', + currency: 'US', + relatedTransactionId: 'test_related_transaction_id', + timeStamp: 'test_time_stamp' + } + end + + def test_calculate_checksum_authenticate + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_idtest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :authenticate) + end + + def test_calculate_checksum_capture + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_idtest_client_unique_id100UStest_related_transaction_idtest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :capture) + end + + def test_calculate_checksum_other + expected_checksum = Digest::SHA256.hexdigest('test_merchant_idtest_merchant_site_idtest_client_request_id100UStest_time_stampSOMECREDENTIAL') + assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :other) + end + + def supported_card_types + assert_equal %i(visa master american_express discover union_pay), NuveiGateway.supported_cardtypes + end + + def test_supported_countries + assert_equal %w(US CA IN NZ GB AU US), NuveiGateway.supported_countries + end + + def build_request_authenticate_url + action = :authenticate + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}/getSessionToken" + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_successful_authorize + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_match(%r(/payment), endpoint) + assert_match(/Auth/, json_data['transactionType']) + end + end.respond_with(successful_authorize_response) + end + + def test_successful_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_match(/#{@amount}/, json_data['amount']) + assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber']) + assert_match(/#{@credit_card.verification_value}/, json_data['paymentOption']['card']['CVV']) + assert_match(%r(/payment), endpoint) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_refund + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '123456', @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /refundTransaction/.match?(endpoint) + assert_match(/123456/, json_data['relatedTransactionId']) + assert_match(/#{@amount}/, json_data['amount']) + end + end + end + + def test_successful_credit + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payout/.match?(endpoint) + assert_match(/#{@amount}/, json_data['amount']) + assert_match(/#{@credit_card.number}/, json_data['cardData']['cardNumber']) + end + end.respond_with(successful_purchase_response) + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"3755516963854600967","merchantSiteId":"255388","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"4761344136141390","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"4761344136141390\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"paymentOption\":{\"userPaymentOptionId\":\"\",\"card\":{\"ccCardNumber\":\"4****1390\",\"bin\":\"476134\",\"last4Digits\":\"1390\",\"ccExpMonth\":\"09\",\"ccExpYear\":\"25\",\"acquirerId\":\"19\",\"cvv2Reply\":\"\",\"avsCode\":\"\",\"cardType\":\"Debit\",\"cardBrand\":\"VISA\",\"issuerBankName\":\"INTL HDQTRS-CENTER OWNED\",\"issuerCountry\":\"SG\",\"isPrepaid\":\"false\",\"threeD\":{},\"processedBrand\":\"VISA\"},\"paymentAccountReference\":\"f4iK2pnudYKvTALGdcwEzqj9p4\"},\"transactionStatus\":\"APPROVED\",\"gwErrorCode\":0,\"gwExtendedErrorCode\":0,\"issuerDeclineCode\":\"\",\"issuerDeclineReason\":\"\",\"transactionType\":\"Auth\",\"transactionId\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"[FILTERED]","merchantSiteId":"[FILTERED]","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"[FILTERED]","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"[FILTERED]\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"paymentOption\":{\"userPaymentOptionId\":\"\",\"card\":{\"ccCardNumber\":\"4****1390\",\"bin\":\"476134\",\"last4Digits\":\"1390\",\"ccExpMonth\":\"09\",\"ccExpYear\":\"25\",\"acquirerId\":\"19\",\"cvv2Reply\":\"\",\"avsCode\":\"\",\"cardType\":\"Debit\",\"cardBrand\":\"VISA\",\"issuerBankName\":\"INTL HDQTRS-CENTER OWNED\",\"issuerCountry\":\"SG\",\"isPrepaid\":\"false\",\"threeD\":{},\"processedBrand\":\"VISA\"},\"paymentAccountReference\":\"f4iK2pnudYKvTALGdcwEzqj9p4\"},\"transactionStatus\":\"APPROVED\",\"gwErrorCode\":0,\"gwExtendedErrorCode\":0,\"issuerDeclineCode\":\"\",\"issuerDeclineReason\":\"\",\"transactionType\":\"Auth\",\"transactionId\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + POST_SCRUBBED + end + + def successful_authorize_response + <<~RESPONSE + {"internalRequestId":1171104468,"status":"SUCCESS","errCode":0,"reason":"","merchantId":"3755516963854600967","merchantSiteId":"255388","version":"1.0","clientRequestId":"02ba666c-e3e5-4ec9-ae30-3f8500b18c96","sessionToken":"29226538-82c7-4a3c-b363-cb6829b8c32a","clientUniqueId":"c00ed73a7d682bf478295d57bdae3028","orderId":"471361708","paymentOption":{"userPaymentOptionId":"","card":{"ccCardNumber":"4****1390","bin":"476134","last4Digits":"1390","ccExpMonth":"09","ccExpYear":"25","acquirerId":"19","cvv2Reply":"","avsCode":"","cardType":"Debit","cardBrand":"VISA","issuerBankName":"INTL HDQTRS-CENTER OWNED","issuerCountry":"SG","isPrepaid":"false","threeD":{},"processedBrand":"VISA"},"paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"},"transactionStatus":"APPROVED","gwErrorCode":0,"gwExtendedErrorCode":0,"issuerDeclineCode":"","issuerDeclineReason":"","transactionType":"Auth","transactionId":"7110000000001908486","externalTransactionId":"","authCode":"111397","customData":"","fraudDetails":{"finalDecision":"Accept","score":"0"},"externalSchemeTransactionId":"","merchantAdviceCode":""} + RESPONSE + end + + def successful_purchase_response + <<~RESPONSE + {"internalRequestId":1172848838, "status":"SUCCESS", "errCode":0, "reason":"", "merchantId":"3755516963854600967", "merchantSiteId":"255388", "version":"1.0", "clientRequestId":"a114381a-0f88-46d0-920c-7b5614f29e5b", "sessionToken":"d3424c9c-dd6d-40dc-85da-a2b92107cbe3", "clientUniqueId":"3ba2a81c46d78837ea819d9f3fe644e7", "orderId":"471833818", "paymentOption":{"userPaymentOptionId":"", "card":{"ccCardNumber":"4****1390", "bin":"476134", "last4Digits":"1390", "ccExpMonth":"09", "ccExpYear":"25", "acquirerId":"19", "cvv2Reply":"", "avsCode":"", "cardType":"Debit", "cardBrand":"VISA", "issuerBankName":"INTL HDQTRS-CENTER OWNED", "issuerCountry":"SG", "isPrepaid":"false", "threeD":{}, "processedBrand":"VISA"}, "paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"}, "transactionStatus":"APPROVED", "gwErrorCode":0, "gwExtendedErrorCode":0, "issuerDeclineCode":"", "issuerDeclineReason":"", "transactionType":"Sale", "transactionId":"7110000000001990927", "externalTransactionId":"", "authCode":"111711", "customData":"", "fraudDetails":{"finalDecision":"Accept", "score":"0"}, "externalSchemeTransactionId":"", "merchantAdviceCode":""} + RESPONSE + end +end diff --git a/test/unit/gateways/orbital_test.rb b/test/unit/gateways/orbital_test.rb index 150cc874ac1..7efa4989643 100644 --- a/test/unit/gateways/orbital_test.rb +++ b/test/unit/gateways/orbital_test.rb @@ -115,6 +115,16 @@ def setup } } + @three_d_secure_options_eci_6 = { + three_d_secure: { + eci: '6', + xid: 'TESTXID', + cavv: 'TESTCAVV', + version: '2.2.0', + ds_transaction_id: '97267598FAE648F28083C23433990FBC' + } + } + @google_pay_card = network_tokenization_credit_card( '4777777777777778', payment_cryptogram: 'BwAQCFVQdwEAABNZI1B3EGLyGC8=', @@ -460,6 +470,7 @@ def test_three_d_secure_data_on_discover_purchase end.check_request do |_endpoint, data, _headers| assert_match %{5}, data assert_match %{TESTCAVV}, data + assert_match %{DPB}, data end.respond_with(successful_purchase_response) end @@ -469,6 +480,7 @@ def test_three_d_secure_data_on_discover_authorization end.check_request do |_endpoint, data, _headers| assert_match %{5}, data assert_match %{TESTCAVV}, data + assert_match %{DPB}, data end.respond_with(successful_purchase_response) end @@ -1316,6 +1328,61 @@ def test_successful_refund_with_echeck assert_equal '1', response.params['approval_status'] end + def test_three_d_secure_data_on_master_purchase_with_custom_ucaf_and_flag_on_if_eci_is_valid + options = @options.merge(@three_d_secure_options) + options.merge!({ ucaf_collection_indicator: '5', alternate_ucaf_flow: true }) + assert_equal '6', @three_d_secure_options_eci_6.dig(:three_d_secure, :eci) + + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), options) + end.check_request do |_endpoint, data, _headers| + assert_no_match(/\5}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_purchase_with_flag_on_but_no_custom_ucaf + options = @options.merge(@three_d_secure_options_eci_6) + options.merge!(alternate_ucaf_flow: true) + assert_equal '6', @three_d_secure_options_eci_6.dig(:three_d_secure, :eci) + + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), options) + end.check_request do |_endpoint, data, _headers| + assert_no_match(/\4}, data + end.respond_with(successful_purchase_response) + end + + def test_three_d_secure_data_on_master_purchase_with_flag_off_and_custom_ucaf + options = @options.merge(@three_d_secure_options) + options.merge!(ucaf_collection_indicator: '5') + stub_comms do + @gateway.purchase(50, credit_card(nil, brand: 'master'), options) + end.check_request do |_endpoint, data, _headers| + assert_match %{5}, data + end.respond_with(successful_purchase_response) + end + def test_failed_refund_with_echeck @gateway.expects(:ssl_post).returns(failed_refund_with_echeck_response) diff --git a/test/unit/gateways/paypal_test.rb b/test/unit/gateways/paypal_test.rb index db9f5c760a0..7f8bd050e1f 100644 --- a/test/unit/gateways/paypal_test.rb +++ b/test/unit/gateways/paypal_test.rb @@ -1312,7 +1312,7 @@ def failed_create_profile_paypal_response - " + RESPONSE end diff --git a/test/unit/gateways/pin_test.rb b/test/unit/gateways/pin_test.rb index 5cff9513e84..200ca321e98 100644 --- a/test/unit/gateways/pin_test.rb +++ b/test/unit/gateways/pin_test.rb @@ -14,6 +14,12 @@ def setup ip: '127.0.0.1' } + @three_d_secure = { + enabled: true, + fallback_ok: true, + callback_url: 'https://yoursite.com/authentication_complete' + } + @three_d_secure_v1 = { version: '1.0.2', eci: '05', @@ -367,6 +373,14 @@ def test_add_creditcard_with_customer_token assert_false post.has_key?(:card) end + def test_add_3ds + post = {} + @gateway.send(:add_3ds, post, @options.merge(three_d_secure: @three_d_secure)) + assert_equal true, post[:three_d_secure][:enabled] + assert_equal true, post[:three_d_secure][:fallback_ok] + assert_equal 'https://yoursite.com/authentication_complete', post[:three_d_secure][:callback_url] + end + def test_add_3ds_v1 post = {} @gateway.send(:add_3ds, post, @options.merge(three_d_secure: @three_d_secure_v1)) diff --git a/test/unit/gateways/plexo_test.rb b/test/unit/gateways/plexo_test.rb index 1aab16caf8a..225864eae1e 100644 --- a/test/unit/gateways/plexo_test.rb +++ b/test/unit/gateways/plexo_test.rb @@ -9,6 +9,15 @@ def setup @amount = 100 @credit_card = credit_card('5555555555554444', month: '12', year: '2024', verification_value: '111', first_name: 'Santiago', last_name: 'Navatta') @declined_card = credit_card('5555555555554445') + @network_token_credit_card = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({ + first_name: 'Santiago', last_name: 'Navatta', + brand: 'Mastercard', + payment_cryptogram: 'UnVBR0RlYm42S2UzYWJKeWJBdWQ=', + number: '5555555555554444', + source: :network_token, + month: '12', + year: 2020 + }) @options = { email: 'snavatta@plexo.com.uy', ip: '127.0.0.1', @@ -329,6 +338,26 @@ def test_scrub assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed end + def test_purchase_with_network_token + purchase = stub_comms do + @gateway.purchase(@amount, @network_token_credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal request['Amount']['Currency'], 'UYU' + assert_equal request['Amount']['Details']['TipAmount'], '5' + assert_equal request['Flow'], 'direct' + assert_equal request['paymentMethod']['source'], 'network-token' + assert_equal @network_token_credit_card.number, request['paymentMethod']['NetworkToken']['Number'] + assert_equal @network_token_credit_card.payment_cryptogram, request['paymentMethod']['NetworkToken']['Cryptogram'] + assert_equal @network_token_credit_card.first_name, request['paymentMethod']['NetworkToken']['Cardholder']['FirstName'] + assert_equal request['paymentMethod']['NetworkToken']['ExpMonth'], '12' + assert_equal request['paymentMethod']['NetworkToken']['ExpYear'], '20' + end.respond_with(successful_network_token_response) + + assert_success purchase + assert_equal 'You have been mocked.', purchase.message + end + private def pre_scrubbed @@ -917,4 +946,132 @@ def failed_verify_response } RESPONSE end + + def successful_network_token_response + <<~RESPONSE + { + "id": "71d4e94a30124a7ba00809c00b7b1149", + "referenceId": "ecca673a4041317aec64e9e823b3c5d9", + "invoiceNumber": "12345abcde", + "status": "approved", + "flow": "direct", + "processingMethod": "api", + "browserDetails": { + "ipAddress": "127.0.0.1" + }, + "createdAt": "2024-05-21T20:18:33.072Z", + "updatedAt": "2024-05-21T20:18:33.3896406Z", + "processedAt": "2024-05-21T20:18:33.3896407Z", + "merchant": { + "id": 3243, + "name": "spreedly", + "settings": { + "merchantIdentificationNumber": "98001456", + "metadata": { + "paymentProcessorId": "fiserv" + }, + "paymentProcessor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "clientId": 221 + }, + "client": { + "id": 221, + "name": "Spreedly", + "owner": "PLEXO" + }, + "paymentMethod": { + "id": "mastercard", + "legacyId": 4, + "name": "MASTERCARD", + "type": "card", + "card": { + "name": "555555XXXXXX4444", + "bin": "555555", + "last4": "4444", + "expMonth": 12, + "expYear": 20, + "cardholder": { + "firstName": "Santiago", + "lastName": "Navatta", + "email": "snavatta@plexo.com.uy", + "identification": { + "type": 1, + "value": "123456" + }, + "billingAddress": { + "city": "Ottawa", + "country": "CA", + "line1": "456 My Street", + "line2": "Apt 1", + "postalCode": "K1C2N6", + "state": "ON" + } + }, + "type": "prepaid", + "origin": "uruguay", + "token": "116d03bef91f4e0e8531af47ed34f690", + "issuer": { + "id": 21289, + "name": "", + "shortName": "" + }, + "tokenization": { + "type": "temporal" + } + }, + "processor": { + "id": 4, + "acquirer": "fiserv" + } + }, + "installments": 1, + "amount": { + "currency": "UYU", + "total": 1, + "details": { + "tax": { + "type": "none", + "amount": 0 + }, + "taxedAmount": 0, + "tipAmount": 5 + } + }, + "items": [ + { + "referenceId": "a6117dae92648552eb83a4ad0548833a", + "name": "prueba", + "description": "prueba desc", + "quantity": 1, + "price": 100, + "discount": 0, + "metadata": {} + } + ], + "metadata": {}, + "transactions": [ + { + "id": "664d019985707cbcfc11f0b2", + "parentId": "71d4e94a30124a7ba00809c00b7b1149", + "traceId": "c7b07c9c-d3c3-466b-8185-973321c6ab70", + "referenceId": "ecca673a4041317aec64e9e823b3c5d9", + "type": "purchase", + "status": "approved", + "createdAt": "2024-05-21T20:18:33.3896404Z", + "processedAt": "2024-05-21T20:18:33.3896397Z", + "resultCode": "0", + "resultMessage": "You have been mocked.", + "authorization": "123456", + "ticket": "02bbae8109fd4ceca0838628692486c6", + "metadata": {}, + "amount": 1 + } + ], + "actions": [] + } + RESPONSE + end end diff --git a/test/unit/gateways/rapyd_test.rb b/test/unit/gateways/rapyd_test.rb index 43f93789952..cb60bdae4a5 100644 --- a/test/unit/gateways/rapyd_test.rb +++ b/test/unit/gateways/rapyd_test.rb @@ -182,6 +182,16 @@ def test_success_purchase_with_recurrence_type end.respond_with(successful_purchase_response) end + def test_successful_purchase_with_save_payment_method + response = stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge({ save_payment_method: true })) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/"save_payment_method":true/, data) + end.respond_with(successful_authorize_response) + + assert_success response + end + def test_successful_purchase_with_3ds_global @options[:three_d_secure] = { required: true, diff --git a/test/unit/gateways/redsys_rest_test.rb b/test/unit/gateways/redsys_rest_test.rb index 89f7cb43814..b5e95882e60 100644 --- a/test/unit/gateways/redsys_rest_test.rb +++ b/test/unit/gateways/redsys_rest_test.rb @@ -107,6 +107,52 @@ def test_use_of_add_threeds assert_equal post.dig(:DS_MERCHANT_EMV3DS, :browserScreenHeight), execute3ds.dig(:three_ds_2, :height) end + def test_use_of_add_stored_credentials_cit + stored_credentials_post = {} + options = { + stored_credential: { + network_transaction_id: nil, + initial_transaction: true, + reason_type: 'recurring', + initiator: 'cardholder' + } + } + @gateway.send(:add_stored_credentials, stored_credentials_post, options) + assert_equal stored_credentials_post[:DS_MERCHANT_IDENTIFIER], 'REQUIRED' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TYPE], 'R' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_INI], 'S' + end + + def test_use_of_add_stored_credentials_mit + stored_credentials_post = {} + options = { + stored_credential: { + network_transaction_id: '9999999999', + initial_transaction: false, + reason_type: 'recurring', + initiator: 'merchant' + } + } + @gateway.send(:add_stored_credentials, stored_credentials_post, options) + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TYPE], 'R' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_INI], 'N' + assert_equal stored_credentials_post[:DS_MERCHANT_COF_TXNID], options[:stored_credential][:network_transaction_id] + end + + def test_use_of_three_ds_exemption + post = {} + options = { three_ds_exemption_type: 'low_value' } + @gateway.send(:add_threeds_exemption_data, post, options) + assert_equal post[:DS_MERCHANT_EXCEP_SCA], 'LWV' + end + + def test_use_of_three_ds_exemption_moto_option + post = {} + options = { three_ds_exemption_type: 'moto' } + @gateway.send(:add_threeds_exemption_data, post, options) + assert_equal post[:DS_MERCHANT_DIRECTPAYMENT], 'MOTO' + end + def test_failed_purchase @gateway.expects(:ssl_post).returns(failed_purchase_response) res = @gateway.purchase(123, credit_card, @options) diff --git a/test/unit/gateways/safe_charge_test.rb b/test/unit/gateways/safe_charge_test.rb index 06ba3574287..60cf0795645 100644 --- a/test/unit/gateways/safe_charge_test.rb +++ b/test/unit/gateways/safe_charge_test.rb @@ -94,6 +94,21 @@ def test_successful_purchase_with_truthy_stored_credential_mode assert purchase.test? end + def test_successful_purchase_with_card_holder_verification + purchase = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge(middle_name: 'middle', card_holder_verification: 1)) + end.check_request do |_endpoint, data, _headers| + assert_match(/sg_middleName=middle/, data) + assert_match(/sg_doCardHolderNameVerification=1/, data) + end.respond_with(successful_purchase_response) + + assert_success purchase + assert_equal '111951|101508189567|ZQBpAFAASABGAHAAVgBPAFUAMABiADMAewBtAGsAd' \ + 'AAvAFIAQQBrAGoAYwBxACoAXABHAEEAOgA3ACsAMgA4AD0AOABDAG4AbQAzAF' \ + 'UAbQBYAFIAMwA=|%02d|%d|1.00|USD' % [@credit_card.month, @credit_card.year.to_s[-2..-1]], purchase.authorization + assert purchase.test? + end + def test_successful_purchase_with_falsey_stored_credential_mode purchase = stub_comms do @gateway.purchase(@amount, @credit_card, @options.merge(stored_credential_mode: false)) diff --git a/test/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index cf80a066e9e..ed45c7bb230 100644 --- a/test/unit/gateways/stripe_payment_intents_test.rb +++ b/test/unit/gateways/stripe_payment_intents_test.rb @@ -59,6 +59,8 @@ def setup first_name: 'Longbob', last_name: 'Longsen' ) + + @network_transaction_id = '1098510912210968' end def test_successful_create_and_confirm_intent @@ -161,6 +163,15 @@ def test_request_three_d_secure assert_match(/\[request_three_d_secure\]=automatic/, data) end.respond_with(successful_request_three_d_secure_response) + request_three_d_secure = 'challenge' + options = @options.merge(request_three_d_secure: request_three_d_secure) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\[request_three_d_secure\]=challenge/, data) + end.respond_with(successful_request_three_d_secure_response) + request_three_d_secure = true options = @options.merge(request_three_d_secure: request_three_d_secure) @@ -315,6 +326,15 @@ def test_successful_purchase end.respond_with(successful_create_intent_response) end + def test_failed_authorize_with_idempotent_replayed + @gateway.instance_variable_set(:@response_headers, { 'idempotent-replayed' => 'true' }) + @gateway.expects(:ssl_request).returns(failed_payment_method_response) + + response = @gateway.authorize(@amount, @credit_card, @options) + assert_failure response + assert response.params['response_headers']['idempotent_replayed'], 'true' + end + def test_failed_error_on_requires_action @gateway.expects(:ssl_request).returns(failed_with_set_error_on_requires_action_response) @@ -347,6 +367,25 @@ def test_successful_verify assert_equal 'succeeded', verify.params['status'] end + def test_successful_verify_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.verify(@google_pay, @options.merge(new_ap_gp_route: true)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('payment_method_data[card][network_token][tokenization_method]=google_pay_dpan', data) + assert_match("payment_method_data[card][network_token][number]=#{@google_pay.number}", data) + assert_match('payment_method_options[card][network_token][cryptogram]', data) + assert_match("payment_method_options[card][network_token][electronic_commerce_indicator]=#{@google_pay.eci}", data) + end.respond_with(successful_verify_response) + end + + def test_successful_verify_non_tokenized_google_pay + stub_comms(@gateway, :ssl_request) do + @gateway.verify(@credit_card, @options.merge!(wallet_type: :non_tokenized_google_pay)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('metadata[input_method]=GooglePay', data) + end.respond_with(successful_verify_response) + end + def test_successful_purchase_with_level3_data @options[:merchant_reference] = 123 @options[:customer_reference] = 456 @@ -392,7 +431,6 @@ def test_successful_purchase_with_card_brand def test_succesful_purchase_with_stored_credentials_without_sending_ntid [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| - network_transaction_id = '1098510912210968' stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, card_to_use, { currency: 'USD', @@ -404,7 +442,7 @@ def test_succesful_purchase_with_stored_credentials_without_sending_ntid initiator: 'cardholder', reason_type: 'installment', initial_transaction: true, - network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + network_transaction_id: @network_transaction_id, # TEST env seems happy with any value :/ ds_transaction_id: 'null' # this is optional and can be null if not available. } }) @@ -418,7 +456,6 @@ def test_succesful_purchase_with_stored_credentials_without_sending_ntid def test_succesful_purchase_with_ntid_when_off_session # don't send NTID if setup_future_usage == off_session [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| - network_transaction_id = '1098510912210968' stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, card_to_use, { currency: 'USD', @@ -430,7 +467,7 @@ def test_succesful_purchase_with_ntid_when_off_session initiator: 'cardholder', reason_type: 'installment', initial_transaction: true, - network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + network_transaction_id: @network_transaction_id, # TEST env seems happy with any value :/ ds_transaction_id: 'null' # this is optional and can be null if not available. } }) @@ -443,7 +480,6 @@ def test_succesful_purchase_with_ntid_when_off_session def test_succesful_purchase_with_stored_credentials [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| - network_transaction_id = '1098510912210968' stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, card_to_use, { currency: 'USD', @@ -451,12 +487,12 @@ def test_succesful_purchase_with_stored_credentials confirm: true, off_session: true, stored_credential: { - network_transaction_id: network_transaction_id, # TEST env seems happy with any value :/ + network_transaction_id: @network_transaction_id, # TEST env seems happy with any value :/ ds_transaction_id: 'null' # this is optional and can be null if not available. } }) end.check_request do |_method, _endpoint, data, _headers| - assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{network_transaction_id}}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[ds_transaction_id\]=null}, data) end.respond_with(successful_create_intent_response) end @@ -464,7 +500,6 @@ def test_succesful_purchase_with_stored_credentials def test_succesful_purchase_with_stored_credentials_without_optional_ds_transaction_id [@three_ds_off_session_credit_card, @three_ds_authentication_required_setup_for_off_session].each do |card_to_use| - network_transaction_id = '1098510912210968' stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, card_to_use, { currency: 'USD', @@ -472,11 +507,11 @@ def test_succesful_purchase_with_stored_credentials_without_optional_ds_transact confirm: true, off_session: true, stored_credential: { - network_transaction_id: network_transaction_id # TEST env seems happy with any value :/ + network_transaction_id: @network_transaction_id # TEST env seems happy with any value :/ } }) end.check_request do |_method, _endpoint, data, _headers| - assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{network_transaction_id}}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) assert_no_match(%r{payment_method_options\[card\]\[mit_exemption\]\[ds_transaction_id\]=null}, data) end.respond_with(successful_create_intent_response) end @@ -496,15 +531,12 @@ def test_succesful_purchase_without_stored_credentials_introduces_no_exemption_f end def test_sends_network_transaction_id_separate_from_stored_creds - network_transaction_id = '1098510912210968' - options = @options.merge( - network_transaction_id: network_transaction_id - ) + options = @options.merge(network_transaction_id: @network_transaction_id) stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @visa_token, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{network_transaction_id}}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) end.respond_with(successful_create_intent_response) end @@ -527,11 +559,20 @@ def test_purchase_with_google_pay stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @google_pay, options) end.check_request do |_method, _endpoint, data, _headers| + assert_match("payment_method_data[card][network_token][number]=#{@google_pay.number}", data) assert_match('payment_method_options[card][network_token][electronic_commerce_indicator]=05', data) assert_match('payment_method_data[card][network_token][tokenization_method]=google_pay_dpan', data) end.respond_with(successful_create_intent_response) end + def test_purchase_with_google_pay_non_tokenized + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options.merge(wallet_type: :non_tokenized_google_pay)) + end.check_request do |_method, _endpoint, data, _headers| + assert_match('metadata[input_method]=GooglePay', data) + end.respond_with(successful_create_intent_response) + end + def test_purchase_with_google_pay_with_billing_address options = { currency: 'GBP', @@ -652,10 +693,9 @@ def test_authorize_with_apple_pay_with_billing_address end def test_stored_credentials_does_not_override_ntid_field - network_transaction_id = '1098510912210968' sc_network_transaction_id = '1078784111114777' options = @options.merge( - network_transaction_id: network_transaction_id, + network_transaction_id: @network_transaction_id, stored_credential: { network_transaction_id: sc_network_transaction_id, ds_transaction_id: 'null' @@ -665,7 +705,7 @@ def test_stored_credentials_does_not_override_ntid_field stub_comms(@gateway, :ssl_request) do @gateway.purchase(@amount, @visa_token, options) end.check_request do |_method, _endpoint, data, _headers| - assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{network_transaction_id}}, data) + assert_match(%r{payment_method_options\[card\]\[mit_exemption\]\[network_transaction_id\]=#{@network_transaction_id}}, data) end.respond_with(successful_create_intent_response) end @@ -873,11 +913,13 @@ def test_succesful_purchase_with_subsequent_cit stored_credential: { initial_transaction: false, initiator: 'cardholder', - reason_type: 'installment' + reason_type: 'installment', + network_transaction_id: '1098510912210968' } }) end.check_request do |_method, _endpoint, data, _headers| assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_on_session', data) + assert_not_match('payment_method_options[card][mit_exemption][network_transaction_id]=1098510912210968', data) end.respond_with(successful_create_intent_response) end @@ -890,11 +932,13 @@ def test_succesful_purchase_with_mit_recurring stored_credential: { initial_transaction: false, initiator: 'merchant', - reason_type: 'recurring' + reason_type: 'recurring', + network_transaction_id: '1098510912210968' } }) end.check_request do |_method, _endpoint, data, _headers| assert_match('payment_method_options[card][stored_credential_transaction_type]=stored_off_session_recurring', data) + assert_match('payment_method_options[card][mit_exemption][network_transaction_id]=1098510912210968', data) end.respond_with(successful_create_intent_response) end @@ -926,6 +970,69 @@ def test_successful_avs_and_cvc_check assert_equal 'Y', purchase.avs_result.dig('code') end + def test_create_setup_intent_with_moto_exemption + options = @options.merge(moto: true, confirm: true) + + stub_comms(@gateway, :ssl_request) do + @gateway.create_setup_intent(@visa_token, options) + end.check_request do |_method, _endpoint, data, _headers| + assert_match(/\[moto\]=true/, data) + end.respond_with(successful_verify_response) + end + + def test_add_network_token_cryptogram_and_eci_for_apple_pay_cit + options = { + currency: 'USD', + execute_threed: true, + confirm: true, + off_session: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'cardholder', + reason_type: 'installment', + initial_transaction: true, + network_transaction_id: @network_transaction_id, # TEST env seems happy with any value :/ + ds_transaction_id: 'null' # this is optional and can be null if not available. + } + } + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @apple_pay, options) + end.check_request do |_method, endpoint, data, _headers| + if /payment_intents/.match?(endpoint) + assert_match(/payment_method_options\[card\]\[stored_credential_transaction_type\]=setup_on_session/, data) + assert_match(/card\[eci\]=05/, data) + assert_match(/card\[cryptogram\]=dGVzdGNyeXB0b2dyYW1YWFhYWFhYWFhYWFg9PQ%3D%3D/, data) + end + end.respond_with(successful_create_intent_response_with_apple_pay_and_billing_address) + end + + def test_skip_network_token_cryptogram_and_eci_for_apple_pay_mit + options = { + currency: 'USD', + execute_threed: true, + confirm: true, + stored_credential_transaction_type: true, + stored_credential: { + initiator: 'merchant', + reason_type: 'recurring', + initial_transaction: false, + network_transaction_id: @network_transaction_id, + off_session: 'true' + } + } + + stub_comms(@gateway, :ssl_request) do + @gateway.create_intent(@amount, @apple_pay, options) + end.check_request do |_method, endpoint, data, _headers| + if /payment_intents/.match?(endpoint) + assert_match(/payment_method_options\[card\]\[stored_credential_transaction_type\]=stored_off_session_recurring/, data) + assert_not_match(/card\[eci\]/, data) + assert_not_match(/card\[cryptogram\]/, data) + end + end.respond_with(successful_verify_response) + end + private def successful_setup_purchase diff --git a/test/unit/gateways/stripe_test.rb b/test/unit/gateways/stripe_test.rb index 1ce0e52c96c..36af54cb72c 100644 --- a/test/unit/gateways/stripe_test.rb +++ b/test/unit/gateways/stripe_test.rb @@ -835,6 +835,19 @@ def test_declined_request assert_equal 'ch_test_charge', response.authorization end + def test_declined_request_returns_header_response + @gateway.instance_variable_set(:@response_headers, { 'idempotent-replayed' => 'true' }) + @gateway.expects(:ssl_request).returns(declined_purchase_response) + + assert response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + + assert_equal Gateway::STANDARD_ERROR_CODE[:card_declined], response.error_code + refute response.test? # unsuccessful request defaults to live + assert_equal 'ch_test_charge', response.authorization + assert response.params['response_headers']['idempotent_replayed'], 'true' + end + def test_declined_request_advanced_decline_codes @gateway.expects(:ssl_request).returns(declined_call_issuer_purchase_response) diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index e35ef845cfd..ae60b8d8d68 100644 --- a/test/unit/gateways/worldpay_test.rb +++ b/test/unit/gateways/worldpay_test.rb @@ -89,11 +89,7 @@ def setup invoice_reference_number: 'INV12233565', customer_reference: 'CUST00000101', card_acceptor_tax_id: 'VAT1999292', - sales_tax: { - amount: '20', - exponent: '2', - currency: 'USD' - }, + tax_amount: '20', ship_from_postal_code: '43245', destination_postal_code: '54545', destination_country_code: 'CO', @@ -101,8 +97,7 @@ def setup day_of_month: Date.today.day, month: Date.today.month, year: Date.today.year - }, - tax_exempt: 'false' + } } } @@ -110,57 +105,77 @@ def setup level_3_data: { customer_reference: 'CUST00000102', card_acceptor_tax_id: 'VAT1999285', - sales_tax: { - amount: '20', - exponent: '2', - currency: 'USD' - }, - discount_amount: { - amount: '1', - exponent: '2', - currency: 'USD' - }, - shipping_amount: { - amount: '50', - exponent: '2', - currency: 'USD' - }, - duty_amount: { - amount: '20', - exponent: '2', - currency: 'USD' - }, - item: { + tax_amount: '20', + discount_amount: '1', + shipping_amount: '50', + duty_amount: '20', + line_items: [{ description: 'Laptop 14', product_code: 'LP00125', commodity_code: 'COM00125', quantity: '2', - unit_cost: { - amount: '1500', - exponent: '2', - currency: 'USD' - }, + unit_cost: '1500', unit_of_measure: 'each', - item_total: { - amount: '3000', - exponent: '2', - currency: 'USD' - }, - item_total_with_tax: { - amount: '3500', - exponent: '2', - currency: 'USD' - }, - item_discount_amount: { - amount: '200', - exponent: '2', - currency: 'USD' - }, - tax_amount: { - amount: '500', - exponent: '2', - currency: 'USD' - } + item_discount_amount: '200', + discount_amount: '0', + tax_amount: '500', + total_amount: '4000' + }, + { + description: 'Laptop 15', + product_code: 'LP00120', + commodity_code: 'COM00125', + quantity: '2', + unit_cost: '1000', + unit_of_measure: 'each', + item_discount_amount: '200', + tax_amount: '500', + discount_amount: '0', + total_amount: '3000' + }] + } + } + + @aft_options = { + account_funding_transaction: true, + aft_type: 'A', + aft_payment_purpose: '01', + aft_sender_account_type: '02', + aft_sender_account_reference: '4111111111111112', + aft_sender_full_name: { + first: 'First', + middle: 'Middle', + last: 'Sender' + }, + aft_sender_funding_address: { + address1: '123 Sender St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Senderville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_account_type: '03', + aft_recipient_account_reference: '4111111111111111', + aft_recipient_full_name: { + first: 'First', + middle: 'Middle', + last: 'Recipient' + }, + aft_recipient_funding_address: { + address1: '123 Recipient St', + address2: 'Apt 1', + postal_code: '12345', + city: 'Recipientville', + state: 'NC', + country_code: 'US' + }, + aft_recipient_funding_data: { + telephone_number: '123456789', + birth_date: { + day_of_month: '01', + month: '01', + year: '1980' } } } @@ -442,12 +457,30 @@ def test_transaction_with_level_two_data assert_match %r(INV12233565), data assert_match %r(CUST00000101), data assert_match %r(VAT1999292), data - assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') assert_match %r(43245), data assert_match %r(54545), data assert_match %r(CO), data assert_match %r(false), data - assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + end.respond_with(successful_authorize_response) + assert_success response + end + + def test_transaction_with_level_two_data_without_tax + @level_two_data[:level_2_data][:tax_amount] = 0 + options = @options.merge(@level_two_data) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match %r(INV12233565), data + assert_match %r(CUST00000101), data + assert_match %r(VAT1999292), data + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(43245), data + assert_match %r(54545), data + assert_match %r(CO), data + assert_match %r(true), data assert_match %r(), data.gsub(/\s+/, '') end.respond_with(successful_authorize_response) assert_success response @@ -460,11 +493,11 @@ def test_transaction_with_level_three_data end.check_request do |_endpoint, data, _headers| assert_match %r(CUST00000102), data assert_match %r(VAT1999285), data - assert_match %r(), data.gsub(/\s+/, '') - assert_match %r(), data.gsub(/\s+/, '') - assert_match %r(), data.gsub(/\s+/, '') - assert_match %r(), data.gsub(/\s+/, '') - assert_match %r(Laptop14LP00125COM001252), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(), data.gsub(/\s+/, '') + assert_match %r(Laptop14LP00125COM001252eachLaptop15LP00120COM001252each), data.gsub(/\s+/, '') end.respond_with(successful_authorize_response) assert_success response end @@ -709,6 +742,16 @@ def test_successful_mastercard_credit assert_equal 'f25257d251b81fb1fd9c210973c941ff', response.authorization end + def test_successful_visa_account_funding_transaction + response = stub_comms do + @gateway.credit(@amount, @credit_card, @options.merge(@aft_options)) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + end.respond_with(successful_visa_credit_response) + assert_success response + assert_equal '3d4187536044bd39ad6a289c4339c41c', response.authorization + end + def test_description stub_comms do @gateway.authorize(@amount, @credit_card, @options) @@ -1189,6 +1232,10 @@ def test_transcript_scrubbing_on_network_token assert_equal network_token_transcript_scrubbed, @gateway.scrub(network_token_transcript) end + def test_transcript_scrubbing_on_aft + assert_equal aft_transcript_scrubbed, @gateway.scrub(aft_transcript) + end + def test_3ds_version_1_request stub_comms do @gateway.authorize(@amount, @credit_card, @options.merge(three_d_secure_option(version: '1.0.2', xid: 'xid'))) @@ -1525,6 +1572,22 @@ def test_order_id_crop_and_clean assert_success response end + def test_authorize_prefers_options_for_ntid + stored_credential_params = stored_credential(:used, :recurring, :merchant, network_transaction_id: '3812908490218390214124') + options = @options.merge( + stored_credential_transaction_id: '000000000000020005060720116005060' + ) + + options.merge!({ stored_credential: stored_credential_params }) + response = stub_comms do + @gateway.authorize(@amount, @credit_card, options) + end.check_request do |_endpoint, data, _headers| + assert_match(//, data) + assert_match(/000000000000020005060720116005060\<\/schemeTransactionIdentifier\>/, data) + end.respond_with(successful_authorize_response) + assert_success response + end + def test_successful_inquire_with_order_id response = stub_comms do @gateway.inquire(nil, { order_id: @options[:order_id].to_s }) @@ -2340,6 +2403,148 @@ def scrubbed_transcript TRANSCRIPT end + def aft_transcript + <<~TRANSCRIPT + + + + Account Funding Transaction + + + + 4111111111111111 + + + + Longbob Longsen + 123 + + + + wow@example.com + + + + + + + 01 + + 4111111111111112 + + First + Middle + Sender + + + 123 Sender St + Apt 1 + 12345 + Senderville + NC + US + + + + 4111111111111111 + + First + Middle + Recipient + + + 123 Recipient St + Apt 1 + 12345 + Recipientville + NC + US + + + + + + 123456789 + + + + + + + TRANSCRIPT + end + + def aft_transcript_scrubbed + <<~TRANSCRIPT + + + + Account Funding Transaction + + + + [FILTERED] + + + + Longbob Longsen + [FILTERED] + + + + wow@example.com + + + + + + + 01 + + [FILTERED] + + First + Middle + Sender + + + 123 Sender St + Apt 1 + 12345 + Senderville + NC + US + + + + [FILTERED] + + First + Middle + Recipient + + + 123 Recipient St + Apt 1 + 12345 + Recipientville + NC + US + + + + + + 123456789 + + + + + + + TRANSCRIPT + end + def network_token_transcript <<~RESPONSE @@ -2467,4 +2672,35 @@ def failed_store_response RESPONSE end + + def successful_aft_response + <<~RESPONSE + + + + + + + VISA_CREDIT-SSL + + AUTHORISED + + + + N/A + + + + 4111********1111 + + + 060720116005062 + + + + + + + RESPONSE + end end diff --git a/test/unit/posts_data_test.rb b/test/unit/posts_data_test.rb index 58de35f5d6c..81f5197c602 100644 --- a/test/unit/posts_data_test.rb +++ b/test/unit/posts_data_test.rb @@ -70,9 +70,13 @@ def test_setting_timeouts def test_setting_proxy_settings @gateway.class.proxy_address = 'http://proxy.com' @gateway.class.proxy_port = 1234 + @gateway.class.proxy_user = 'user' + @gateway.class.proxy_password = 'password' ActiveMerchant::Connection.any_instance.expects(:request).returns(@ok) ActiveMerchant::Connection.any_instance.expects(:proxy_address=).with('http://proxy.com') ActiveMerchant::Connection.any_instance.expects(:proxy_port=).with(1234) + ActiveMerchant::Connection.any_instance.expects(:proxy_user=).with('user') + ActiveMerchant::Connection.any_instance.expects(:proxy_password=).with('password') assert_nothing_raised do @gateway.ssl_post(@url, '')