diff --git a/CHANGELOG b/CHANGELOG index 38c8836c43d..bbab88f6888 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,12 +2,38 @@ = ActiveMerchant CHANGELOG == HEAD + += 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 diff --git a/activemerchant.gemspec b/activemerchant.gemspec index 78484f81232..115de333e9e 100644 --- a/activemerchant.gemspec +++ b/activemerchant.gemspec @@ -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/lib/active_merchant/billing/credit_card_methods.rb b/lib/active_merchant/billing/credit_card_methods.rb index 82508247f4c..619601cb2d3 100644 --- a/lib/active_merchant/billing/credit_card_methods.rb +++ b/lib/active_merchant/billing/credit_card_methods.rb @@ -128,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 @@ -175,7 +176,7 @@ module CreditCardMethods (501104..501105), (501107..501108), (501104..501105), - (501107..501108), + (501107..501109), (501800..501899), (502000..502099), (503800..503899), diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index 465be06170b..386669f18ee 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -755,10 +755,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) @@ -903,7 +927,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) 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 8a9782e3baf..42ea1ff9234 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 @@ -907,18 +912,10 @@ def add_stored_credential_data(parameters, credit_card_or_vault_id, options) return unless (stored_credential = options[:stored_credential]) add_external_vault(parameters, options) - - if options[:stored_credentials_v2] - stored_credentials_v2(parameters, stored_credential) - else - stored_credentials_v1(parameters, stored_credential) - end + 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] @@ -935,20 +932,6 @@ 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, options = {}) stored_credential = options[:stored_credential] parameters[:external_vault] = {} @@ -1053,7 +1036,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 f0c2fbec7fb..2a3d6c40fe9 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 @@ -146,6 +147,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) @@ -161,6 +164,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) @@ -193,6 +200,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) @@ -659,12 +680,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 @@ -694,11 +710,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..4ff3b68af15 100644 --- a/lib/active_merchant/billing/gateways/commerce_hub.rb +++ b/lib/active_merchant/billing/gateways/commerce_hub.rb @@ -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..11fd37be4c3 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: '058' } @@decision_codes = { @@ -609,12 +610,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 +873,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 +897,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 @@ -1104,7 +1120,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 +1133,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 8aa79675947..1914d3d93a8 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: '058' } WALLET_PAYMENT_SOLUTION = { 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_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/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index f7f5e678575..fa4892618da 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -51,7 +51,7 @@ 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) @@ -70,7 +70,7 @@ def authorize(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) @@ -86,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) @@ -133,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) @@ -146,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) @@ -159,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) @@ -172,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) @@ -195,12 +195,12 @@ def scrub(transcript) private def add_payment(xml, payment, options) - if payment.is_a?(String) - xml.ssl_token payment + 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) + add_creditcard(xml, payment, options) end end @@ -227,11 +227,11 @@ def add_network_token(xml, payment_method) end end - def add_creditcard(xml, creditcard) + 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) @@ -244,12 +244,12 @@ 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? + # Don't add cvv if this is a non-initial stored credential transaction + return if options[:stored_credential] && !options.dig(:stored_credential, :initial_transaction) && options[:stored_cred_v2] - 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 @@ -308,16 +308,20 @@ 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] + if options[:stored_cred_v2] + add_stored_credential_v2(xml, payment_method, options) + add_installment_fields(xml, options) + else + add_stored_credential(xml, options) + end end def add_custom_fields(xml, options) @@ -367,6 +371,8 @@ def add_line_items(xml, level_3_data) end def add_stored_credential(xml, options) + return unless options[:stored_credential] + network_transaction_id = options.dig(:stored_credential, :network_transaction_id) case when network_transaction_id.nil? @@ -382,14 +388,60 @@ def add_stored_credential(xml, options) end end + def add_stored_credential_v2(xml, payment_method, options) + return unless options[:stored_credential] + + network_transaction_id = 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' && merchant_reason_type(options) + end + + def merchant_reason_type(options) + if options[:stored_cred_v2] + options.dig(:stored_credential, :reason_type) == 'unscheduled' + else + options.dig(:stored_credential, :reason_type) == 'unscheduled' || options.dig(:stored_credential, :reason_type) == 'recurring' + end end - def entry_mode(options) + 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(payment_method, options) return options[:entry_mode] if options[:entry_mode] - return 12 if options[:stored_credential] + return 12 if options[:stored_credential] && options[:stored_cred_v2] != true + + 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 b3ff85061b1..3471711e72a 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -17,10 +17,12 @@ class FlexChargeGateway < Gateway sync: 'outcome', refund: 'orders/%s/refund', store: 'tokenize', - inquire: 'orders/%s' + 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 def initialize(options = {}) requires!(options, :app_key, :app_secret, :site_id, :mid) @@ -28,27 +30,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 +111,21 @@ def scrub(transcript) end def inquire(authorization, options = {}) - commit(:inquire, {}, authorization, :get) + 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 +150,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 +166,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? + + first_name, last_name = names_from_address(address, payment) - post[:billingInformation] = { + post[address_type] = { firstName: first_name, lastName: last_name, country: address[:country], @@ -165,27 +205,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 @@ -227,6 +271,8 @@ def headers end def parse(body) + body = '{}' if body.blank? + JSON.parse(body).with_indifferent_access rescue JSON::ParserError { @@ -253,7 +299,7 @@ def api_request(action, post, authorization = nil, method = :post) success_from(action, response), message_from(response), response, - authorization: authorization_from(action, response), + authorization: authorization_from(action, response, post), test: test?, error_code: error_code_from(action, response) ) @@ -271,6 +317,7 @@ 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 @@ -280,8 +327,12 @@ def message_from(response) response[:title] || response[:responseMessage] || response[:statusName] || response[:status] end - def authorization_from(action, response) - action == :store ? response.dig(:transaction, :payment_method, :token) : response[:orderId] + 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(action, response) 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/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..8d91472f0d6 100644 --- a/lib/active_merchant/billing/gateways/nmi.rb +++ b/lib/active_merchant/billing/gateways/nmi.rb @@ -134,6 +134,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 +167,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 +188,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 + post[:decrypted_googlepay_data] = 1 + else + post[:token_cryptogram] = payment_method.payment_cryptogram + end + end + def add_stored_credential(post, options) return unless (stored_credential = options[:stored_credential]) diff --git a/lib/active_merchant/billing/gateways/orbital.rb b/lib/active_merchant/billing/gateways/orbital.rb index 872fda576c7..f22303daa40 100644 --- a/lib/active_merchant/billing/gateways/orbital.rb +++ b/lib/active_merchant/billing/gateways/orbital.rb @@ -724,7 +724,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 +753,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/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 d0bf2448ffc..45793176b2b 100644 --- a/lib/active_merchant/billing/gateways/plexo.rb +++ b/lib/active_merchant/billing/gateways/plexo.rb @@ -196,19 +196,48 @@ 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, + Bin: get_last_eight_digits(payment.number), + Last4: get_last_four_digits(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 - post[:paymentMethod][:Card][:Cryptogram] = payment.payment_cryptogram if payment&.is_a?(NetworkTokenizationCreditCard) + end + + def get_last_eight_digits(number) + number[-8..-1] + end + + def get_last_four_digits(number) + number[-4..-1] end def add_card_holder(card, payment, options) diff --git a/lib/active_merchant/billing/gateways/rapyd.rb b/lib/active_merchant/billing/gateways/rapyd.rb index e99b8c10eb7..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 = '') 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/stripe.rb b/lib/active_merchant/billing/gateways/stripe.rb index 17bc8c5035c..579735cefe9 100644 --- a/lib/active_merchant/billing/gateways/stripe.rb +++ b/lib/active_merchant/billing/gateways/stripe.rb @@ -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..dc23627e858 100644 --- a/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +++ b/lib/active_merchant/billing/gateways/stripe_payment_intents.rb @@ -172,6 +172,7 @@ def create_setup_intent(payment_method, 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] @@ -531,7 +532,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] ||= {} diff --git a/lib/active_merchant/billing/gateways/worldpay.rb b/lib/active_merchant/billing/gateways/worldpay.rb index 1eac5a69b0d..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 @@ -400,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 diff --git a/lib/active_merchant/version.rb b/lib/active_merchant/version.rb index 2a2845e4371..0f14bccd30b 100644 --- a/lib/active_merchant/version.rb +++ b/lib/active_merchant/version.rb @@ -1,3 +1,3 @@ module ActiveMerchant - VERSION = '1.136.0' + VERSION = '1.137.0' end diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 859dacf1288..8360a77a009 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 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 ffc628ad7ea..f28c04d1cf5 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') @@ -664,6 +665,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: { @@ -1132,4 +1165,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_commerce_hub_test.rb b/test/remote/gateways/remote_commerce_hub_test.rb index d52c1d93a29..d3660e2b49c 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) @@ -203,7 +246,7 @@ def test_successful_authorize_and_void def test_failed_void response = @gateway.void('123', @options) assert_failure response - assert_equal 'Referenced transaction is invalid or not found', response.message + assert_equal 'Invalid primary transaction ID or not found', response.message end def test_successful_verify @@ -256,7 +299,7 @@ def test_successful_purchase_and_partial_refund def test_failed_refund response = @gateway.refund(nil, 'abc123|123', @options) assert_failure response - assert_equal 'Referenced transaction is invalid or not found', response.message + assert_equal 'Invalid primary transaction ID or not found', response.message end def test_successful_credit 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_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 1ff2469ca47..a458a67cf6d 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -96,6 +96,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 +133,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 +196,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 diff --git a/test/remote/gateways/remote_d_local_test.rb b/test/remote/gateways/remote_d_local_test.rb index c46b6aeae6c..7a2ff15f34a 100644 --- a/test/remote/gateways/remote_d_local_test.rb +++ b/test/remote/gateways/remote_d_local_test.rb @@ -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_elavon_test.rb b/test/remote/gateways/remote_elavon_test.rb index 6ad3e3c6097..2be919c0efd 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,184 @@ 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_cred_v2: true, + 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_cred_v2: true, + 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 = { + stored_cred_v2: true, + 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 +425,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 +486,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' })) 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 fd2ce646c94..d38cff1a7b6 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( @@ -111,7 +112,34 @@ def test_successful_purchase_mit set_credentials! response = @gateway.purchase(@amount, @credit_card_mit, @options) assert_success response - assert_equal 'SUBMITTED', response.message + assert_equal 'APPROVED', response.message + end + + 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 @@ -139,6 +167,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 +212,15 @@ def test_successful_purchase_with_token end def test_successful_inquire_request + @cit_options[:phone] = '998888' set_credentials! - response = @gateway.inquire('abe573e3-7567-4cc6-a7a4-02766dbd881a', {}) + + 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_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_nmi_test.rb b/test/remote/gateways/remote_nmi_test.rb index bd77940790b..ad1f80d48ce 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, @@ -130,17 +141,33 @@ 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_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 @@ -482,12 +509,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_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_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 88f70b20de6..69cda009ecf 100644 --- a/test/remote/gateways/remote_plexo_test.rb +++ b/test/remote/gateways/remote_plexo_test.rb @@ -24,7 +24,8 @@ def setup }, identification_type: '1', identification_value: '123456', - billing_address: address + billing_address: address, + invoice_number: '12345abcde' } @cancel_options = { @@ -41,10 +42,22 @@ def setup 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, @network_token_credit_card, @options.merge({ invoice_number: '12345abcde' })) + response = @gateway.purchase(@amount, @decrypted_network_token, @options.merge({ invoice_number: '12345abcde' })) assert_success response assert_equal 'You have been mocked.', response.message end 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_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb index fe2769e5115..2b7d5fa2120 100644 --- a/test/remote/gateways/remote_stripe_payment_intents_test.rb +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -380,6 +380,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 +681,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, { diff --git a/test/remote/gateways/remote_stripe_test.rb b/test/remote/gateways/remote_stripe_test.rb index 85417f3c583..90f0323ecf9 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) diff --git a/test/remote/gateways/remote_worldpay_test.rb b/test/remote/gateways/remote_worldpay_test.rb index 07540d478e7..f74e0372ea1 100644 --- a/test/remote/gateways/remote_worldpay_test.rb +++ b/test/remote/gateways/remote_worldpay_test.rb @@ -147,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 @@ -921,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/credit_card_methods_test.rb b/test/unit/credit_card_methods_test.rb index 0b0690bf248..735d512d708 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 diff --git a/test/unit/gateways/adyen_test.rb b/test/unit/gateways/adyen_test.rb index 28a766f6ca6..f8bf9dff8ec 100644 --- a/test/unit/gateways/adyen_test.rb +++ b/test/unit/gateways/adyen_test.rb @@ -262,6 +262,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,6 +360,16 @@ 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_fraud_raw_refusal + @gateway.expects(:ssl_post).returns(failed_fraud_visa_response) + + response = @gateway.send(:commit, 'authorise', {}, {}) + + assert_equal 'N7', response.error_code assert_failure response end @@ -359,6 +379,7 @@ def test_failed_authorise_mastercard response = @gateway.send(:commit, 'authorise', {}, {}) assert_equal 'Refused | 01 : New account information available', response.message + assert_equal '01', response.error_code assert_failure response end @@ -1916,6 +1937,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 be9edb8ffc0..c620e68ee62 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -1165,7 +1165,7 @@ def test_stored_credential_recurring_cit_initial external_vault: { status: 'will_vault' }, - transaction_source: '' + transaction_source: 'recurring_first' } ) ).returns(braintree_result) @@ -1181,7 +1181,7 @@ def test_stored_credential_recurring_cit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: '' + transaction_source: 'recurring' } ) ).returns(braintree_result) @@ -1197,7 +1197,7 @@ def test_stored_credential_prefers_options_for_ntid status: 'vaulted', previous_network_transaction_id: '321XYZ' }, - transaction_source: '' + transaction_source: 'recurring' } ) ).returns(braintree_result) @@ -1212,7 +1212,7 @@ def test_stored_credential_recurring_mit_initial external_vault: { status: 'will_vault' }, - transaction_source: 'recurring' + transaction_source: 'recurring_first' } ) ).returns(braintree_result) @@ -1243,7 +1243,7 @@ def test_stored_credential_installment_cit_initial external_vault: { status: 'will_vault' }, - transaction_source: '' + transaction_source: 'installment_first' } ) ).returns(braintree_result) @@ -1259,7 +1259,7 @@ def test_stored_credential_installment_cit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: '' + transaction_source: 'installment' } ) ).returns(braintree_result) @@ -1274,7 +1274,7 @@ def test_stored_credential_installment_mit_initial external_vault: { status: 'will_vault' }, - transaction_source: 'recurring' + transaction_source: 'installment_first' } ) ).returns(braintree_result) @@ -1290,7 +1290,7 @@ def test_stored_credential_installment_mit_used status: 'vaulted', previous_network_transaction_id: '123ABC' }, - transaction_source: 'recurring' + transaction_source: 'installment' } ) ).returns(braintree_result) @@ -1387,7 +1387,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 @@ -1417,7 +1417,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 @@ -1433,7 +1433,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 @@ -1448,7 +1448,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 @@ -1464,7 +1464,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 @@ -1479,7 +1479,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 @@ -1494,7 +1494,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 diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index fd6ef6bd431..4617184c30e 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -86,6 +86,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 +431,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 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 ba3c6af8af7..8bed7a21d2c 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', @@ -586,6 +587,15 @@ def test_purchase_with_level_3_data 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 '058', 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..84da17c7964 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', @@ -248,11 +249,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 @@ -1112,7 +1117,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 +1298,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 +1321,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 +1347,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 +1366,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 +1389,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 +1415,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 +1434,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 +1994,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(/058<\/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/elavon_test.rb b/test/unit/gateways/elavon_test.rb index 49f8ca8c207..760d210e927 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -26,6 +26,7 @@ def setup @credit_card = credit_card @amount = 100 + @stored_card = '4421912014039990' @options = { order_id: '1', @@ -45,9 +46,12 @@ def setup end def test_successful_purchase - @gateway.expects(:ssl_post).returns(successful_purchase_response) + response = stub_comms do + @gateway.purchase(@amount, @credit_card, @options.merge!(stored_cred_v2: true)) + 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? @@ -182,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) @@ -386,10 +400,276 @@ 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) + refute_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 @@ -408,19 +688,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 4d0acc69b80..09683bc59f3 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 = { @@ -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,23 +119,38 @@ 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(@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 @@ -162,6 +183,27 @@ 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(@gateway, :ssl_request) do @gateway.refund(@amount, 'reference', @options) @@ -186,17 +228,25 @@ def test_scrub 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 @@ -218,6 +268,44 @@ def test_successful_inquire_request 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 diff --git a/test/unit/gateways/litle_test.rb b/test/unit/gateways/litle_test.rb index 4af53261b61..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( @@ -364,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) 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..5defed1713e 100644 --- a/test/unit/gateways/mercury_test.rb +++ b/test/unit/gateways/mercury_test.rb @@ -126,7 +126,7 @@ def test_transcript_scrubbing def successful_purchase_response <<~RESPONSE - + Processor @@ -163,7 +163,7 @@ def successful_purchase_response def failed_purchase_response <<~RESPONSE - + Server @@ -179,7 +179,7 @@ def failed_purchase_response def successful_refund_response <<~RESPONSE - + Processor 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..adf79d0c1e6 100644 --- a/test/unit/gateways/nmi_test.rb +++ b/test/unit/gateways/nmi_test.rb @@ -29,6 +29,52 @@ 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) + 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) + end.respond_with(successful_purchase_response) end def test_successful_authorize_and_capture_using_security_key diff --git a/test/unit/gateways/orbital_test.rb b/test/unit/gateways/orbital_test.rb index 150cc874ac1..e46959124ab 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=', @@ -1316,6 +1326,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 a673239ce48..c2a63cc713c 100644 --- a/test/unit/gateways/plexo_test.rb +++ b/test/unit/gateways/plexo_test.rb @@ -346,9 +346,9 @@ def test_purchase_with_network_token assert_equal request['Amount']['Currency'], 'UYU' assert_equal request['Amount']['Details']['TipAmount'], '5' assert_equal request['Flow'], 'direct' - assert_equal @network_token_credit_card.number, request['paymentMethod']['Card']['Number'] - assert_equal @network_token_credit_card.payment_cryptogram, request['paymentMethod']['Card']['Cryptogram'] - assert_equal @network_token_credit_card.first_name, request['paymentMethod']['Card']['Cardholder']['FirstName'] + 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'] end.respond_with(successful_network_token_response) assert_success purchase 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/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index cf80a066e9e..989b19096b3 100644 --- a/test/unit/gateways/stripe_payment_intents_test.rb +++ b/test/unit/gateways/stripe_payment_intents_test.rb @@ -315,6 +315,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) @@ -926,6 +935,16 @@ 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 + 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/trans_first_test.rb b/test/unit/gateways/trans_first_test.rb index d8a2ca93ed2..94b5fdeff38 100644 --- a/test/unit/gateways/trans_first_test.rb +++ b/test/unit/gateways/trans_first_test.rb @@ -15,16 +15,6 @@ def setup @amount = 100 end - def test_missing_field_response - @gateway.stubs(:ssl_post).returns(missing_field_response) - - response = @gateway.purchase(@amount, @credit_card, @options) - - assert_failure response - assert response.test? - assert_equal 'Missing parameter: UserId.', response.message - end - def test_successful_purchase @gateway.stubs(:ssl_post).returns(successful_purchase_response) diff --git a/test/unit/gateways/worldpay_test.rb b/test/unit/gateways/worldpay_test.rb index 9a12ae35728..ae60b8d8d68 100644 --- a/test/unit/gateways/worldpay_test.rb +++ b/test/unit/gateways/worldpay_test.rb @@ -135,6 +135,50 @@ def setup }] } } + + @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_payment_type_for_network_card @@ -698,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) @@ -1178,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'))) @@ -2345,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 @@ -2472,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