From e282efb447bf20688254301fda0534d6201f97f1 Mon Sep 17 00:00:00 2001 From: Dustin A Haefele <45601251+DustinHaefele@users.noreply.github.com> Date: Wed, 12 Jun 2024 13:54:52 -0400 Subject: [PATCH 01/46] Update the error_code_from method to grab and alpha_numeric characters (#5133) Spreedly ref: ECS-3536 --- lib/active_merchant/billing/gateways/adyen.rb | 5 +++- test/unit/gateways/adyen_test.rb | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index 465be06170b..648cb4299a8 100644 --- a/lib/active_merchant/billing/gateways/adyen.rb +++ b/lib/active_merchant/billing/gateways/adyen.rb @@ -903,7 +903,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/test/unit/gateways/adyen_test.rb b/test/unit/gateways/adyen_test.rb index 28a766f6ca6..e673cc9251b 100644 --- a/test/unit/gateways/adyen_test.rb +++ b/test/unit/gateways/adyen_test.rb @@ -350,6 +350,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 +369,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 +1927,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 { From 32e4da3ac83c6e366580237f2db8bcf80e65f10e Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Tue, 4 Jun 2024 16:35:38 -0700 Subject: [PATCH 02/46] Braintree Blue: add graceul failure if zipcode is not present --- CHANGELOG | 1 + .../billing/gateways/braintree_blue.rb | 13 +++-- .../gateways/remote_braintree_blue_test.rb | 48 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 38c8836c43d..9438e5336d4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 8a9782e3baf..82e21dc9959 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 diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 859dacf1288..d36f3187731 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'] From 23169a520c4719554851f35fa59b1b1b3821d89e Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Thu, 6 Jun 2024 15:13:14 -0700 Subject: [PATCH 03/46] DLocal: update the zip and ip fields --- CHANGELOG | 1 + lib/active_merchant/billing/gateways/d_local.rb | 6 ++++-- test/remote/gateways/remote_d_local_test.rb | 6 ++++++ test/unit/gateways/d_local_test.rb | 9 +++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9438e5336d4..24a5c4c0965 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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/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) From 5bd880fe194457b3f9edbad078373e3fb9c66fe6 Mon Sep 17 00:00:00 2001 From: Jhoan Buitrago <57675446+Buitragox@users.noreply.github.com> Date: Tue, 18 Jun 2024 12:46:14 -0500 Subject: [PATCH 04/46] Litle: Add tests for network tokenization (#5145) Summary: ------------------------------ Add unit and remote tests for network token transactions [SER-1270](https://spreedly.atlassian.net/browse/SER-1270) Remote Test: ------------------------------ Finished in 88.332434 seconds. 60 tests, 261 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Unit Test: ------------------------------ Finished in 45.844644 seconds. 5931 tests, 79847 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- test/remote/gateways/remote_litle_test.rb | 35 +++++++++++++++++++++++ test/unit/gateways/litle_test.rb | 18 ++++++++++++ 2 files changed, 53 insertions(+) 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/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) From b636002521948a0058c622650c1b450604d792e6 Mon Sep 17 00:00:00 2001 From: Luis Felipe Angulo Torres <42988115+pipe2442@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:45:41 -0500 Subject: [PATCH 05/46] Datatrans: Add support for verify transactions (#5148) SER-1302 Description ------------------------- Add support to make verify transactions with authorize and void using a multiresponse thread Unit test ------------------------- 25 tests, 136 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote test ------------------------- 23 tests, 57 assertions, 2 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 90.9091% passed Rubocop ------------------------- 798 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/datatrans.rb | 7 +++++++ test/remote/gateways/remote_datatrans_test.rb | 10 ++++++++++ test/unit/gateways/datatrans_test.rb | 11 +++++++++++ 3 files changed, 28 insertions(+) diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb index 6d1a3c686d9..5f7cbd87cf5 100644 --- a/lib/active_merchant/billing/gateways/datatrans.rb +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -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) diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb index 43d74f755ed..1fce6e5e239 100644 --- a/test/remote/gateways/remote_datatrans_test.rb +++ b/test/remote/gateways/remote_datatrans_test.rb @@ -195,6 +195,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) diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb index 532cea6b645..951cf37bfe0 100644 --- a/test/unit/gateways/datatrans_test.rb +++ b/test/unit/gateways/datatrans_test.rb @@ -98,6 +98,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) From d31c20c2b4a199fb7a1fe7d73c5c198071a0bf43 Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Thu, 20 Jun 2024 16:03:51 -0700 Subject: [PATCH 06/46] Checkout V2: add support for risk data fields --- CHANGELOG | 1 + .../billing/gateways/checkout_v2.rb | 15 +++++++++ .../gateways/remote_checkout_v2_test.rb | 32 +++++++++++++++++++ test/unit/gateways/checkout_v2_test.rb | 15 +++++++++ 4 files changed, 63 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 24a5c4c0965..fa0facdae9e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index 28cd1014a0c..94994025a67 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -144,6 +144,7 @@ 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) end def add_invoice(post, money, options) @@ -191,6 +192,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) diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index 4347b82842c..fde680a0d79 100644 --- a/test/remote/gateways/remote_checkout_v2_test.rb +++ b/test/remote/gateways/remote_checkout_v2_test.rb @@ -650,6 +650,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: { diff --git a/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index 1fcc42989e2..a39f9ca6359 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' }) From 94377a3b540e2f4b3b7564870b9b71886a4624b3 Mon Sep 17 00:00:00 2001 From: Huda <18461096+hudakh@users.noreply.github.com> Date: Mon, 24 Jun 2024 23:39:29 +0930 Subject: [PATCH 07/46] Pin: Add new 3DS params mentioned in Pin Payments docs (#4720) Co-authored-by: Huda --- CHANGELOG | 1 + lib/active_merchant/billing/gateways/pin.rb | 21 +++++++++++++++++---- test/remote/gateways/remote_pin_test.rb | 20 +++++++++++++++++++- test/unit/gateways/pin_test.rb | 14 ++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fa0facdae9e..67ae19dae1b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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/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)) From 80c3cb503ff42327e7a54eabb981c79b7f231bff Mon Sep 17 00:00:00 2001 From: Johan Herrera Date: Wed, 22 May 2024 15:39:01 -0500 Subject: [PATCH 08/46] RedsysRest: Add support for stored credentials & 3DS exemptions [ECS-3450](https://spreedly.atlassian.net/browse/ECS-3450) This PR updates adds stored credendials and 3ds exemptions for redsys rest Unit tests ---------------- Finished in 0.023518 seconds. 28 tests, 118 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote tests ---------------- Finished in 33.326868 seconds. 26 tests, 93 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 96.1538% passed 0.78 tests/s, 2.79 assertions/s -> failure not related to changes --- CHANGELOG | 1 + .../billing/gateways/redsys_rest.rb | 43 ++++++++- .../gateways/remote_redsys_rest_test.rb | 89 +++++++++++++++---- test/unit/gateways/redsys_rest_test.rb | 46 ++++++++++ 4 files changed, 161 insertions(+), 18 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 67ae19dae1b..1027cba5134 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/redsys_rest.rb b/lib/active_merchant/billing/gateways/redsys_rest.rb index 3e8de87ed68..8cf8e8d33c5 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,34 @@ 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/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/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) From 3805b4b70b4081f2c85617d1a8baee2c38cc8a89 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Tue, 25 Jun 2024 14:12:12 -0500 Subject: [PATCH 09/46] Fix rubocop error --- .../billing/gateways/redsys_rest.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/active_merchant/billing/gateways/redsys_rest.rb b/lib/active_merchant/billing/gateways/redsys_rest.rb index 8cf8e8d33c5..60f268a6ea0 100644 --- a/lib/active_merchant/billing/gateways/redsys_rest.rb +++ b/lib/active_merchant/billing/gateways/redsys_rest.rb @@ -397,13 +397,14 @@ def add_stored_credentials(post, options) 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 + 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 From 2aa3c3243377939bd2804a535cfd3299eb21c406 Mon Sep 17 00:00:00 2001 From: Luis Felipe Angulo Torres <42988115+pipe2442@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:48:02 -0500 Subject: [PATCH 10/46] Datatrans: Fix InvalidCountryCodeError (#5156) Description ------------------------- SER-1323 Unit test ------------------------- 25 tests, 136 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote test ------------------------- 23 tests, 57 assertions, 2 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 90.9091% passed --- lib/active_merchant/billing/gateways/datatrans.rb | 8 +++++++- test/remote/gateways/remote_datatrans_test.rb | 7 +++++++ test/unit/gateways/datatrans_test.rb | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb index 5f7cbd87cf5..26ba761d8f7 100644 --- a/lib/active_merchant/billing/gateways/datatrans.rb +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -131,6 +131,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] @@ -139,7 +145,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] diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb index 1fce6e5e239..87a87484ea2 100644 --- a/test/remote/gateways/remote_datatrans_test.rb +++ b/test/remote/gateways/remote_datatrans_test.rb @@ -30,6 +30,7 @@ def setup } @billing_address = address + @no_country_billing_address = address(country: nil) @google_pay_card = network_tokenization_credit_card( '4900000000000094', @@ -221,6 +222,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/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb index 951cf37bfe0..cbe2316738f 100644 --- a/test/unit/gateways/datatrans_test.rb +++ b/test/unit/gateways/datatrans_test.rb @@ -30,6 +30,7 @@ def setup @transaction_reference = '240214093712238757|093712' @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) From 819907d534eb1a4c433d07952556e2ea5ca0d7c2 Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Mon, 24 Jun 2024 16:35:26 -0700 Subject: [PATCH 11/46] CheckoutV2: truncate the reference id for amex transactions --- CHANGELOG | 1 + .../billing/gateways/checkout_v2.rb | 6 +++++ .../gateways/remote_checkout_v2_test.rb | 24 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1027cba5134..63e9163dcd1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index 94994025a67..d5112d0c5e1 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 @@ -145,6 +146,7 @@ def build_auth_or_purchase(post, amount, payment_method, 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) @@ -160,6 +162,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) diff --git a/test/remote/gateways/remote_checkout_v2_test.rb b/test/remote/gateways/remote_checkout_v2_test.rb index fde680a0d79..fa0b1a6dc9c 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') @@ -1144,4 +1145,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 From 17b9ff84a6224e677379a58f39c729ac06f5e09e Mon Sep 17 00:00:00 2001 From: David Perry Date: Fri, 28 Jun 2024 13:33:05 -0400 Subject: [PATCH 12/46] Worldpay: Support AFTs (#5154) Account Funding Transactions, which Worldpay refers to as "Pull from Card" https://staging-developer.fisglobal.com/apis/wpg/manage/pull-from-card Remote: (10 unrelated failures on master) 110 tests, 443 assertions, 10 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 90.9091% passed Unit: 124 tests, 698 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed ECS-3554 --- .../billing/gateways/worldpay.rb | 69 +++++- test/remote/gateways/remote_worldpay_test.rb | 72 ++++++ test/unit/gateways/worldpay_test.rb | 231 ++++++++++++++++++ 3 files changed, 371 insertions(+), 1 deletion(-) 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/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/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 From a26afd1110fa18f16a6a8cb94c205d95a5326fec Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Thu, 27 Jun 2024 15:43:06 -0700 Subject: [PATCH 13/46] CommerceHub: Add billing address name overide --- CHANGELOG | 1 + .../billing/gateways/commerce_hub.rb | 22 +++++++-- .../gateways/remote_commerce_hub_test.rb | 47 ++++++++++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 63e9163dcd1..bb1b42861ef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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 From d6b450082775dcead08203fc10e50bc2e605d75a Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Mon, 1 Jul 2024 13:54:00 -0700 Subject: [PATCH 14/46] Stripe PI: add optional ability for 3DS exemption on verify calls --- CHANGELOG | 1 + .../gateways/stripe_payment_intents.rb | 3 ++- .../remote_stripe_payment_intents_test.rb | 24 +++++++++++++++++++ .../gateways/stripe_payment_intents_test.rb | 10 ++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bb1b42861ef..d4aed2782a1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/test/remote/gateways/remote_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb index fe2769e5115..730f00dd1cb 100644 --- a/test/remote/gateways/remote_stripe_payment_intents_test.rb +++ b/test/remote/gateways/remote_stripe_payment_intents_test.rb @@ -670,6 +670,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/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index cf80a066e9e..241977f0550 100644 --- a/test/unit/gateways/stripe_payment_intents_test.rb +++ b/test/unit/gateways/stripe_payment_intents_test.rb @@ -926,6 +926,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 From c5a4d220e69ecaa9ce20b61b887a00b9e5c5cf3a Mon Sep 17 00:00:00 2001 From: Luis Urrea Date: Wed, 29 May 2024 09:45:54 -0500 Subject: [PATCH 15/46] CyberSource: Update Stored Credential flow Always send commerceIndicator if stored credential reason_type is present. Update the value send for NetworkTokens commerceIndicator based on if they are using stored credentials. Spreedly reference: [ECS-3532](https://spreedly.atlassian.net/browse/ECS-3532) Unit tests Finished in 26.745895 seconds. 5912 tests, 79756 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop 798 files inspected, no offenses detected --- CHANGELOG | 1 + .../billing/gateways/cyber_source.rb | 23 ++++++++++++------- .../gateways/remote_cyber_source_test.rb | 2 +- test/unit/gateways/cyber_source_test.rb | 20 ++++++++-------- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d4aed2782a1..329ae68e72c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,6 +16,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index 78cc67b7d5d..e5e20d123e5 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -860,13 +860,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 +884,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 +1107,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 +1120,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/test/remote/gateways/remote_cyber_source_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 1ff2469ca47..8d3137f138f 100644 --- a/test/remote/gateways/remote_cyber_source_test.rb +++ b/test/remote/gateways/remote_cyber_source_test.rb @@ -190,7 +190,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/unit/gateways/cyber_source_test.rb b/test/unit/gateways/cyber_source_test.rb index 4b6fb1c914a..7cc77598e51 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -1112,7 +1112,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 +1293,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 +1316,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 +1342,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 +1361,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 +1384,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 +1410,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 +1429,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 From 3274d5b5ec78afbe0c11d82be02ccf0b8b8ff3f2 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Mon, 24 Jun 2024 15:51:46 -0500 Subject: [PATCH 16/46] Orbital: Update to accept UCAF Indicator GSF If alternate_ucaf_flow is true and the eci value is 4, 6, or 7 then send the ucaf passed in by customer. If alternate_ucaf_flow is not passed then only send ucaf if provided by customer if not pass default of 4. Remote: 134 tests, 543 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Unit: 152 tests, 892 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + .../billing/gateways/orbital.rb | 12 +++- test/remote/gateways/remote_orbital_test.rb | 50 +++++++------- test/unit/gateways/orbital_test.rb | 65 +++++++++++++++++++ 4 files changed, 97 insertions(+), 31 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 329ae68e72c..2115b4a0fff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,6 +17,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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/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) From b72e61f9af32a78d0fe6465227f1afbc07e9c98c Mon Sep 17 00:00:00 2001 From: cristian Date: Tue, 2 Jul 2024 15:11:27 -0500 Subject: [PATCH 17/46] FlexCharge: Adding authorize-capture functionality Summary: ------------------------------ Changes FlexCharge to add support for the authorize and capture operations and also a fix a bug on the `address_names` method [SER-1337](https://spreedly.atlassian.net/browse/SER-1337) [SER-1324](https://spreedly.atlassian.net/browse/SER-1324) Remote Test: ------------------------------ Finished in 72.283834 seconds. 19 tests, 54 assertions, 1 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 94.4444% passed Unit Tests: ------------------------------ Finished in 43.11362 seconds. 5944 tests, 79908 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- .../billing/gateways/flex_charge.rb | 68 ++++++++++++++----- .../gateways/remote_flex_charge_test.rb | 39 ++++++++++- test/unit/gateways/flex_charge_test.rb | 58 ++++++++++++++-- 3 files changed, 141 insertions(+), 24 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index b3ff85061b1..527b975c957 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -17,10 +17,11 @@ class FlexChargeGateway < Gateway sync: 'outcome', refund: 'orders/%s/refund', store: 'tokenize', - inquire: 'orders/%s' + inquire: 'orders/%s', + capture: 'capture' } - 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 +29,50 @@ def initialize(options = {}) end def purchase(money, credit_card, options = {}) + options[:transactionType] ||= 'Purchase' + post = {} - address = options[:billing_address] || options[:address] 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)) add_customer_data(post, options) add_three_ds(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(money, authorization, options = {}) + refund(money, authorization, options) 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 +108,16 @@ def scrub(transcript) end def inquire(authorization, options = {}) - commit(:inquire, {}, authorization, :get) + order_id, _currency = authorization.split('#') + commit(:inquire, {}, order_id, :get) 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] @@ -129,7 +158,7 @@ def add_customer_data(post, options) end def add_address(post, payment, address) - first_name, last_name = address_names(address[:name], payment) + first_name, last_name = names_from_address(address, payment) post[:billingInformation] = { firstName: first_name, @@ -157,6 +186,7 @@ def add_invoice(post, money, credit_card, options) avsResultCode: options[:avs_result_code], cvvResultCode: options[:cvv_result_code], cavvResultCode: options[:cavv_result_code], + transactionType: options[:transactionType], cardNotPresent: credit_card.is_a?(String) ? false : credit_card.verification_value.blank? }.compact end @@ -182,10 +212,10 @@ def add_payment_method(post, credit_card, address, options) 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 @@ -253,7 +283,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) ) @@ -280,8 +310,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/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index fd2ce646c94..9a9edd244f5 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -111,7 +111,26 @@ 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_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 +158,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(@amount, 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 +203,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/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 4d0acc69b80..bea51350be7 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -113,6 +113,7 @@ 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['transaction']['transactionType'], 'Purchase' assert_equal request['payer']['email'], @options[:email] assert_equal request['description'], @options[:description] end @@ -120,16 +121,25 @@ def test_successful_purchase 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['transaction']['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 @@ -186,17 +196,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 +236,36 @@ 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 + private def pre_scrubbed From 00b2ae74a9f9790a65ae53fd9d1b020005037c54 Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Mon, 1 Jul 2024 16:37:09 -0700 Subject: [PATCH 18/46] CyberSource: Add the merchant_descriptor_city and submerchant_id fields --- CHANGELOG | 1 + .../billing/gateways/cyber_source.rb | 14 +++++++++++++- test/remote/gateways/remote_cyber_source_test.rb | 6 ++++++ test/unit/gateways/cyber_source_test.rb | 6 +++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2115b4a0fff..55df5dd5f06 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index e5e20d123e5..0e28c43e100 100644 --- a/lib/active_merchant/billing/gateways/cyber_source.rb +++ b/lib/active_merchant/billing/gateways/cyber_source.rb @@ -609,12 +609,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 diff --git a/test/remote/gateways/remote_cyber_source_test.rb b/test/remote/gateways/remote_cyber_source_test.rb index 8d3137f138f..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) diff --git a/test/unit/gateways/cyber_source_test.rb b/test/unit/gateways/cyber_source_test.rb index 7cc77598e51..fc06e54454b 100644 --- a/test/unit/gateways/cyber_source_test.rb +++ b/test/unit/gateways/cyber_source_test.rb @@ -248,11 +248,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 From 83bb31d2c449bb45fddd28e02744d3d10110da80 Mon Sep 17 00:00:00 2001 From: cristian Date: Tue, 9 Jul 2024 10:41:05 -0500 Subject: [PATCH 19/46] FlexCharge: Enabling void call Summary: ------------------------------ Changes FlexCharge to add support for the void operation. [SER-1327](https://spreedly.atlassian.net/browse/SER-1327) Remote Test: ------------------------------ Finished in 70.477035 seconds. 19 tests, 54 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 60.782586 seconds. 5956 tests, 79948 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/flex_charge.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index 527b975c957..a23b821d071 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -18,7 +18,8 @@ class FlexChargeGateway < Gateway refund: 'orders/%s/refund', store: 'tokenize', inquire: 'orders/%s', - capture: 'capture' + capture: 'capture', + void: 'orders/%s/cancel' } SUCCESS_MESSAGES = %w(APPROVED CHALLENGE SUBMITTED SUCCESS PROCESSING CAPTUREREQUIRED).freeze @@ -68,7 +69,8 @@ def refund(money, authorization, options = {}) end def void(money, authorization, options = {}) - refund(money, authorization, options) + order_id, _currency = authorization.split('#') + commit(:void, {}, order_id) end def store(credit_card, options = {}) @@ -257,6 +259,8 @@ def headers end def parse(body) + body = '{}' if body.blank? + JSON.parse(body).with_indifferent_access rescue JSON::ParserError { @@ -301,6 +305,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 From b3e12ee9452d31d153603a026814e60c79efc70e Mon Sep 17 00:00:00 2001 From: Rachel Kirk Date: Tue, 9 Jul 2024 11:29:53 -0400 Subject: [PATCH 20/46] CyberSource: bugfix - send correct card type/code for carnet CER-1567 Sends code '058' for carnet card type in legacy CyberSource and CyberSource REST gateways --- lib/active_merchant/billing/gateways/cyber_source.rb | 3 ++- .../billing/gateways/cyber_source_rest.rb | 3 ++- test/unit/gateways/cyber_source_rest_test.rb | 10 ++++++++++ test/unit/gateways/cyber_source_test.rb | 9 +++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source.rb b/lib/active_merchant/billing/gateways/cyber_source.rb index 0e28c43e100..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 = { 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/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 fc06e54454b..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', @@ -1993,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( From 6f024c90129a11d5fd514d8036d38db5f97df761 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Fri, 28 Jun 2024 14:24:56 -0500 Subject: [PATCH 21/46] MerchantWarrior: Update phone, email, ip and store_ID Updae customerPhone to grab phone from phone or phone_number in options. If address doesn't contain ip and email then grab them directly from options. Add new field called storeID. Unit: 29 tests, 164 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 20 tests, 106 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + .../billing/gateways/merchant_warrior.rb | 8 ++-- test/unit/gateways/merchant_warrior_test.rb | 43 +++++++++++++++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 55df5dd5f06..a87c4a88e4c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,6 +19,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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 From 4f349333f7d6e39420ada437bcf521abd2501cc8 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Mon, 1 Jul 2024 15:15:05 -0500 Subject: [PATCH 22/46] Credorax: Update 3DS version mapping Update so that if a 3DS version starts with 2 it doesn't default to 2.0 but on what is passed in for the three_ds_version field. Unit: 82 tests, 394 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 51 tests, 173 assertions, 6 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 88.2353% passed --- CHANGELOG | 1 + .../billing/gateways/credorax.rb | 2 +- test/remote/gateways/remote_credorax_test.rb | 22 ++++++++++++++----- test/unit/gateways/credorax_test.rb | 4 ++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a87c4a88e4c..2151510045d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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/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 From 0ae0158ae8425ce6a479bc24debd171a385943f9 Mon Sep 17 00:00:00 2001 From: Luis Felipe Angulo Torres <42988115+pipe2442@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:13:45 -0500 Subject: [PATCH 23/46] FlexCharge - NoMethodError: nil CreditCard#number (#5164) Description ------------------------- SER-1339 Unit test ------------------------- 17 tests, 81 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote test ------------------------- 5923 tests, 79804 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected Co-authored-by: Nick Ashton --- lib/active_merchant/billing/gateways/flex_charge.rb | 2 ++ test/unit/gateways/flex_charge_test.rb | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index a23b821d071..efd11cf6214 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -194,6 +194,8 @@ def add_invoice(post, money, credit_card, options) end def add_payment_method(post, credit_card, address, options) + return unless credit_card.number.present? + payment_method = case credit_card when String { Token: true, cardNumber: credit_card } diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index bea51350be7..0c2fbfef67b 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -172,6 +172,17 @@ 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_failed_refund response = stub_comms(@gateway, :ssl_request) do @gateway.refund(@amount, 'reference', @options) From 8dc76a4380f59fd93a8f27e14fbf512452dd2564 Mon Sep 17 00:00:00 2001 From: cristian Date: Wed, 10 Jul 2024 16:09:52 -0500 Subject: [PATCH 24/46] FlexCharge: quick fix on void call FlexCharge: Enabling void call Summary: ------------------------------ Changes FlexCharge to fix support for the void operation and also add the sense_key gsf [SER-1327](https://spreedly.atlassian.net/browse/SER-1327) [SER-1307](https://spreedly.atlassian.net/browse/SER-1307) Remote Test: ------------------------------ Finished in 70.477035 seconds. 19 tests, 54 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 124.425603 seconds. 5959 tests, 79971 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/flex_charge.rb | 3 ++- test/remote/gateways/remote_flex_charge_test.rb | 2 +- test/unit/gateways/flex_charge_test.rb | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index efd11cf6214..3874f743fe9 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -68,7 +68,7 @@ def refund(money, authorization, options = {}) commit(:refund, { amountToRefund: localized_amount(money, 2).to_f }, order_id) end - def void(money, authorization, options = {}) + def void(authorization, options = {}) order_id, _currency = authorization.split('#') commit(:void, {}, order_id) end @@ -145,6 +145,7 @@ 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[:senseKey] = options[:sense_key] end def add_mit_data(post, options) diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index 9a9edd244f5..b19dad30c09 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -164,7 +164,7 @@ def test_successful_void response = @gateway.authorize(@amount, @credit_card_mit, @cit_options) assert_success response - assert void = @gateway.void(@amount, response.authorization) + assert void = @gateway.void(response.authorization) assert_success void end diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 0c2fbfef67b..1d89ccc47a5 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -24,7 +24,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'), + sense_key: 'abc123' } @cit_options = { @@ -106,6 +107,7 @@ 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['transaction']['timezoneUtcOffset'], @options[:timezone_utc_offset] assert_equal request['transaction']['amount'], @amount assert_equal request['transaction']['responseCode'], @options[:response_code] From 19d397983a369f8110f140316dc37984a34c6d80 Mon Sep 17 00:00:00 2001 From: Nick Ashton Date: Fri, 12 Jul 2024 11:18:49 -0400 Subject: [PATCH 25/46] Fix bug where `add_payment_method` was incorrectly returned early if the payment value was a token (#5173) --- .../billing/gateways/flex_charge.rb | 32 ++++++++++--------- test/unit/gateways/flex_charge_test.rb | 10 ++++++ 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index 3874f743fe9..955925bcc7d 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -195,24 +195,26 @@ def add_invoice(post, money, credit_card, options) end def add_payment_method(post, credit_card, address, options) - return unless credit_card.number.present? - 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 diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 1d89ccc47a5..aec427eb14e 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -185,6 +185,16 @@ def test_purchase_using_card_with_no_number 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) From f270a60639824cafb6a7a3d9198c926049c38f74 Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Thu, 11 Jul 2024 13:24:29 -0700 Subject: [PATCH 26/46] Add neew Bins: Maestro --- CHANGELOG | 1 + lib/active_merchant/billing/credit_card_methods.rb | 3 ++- test/unit/credit_card_methods_test.rb | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2151510045d..149da679bb9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -21,6 +21,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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 From dfaccf40b88ae60a7a88c0086df0664c1e702a40 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Fri, 12 Jul 2024 16:17:48 -0500 Subject: [PATCH 27/46] Braintree: Remove stored credential v1 Update Stored Credential flow to keep v2. Unit: 104 tests, 219 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 123 tests, 661 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + .../billing/gateways/braintree_blue.rb | 26 ++-------------- .../gateways/remote_braintree_blue_test.rb | 8 ++--- test/unit/gateways/braintree_blue_test.rb | 30 +++++++++---------- 4 files changed, 22 insertions(+), 43 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 149da679bb9..e462b444e69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,6 +22,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 82e21dc9959..9ac7921dbff 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -912,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] @@ -940,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] = {} diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index d36f3187731..8360a77a009 100644 --- a/test/remote/gateways/remote_braintree_blue_test.rb +++ b/test/remote/gateways/remote_braintree_blue_test.rb @@ -1073,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'] @@ -1082,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'] @@ -1091,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'] @@ -1100,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/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 From 957dd753ae83d178355f769644f5df823d93496a Mon Sep 17 00:00:00 2001 From: Javier Pedroza Date: Tue, 16 Jul 2024 13:58:11 -0500 Subject: [PATCH 28/46] Plexo: Update Network Token implementation (#5169) Description ------------------------- [SER-1377](https://spreedly.atlassian.net/browse/SER-1377) This commit update the previous implementation of NT in order to use network token instead if card Unit test ------------------------- Finished in 0.033159 seconds. 25 tests, 137 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 753.94 tests/s, 4131.61 assertions/s Remote test ------------------------- Finished in 47.41717 seconds. 32 tests, 62 assertions, 0 failures, 0 errors, 0 pendings, 3 omissions, 0 notifications 100% passed 0.67 tests/s, 1.31 assertions/s Rubocop ------------------------- 798 files inspected, no offenses detected Co-authored-by: Javier Pedroza --- lib/active_merchant/billing/gateways/plexo.rb | 49 +++++++++++++++---- test/remote/gateways/remote_plexo_test.rb | 17 ++++++- test/unit/gateways/plexo_test.rb | 6 +-- 3 files changed, 57 insertions(+), 15 deletions(-) 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/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/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 From 941c6d2a756d1dcb23619ebe0cb7eb3caee05a1b Mon Sep 17 00:00:00 2001 From: Javier Pedroza Date: Wed, 17 Jul 2024 08:18:48 -0500 Subject: [PATCH 29/46] NMI: Adding GooglePay and ApplePay (#5146) Description ------------------------- This commit add support to create transaction with GooglePay and ApplePay. This payment methods are working for nmi_secure. Unit test ------------------------- Finished in 11.283174 seconds. 59 tests, 475 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 5.23 tests/s, 42.10 assertions/s Remote test ------------------------- Finished in 115.513346 seconds. 55 tests, 199 assertions, 12 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 78.1818% passed 0.48 tests/s, 1.72 assertions/s Rubocop ------------------------- 798 files inspected, no offenses detected Co-authored-by: Javier Pedroza --- lib/active_merchant/billing/gateways/nmi.rb | 14 ++++- test/remote/gateways/remote_nmi_test.rb | 58 +++++++++++++++++---- test/unit/gateways/nmi_test.rb | 46 ++++++++++++++++ 3 files changed, 107 insertions(+), 11 deletions(-) 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/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/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 From 568466b88b8671f4f5f54ef9253c6c576661a00c Mon Sep 17 00:00:00 2001 From: Luis Mario Urrea Murillo Date: Wed, 17 Jul 2024 11:05:41 -0500 Subject: [PATCH 30/46] Braintree: Pass overridden mid into client token for GS 3DS (#5166) Summary: ------------------------------ Add merchant_account_id for the Client Token Generate and returns a string which contains all authorization and configuration information that the client needs to initialize the client SDK to communicate with Braintree [ECS-3617](https://spreedly.atlassian.net/browse/ECS-3617) [ECS-3607](https://spreedly.atlassian.net/browse/ECS-3607) Remote Test: ------------------------------ Finished in 6.262003 seconds. 7 tests, 15 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 1.12 tests/s, 2.40 assertions/s Unit Tests: ------------------------------ Finished in 38.744354 seconds. 5956 tests, 79952 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 153.73 tests/s, 2063.58 assertions/s RuboCop: ------------------------------ 798 files inspected, no offenses detected Co-authored-by: Luis Urrea --- CHANGELOG | 1 + .../billing/gateways/braintree/token_nonce.rb | 10 ++++----- .../billing/gateways/braintree_blue.rb | 2 +- .../remote_braintree_token_nonce_test.rb | 21 +++++++++++++++++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e462b444e69..5f5ca94ff59 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -23,6 +23,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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 9ac7921dbff..42ea1ff9234 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -1036,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/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 From 27ae6b7e3a3d59a9c8e6dfc02d37d4f81f5aadbd Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Tue, 2 Jul 2024 13:30:25 -0500 Subject: [PATCH 31/46] Moneris: Update crypt_type for 3DS Update crypt_type to only be one digit by removing leading zero if present. Unit: 54 tests, 294 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 53 tests, 259 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + lib/active_merchant/billing/gateways/moneris.rb | 2 +- test/unit/gateways/moneris_test.rb | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5f5ca94ff59..71ec468d788 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,6 +24,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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 From 4b4ccb9223bee9e13fa4cbadda73be7532586536 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Fri, 12 Jul 2024 16:42:24 -0500 Subject: [PATCH 32/46] Update CheckoutV2 3DS message & error code Update CheckoutV2 3DS message & error code to keep waht was bening threed_response_message Unit: 67 tests, 420 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 111 tests, 274 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + .../billing/gateways/checkout_v2.rb | 13 ++----------- test/unit/gateways/checkout_v2_test.rb | 14 ++------------ 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 71ec468d788..f45c855f739 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -25,6 +25,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/checkout_v2.rb b/lib/active_merchant/billing/gateways/checkout_v2.rb index d5112d0c5e1..1725b14ac05 100644 --- a/lib/active_merchant/billing/gateways/checkout_v2.rb +++ b/lib/active_merchant/billing/gateways/checkout_v2.rb @@ -661,12 +661,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 @@ -696,11 +691,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/test/unit/gateways/checkout_v2_test.rb b/test/unit/gateways/checkout_v2_test.rb index a39f9ca6359..220ad5a70c9 100644 --- a/test/unit/gateways/checkout_v2_test.rb +++ b/test/unit/gateways/checkout_v2_test.rb @@ -431,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 From 214d4839d66b62caa977f48719386270bc8f07b1 Mon Sep 17 00:00:00 2001 From: cristian Date: Mon, 15 Jul 2024 16:37:50 -0500 Subject: [PATCH 33/46] SER-1386 add ExtraData and Source GSFs Summary: ------------------------------ FlexCharge: quick fix on void call [SER-1386](https://spreedly.atlassian.net/browse/SER-1386) Remote Test: ------------------------------ Finished in 70.477035 seconds. 19 tests, 54 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 124.425603 seconds. 5959 tests, 79971 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/flex_charge.rb | 6 ++++++ test/remote/gateways/remote_flex_charge_test.rb | 3 ++- test/unit/gateways/flex_charge_test.rb | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index 955925bcc7d..7a7c05dd390 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -41,6 +41,7 @@ def purchase(money, credit_card, options = {}) add_address(post, credit_card, address(options)) add_customer_data(post, options) add_three_ds(post, options) + add_metadata(post, options) commit(:purchase, post) end @@ -114,6 +115,11 @@ def inquire(authorization, options = {}) 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) diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index b19dad30c09..731020877eb 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: { hello: 'world' }.to_json } @cit_options = @options.merge( diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index aec427eb14e..341165ab298 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -25,7 +25,8 @@ def setup cavv_result_code: '111', timezone_utc_offset: '-5', billing_address: address.merge(name: 'Cure Tester'), - sense_key: 'abc123' + sense_key: 'abc123', + extra_data: { hello: 'world' }.to_json } @cit_options = { @@ -108,6 +109,8 @@ def test_successful_purchase 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] From a2b79f41c8966747284a28e6276302f7aab2d037 Mon Sep 17 00:00:00 2001 From: cristian Date: Fri, 19 Jul 2024 16:39:06 -0500 Subject: [PATCH 34/46] SER-1387 fix shipping address and idempotency key Summary: ------------------------------ FlexCharge: Including shipping address if provided and fix idempotency key to default to random UUID. [SER-1387](https://spreedly.atlassian.net/browse/SER-1387) [SER-1338](https://spreedly.atlassian.net/browse/SER-1338) Remote Test: ------------------------------ Finished in 77.783815 seconds. 20 tests, 56 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 46.32943 seconds. 5963 tests, 79998 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- .../billing/gateways/flex_charge.rb | 11 +++++++---- test/remote/gateways/remote_flex_charge_test.rb | 8 ++++++++ test/unit/gateways/flex_charge_test.rb | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index 7a7c05dd390..ed2ab26e96d 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -38,7 +38,8 @@ def purchase(money, credit_card, options = {}) add_invoice(post, money, credit_card, options) add_mit_data(post, options) add_payment_method(post, credit_card, address(options), options) - add_address(post, credit_card, address(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) @@ -150,7 +151,7 @@ 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 @@ -166,10 +167,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) + 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], diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index 731020877eb..1c1d5c2125f 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -115,6 +115,14 @@ def test_successful_purchase_mit 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! diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 341165ab298..2c6eabc1a91 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -25,6 +25,7 @@ def setup cavv_result_code: '111', timezone_utc_offset: '-5', 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 } @@ -121,6 +122,11 @@ def test_successful_purchase assert_equal request['transaction']['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) @@ -292,6 +298,14 @@ def test_authorization_from_on_purchase 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 From 39878f64fa1089fe705a07d10e6756a00553c8a8 Mon Sep 17 00:00:00 2001 From: Gustavo Sanmartin Date: Wed, 17 Jul 2024 08:46:25 -0500 Subject: [PATCH 35/46] Datatrans: Add TPV Summary: _________________________ Include Store and Unstore Methods in datatrans to support Third Party Token. [SER-1395](https://spreedly.atlassian.net/browse/SER-1395) Tests _________________________ Remote Test: ------------------------- Finished in 31.477035 seconds. 27 tests, 76 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------- Finished in 0.115603 seconds. 29 tests, 165 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Rubocop ------------------------- 798 files inspected, no offenses detected --- .../billing/gateways/datatrans.rb | 90 +++++++++++++------ test/remote/gateways/remote_datatrans_test.rb | 44 ++++++++- test/unit/gateways/datatrans_test.rb | 67 ++++++++++++-- 3 files changed, 168 insertions(+), 33 deletions(-) diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb index 26ba761d8f7..c4b53a4586c 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' @@ -72,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].split('-')[0] + commit('delete_alias', {}, { alias_id: data_alias }, :delete) + end + def supports_scrubbing? true end @@ -86,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].split('-') + 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) @@ -157,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) ) @@ -196,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) + string = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('-') if action == 'tokenize' + + auth = [response['transactionId'], response['acquirerAuthorizationCode'], string].join('|') + return auth unless auth == '||' end def message_from(succeeded, response) diff --git a/test/remote/gateways/remote_datatrans_test.rb b/test/remote/gateways/remote_datatrans_test.rb index 87a87484ea2..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) @@ -183,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 diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb index cbe2316738f..e66f9cd6ccd 100644 --- a/test/unit/gateways/datatrans_test.rb +++ b/test/unit/gateways/datatrans_test.rb @@ -27,7 +27,7 @@ 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) @@ -219,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 @@ -274,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 @@ -283,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 @@ -314,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']) @@ -324,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 From cc7ec9b5635cabd8e962d385d86f83cb7e8f670e Mon Sep 17 00:00:00 2001 From: cristian Date: Tue, 23 Jul 2024 09:26:32 -0500 Subject: [PATCH 36/46] FlexCharge: change transactionType placement Summary: ------------------------------ Change FlexCharge transactionType to a new placement. [SER-1400](https://spreedly.atlassian.net/browse/SER-1400) Remote Test: ------------------------------ Finished in 79.263845 seconds. 20 tests, 56 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 38.175694 seconds. 5963 tests, 79998 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/flex_charge.rb | 4 +--- test/remote/gateways/remote_flex_charge_test.rb | 2 +- test/unit/gateways/flex_charge_test.rb | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/active_merchant/billing/gateways/flex_charge.rb b/lib/active_merchant/billing/gateways/flex_charge.rb index ed2ab26e96d..3471711e72a 100644 --- a/lib/active_merchant/billing/gateways/flex_charge.rb +++ b/lib/active_merchant/billing/gateways/flex_charge.rb @@ -30,9 +30,8 @@ def initialize(options = {}) end def purchase(money, credit_card, options = {}) - options[:transactionType] ||= 'Purchase' + post = { transactionType: options.fetch(:transactionType, 'Purchase') } - post = {} add_merchant_data(post, options) add_base_data(post, options) add_invoice(post, money, credit_card, options) @@ -198,7 +197,6 @@ def add_invoice(post, money, credit_card, options) avsResultCode: options[:avs_result_code], cvvResultCode: options[:cvv_result_code], cavvResultCode: options[:cavv_result_code], - transactionType: options[:transactionType], cardNotPresent: credit_card.is_a?(String) ? false : credit_card.verification_value.blank? }.compact end diff --git a/test/remote/gateways/remote_flex_charge_test.rb b/test/remote/gateways/remote_flex_charge_test.rb index 1c1d5c2125f..d38cff1a7b6 100644 --- a/test/remote/gateways/remote_flex_charge_test.rb +++ b/test/remote/gateways/remote_flex_charge_test.rb @@ -27,7 +27,7 @@ def setup cavv_result_code: '111', timezone_utc_offset: '-5', billing_address: address.merge(name: 'Cure Tester'), - extra_data: { hello: 'world' }.to_json + extra_data: '' } @cit_options = @options.merge( diff --git a/test/unit/gateways/flex_charge_test.rb b/test/unit/gateways/flex_charge_test.rb index 2c6eabc1a91..09683bc59f3 100644 --- a/test/unit/gateways/flex_charge_test.rb +++ b/test/unit/gateways/flex_charge_test.rb @@ -119,7 +119,7 @@ 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['transaction']['transactionType'], 'Purchase' + assert_equal request['transactionType'], 'Purchase' assert_equal request['payer']['email'], @options[:email] assert_equal request['description'], @options[:description] @@ -141,7 +141,7 @@ def test_successful_authorization @gateway.authorize(@amount, @credit_card, @options) end.check_request do |_method, endpoint, data, _headers| request = JSON.parse(data) - assert_equal request['transaction']['transactionType'], 'Authorization' if /evaluate/.match?(endpoint) + assert_equal request['transactionType'], 'Authorization' if /evaluate/.match?(endpoint) end.respond_with(successful_access_token_response, successful_purchase_response) end From 3f85ecdc6bb75cbc28eaf694ff94f03d01b6cacb Mon Sep 17 00:00:00 2001 From: Rachel Kirk Date: Wed, 24 Jul 2024 15:23:30 -0400 Subject: [PATCH 37/46] Rapyd: Add support for save_payment_method field CER-1613 Top level boolean field that will trigger payment method to be saved. Remote Tests: 54 tests, 152 assertions, 3 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 94.4444% passed * I fixed a few failing tests, so less failing on master. --- lib/active_merchant/billing/gateways/rapyd.rb | 1 + test/remote/gateways/remote_rapyd_test.rb | 18 +++++++++++++----- test/unit/gateways/rapyd_test.rb | 10 ++++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) 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/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/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, From efd27e7bb3e2c8d682e69afef22f9b299f39661d Mon Sep 17 00:00:00 2001 From: Gustavo Sanmartin Date: Fri, 26 Jul 2024 12:00:27 -0500 Subject: [PATCH 38/46] Datatrans: Modify authorization_from string for store (#5193) Summary: Modify the string for store to be separated by '|' instead of '-'. SER-1395 Tests Remote Test: Finished in 31.477035 seconds. 25 tests, 72 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: Finished in 0.115603 seconds. 29 tests, 165 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Rubocop 798 files inspected, no offenses detected Co-authored-by: Gustavo Sanmartin --- lib/active_merchant/billing/gateways/datatrans.rb | 8 ++++---- test/unit/gateways/datatrans_test.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/active_merchant/billing/gateways/datatrans.rb b/lib/active_merchant/billing/gateways/datatrans.rb index c4b53a4586c..0b7042d0658 100644 --- a/lib/active_merchant/billing/gateways/datatrans.rb +++ b/lib/active_merchant/billing/gateways/datatrans.rb @@ -90,7 +90,7 @@ def store(payment_method, options = {}) end def unstore(authorization, options = {}) - data_alias = authorization.split('|')[2].split('-')[0] + data_alias = authorization.split('|')[2] commit('delete_alias', {}, { alias_id: data_alias }, :delete) end @@ -110,7 +110,7 @@ def scrub(transcript) def add_payment_method(post, payment_method) case payment_method when String - token, exp_month, exp_year = payment_method.split('|')[2].split('-') + token, exp_month, exp_year = payment_method.split('|')[2..4] card = { type: 'ALIAS', alias: token, @@ -250,9 +250,9 @@ def success_from(action, response) end def authorization_from(response, action, options) - string = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('-') if action == 'tokenize' + token_array = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('|') if action == 'tokenize' - auth = [response['transactionId'], response['acquirerAuthorizationCode'], string].join('|') + auth = [response['transactionId'], response['acquirerAuthorizationCode'], token_array].join('|') return auth unless auth == '||' end diff --git a/test/unit/gateways/datatrans_test.rb b/test/unit/gateways/datatrans_test.rb index e66f9cd6ccd..0c249bf6777 100644 --- a/test/unit/gateways/datatrans_test.rb +++ b/test/unit/gateways/datatrans_test.rb @@ -325,9 +325,9 @@ def test_authorization_from 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' }) + 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' }) + assert_equal '|||any_month|any_year', @gateway.send(:authorization_from, {}, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' }) end def test_parse From d0f76157d0a3ea86ba34376de4446ef905f87dc8 Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Thu, 18 Jul 2024 11:55:05 -0500 Subject: [PATCH 39/46] DecidirPlus: Update error_message to add safety navigator Add safety navigator to error_message to prevent NoMethod errors and create a validation_error_message to construct a new error message. --- CHANGELOG | 1 + .../billing/gateways/decidir_plus.rb | 5 ++++- test/unit/gateways/decidir_plus_test.rb | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f45c855f739..34869f8ac0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 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/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}} From d4b63b4e0a00020dba56f28adbcf85ad71a1916e Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Wed, 10 Jul 2024 15:25:59 -0500 Subject: [PATCH 40/46] Elavon: Update Stored Credentials To satisfy new Elavon API requirements, including recurring_flag, approval_code for non-tokenized PMs, installment_count and _number, and situational par_value and association_token_data fields. Remote 40 tests, 178 assertions, 2 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 95% passed Unit 60 tests, 356 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- CHANGELOG | 1 + .../billing/gateways/elavon.rb | 102 ++++-- test/remote/gateways/remote_elavon_test.rb | 270 +++++++++++----- test/unit/gateways/elavon_test.rb | 306 +++++++++++++++++- 4 files changed, 559 insertions(+), 120 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 34869f8ac0a..4da4e94c6d5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index f7f5e678575..ce1030bb193 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.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/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/unit/gateways/elavon_test.rb b/test/unit/gateways/elavon_test.rb index 49f8ca8c207..510458388db 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) + 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 From e9ea86f7293f14c27fb46ebfaed96f5d5e11f5ad Mon Sep 17 00:00:00 2001 From: Alma Malambo Date: Thu, 1 Aug 2024 09:21:09 -0500 Subject: [PATCH 41/46] Elavon: Update cvv for stored credential Only skip CVV if stored credentials is being used and it's not an initial transaction. --- lib/active_merchant/billing/gateways/elavon.rb | 2 +- test/unit/gateways/elavon_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index ce1030bb193..fa4892618da 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -247,7 +247,7 @@ def add_currency(xml, money, options) 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.dig(:stored_credential, :initial_transaction) && options[:stored_cred_v2] + return if options[:stored_credential] && !options.dig(:stored_credential, :initial_transaction) && options[:stored_cred_v2] xml.ssl_cvv2cvc2 credit_card.verification_value xml.ssl_cvv2cvc2_indicator 1 diff --git a/test/unit/gateways/elavon_test.rb b/test/unit/gateways/elavon_test.rb index 510458388db..760d210e927 100644 --- a/test/unit/gateways/elavon_test.rb +++ b/test/unit/gateways/elavon_test.rb @@ -47,7 +47,7 @@ def setup def test_successful_purchase response = stub_comms do - @gateway.purchase(@amount, @credit_card, @options) + @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) From 0c9acc1d22546ffea0eae72a29f4f8bf87a9bd14 Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Wed, 17 Jul 2024 10:10:50 -0700 Subject: [PATCH 42/46] Adyen: Include header fields in response body --- CHANGELOG | 1 + lib/active_merchant/billing/gateways/adyen.rb | 26 ++++++++++++++++++- test/unit/gateways/adyen_test.rb | 10 +++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4da4e94c6d5..8884bf6cc9b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ * 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 == Version 1.136.0 (June 3, 2024) * Shift4V2: Add new gateway based on SecurionPay adapter [heavyblade] #4860 diff --git a/lib/active_merchant/billing/gateways/adyen.rb b/lib/active_merchant/billing/gateways/adyen.rb index 648cb4299a8..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) diff --git a/test/unit/gateways/adyen_test.rb b/test/unit/gateways/adyen_test.rb index e673cc9251b..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) From d9cffb6810c3f1a7b60febed4636a6f9303ce06b Mon Sep 17 00:00:00 2001 From: Nhon Dang Date: Wed, 17 Jul 2024 13:28:04 -0700 Subject: [PATCH 43/46] Stripe and Stripe PI: add headers to response body --- CHANGELOG | 1 + .../billing/gateways/stripe.rb | 27 ++++++++++++++++++- .../remote_stripe_payment_intents_test.rb | 11 ++++++++ test/remote/gateways/remote_stripe_test.rb | 8 ++++++ .../gateways/stripe_payment_intents_test.rb | 9 +++++++ test/unit/gateways/stripe_test.rb | 13 +++++++++ 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8884bf6cc9b..c669c3e0307 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,7 @@ * 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/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/test/remote/gateways/remote_stripe_payment_intents_test.rb b/test/remote/gateways/remote_stripe_payment_intents_test.rb index 730f00dd1cb..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', 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/unit/gateways/stripe_payment_intents_test.rb b/test/unit/gateways/stripe_payment_intents_test.rb index 241977f0550..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) 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) From f788fd85e9862f79cabc587536d935a277d14994 Mon Sep 17 00:00:00 2001 From: Gustavo Sanmartin Date: Thu, 1 Aug 2024 14:58:56 -0500 Subject: [PATCH 44/46] Fatzebra: fix directory_server_transaction_id mapping (#5197) Summary: ------------------------------ Rename the equivalent ds_transaction_id in fatzebra for directory_server_txn_id, to map it corrctly. [ECS-3660](https://spreedly.atlassian.net/browse/ECS-3660) [UBI-132](https://spreedly.atlassian.net/browse/UBI-132) Remote Tests: ------------------------------ Finished in 87.765183 seconds. 30 tests, 104 assertions, 2 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 93.3333% passed *Note Failing Test*: We have to remote tests failing because seems to be a change on the sandbox behavior when a transaction doesn't include a cvv Unit Tests: ------------------------------ Finished in 0.073139 seconds. 23 tests, 130 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 798 files inspected, no offenses detected Co-authored-by: Gustavo Sanmartin --- lib/active_merchant/billing/gateways/fat_zebra.rb | 2 +- test/remote/gateways/remote_fat_zebra_test.rb | 2 +- test/unit/gateways/fat_zebra_test.rb | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) 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/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/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 From f2ed5b6219af7b30001a3e2c43fc5dca79b33fe5 Mon Sep 17 00:00:00 2001 From: Raymond Ag Date: Fri, 2 Aug 2024 23:15:18 +1000 Subject: [PATCH 45/46] Upgrade rexml to 3.3.4 to address CVE-2024-39908, 41123, 41946 (#5181) * Upgrade rexml to 3.3.2 This resolves CVE-2024-39908 : DoS in REXML * Apply suggestion * Fix tests * Upgrade rexml to 3.3.4 --- activemerchant.gemspec | 2 +- test/unit/gateways/mercury_test.rb | 6 +++--- test/unit/gateways/paypal_test.rb | 2 +- test/unit/gateways/trans_first_test.rb | 10 ---------- 4 files changed, 5 insertions(+), 15 deletions(-) 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/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/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/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) From f52d344be4a2b6ab62b6f8849726333b3b4572df Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 2 Aug 2024 15:22:27 +0200 Subject: [PATCH 46/46] Release 1.137.0 --- CHANGELOG | 4 ++++ lib/active_merchant/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c669c3e0307..bbab88f6888 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,10 @@ = 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 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