diff --git a/CHANGELOG b/CHANGELOG index 2142fc29de1..c10fef2ab21 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ * Adyen: Add option to elect which error message [aenand] #4843 * Reach: Update list of supported countries [jcreiff] #4842 * Paysafe: Truncate address fields [jcreiff] #4841 +* Braintree: Support third party Network Tokens [aenand] #4775 == Version 1.134.0 (July 25, 2023) * Update required Ruby version [almalee24] #4823 diff --git a/Gemfile b/Gemfile index 01084b00034..f653ef90ec3 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem 'rubocop', '~> 0.72.0', require: false group :test, :remote_test do # gateway-specific dependencies, keeping these gems out of the gemspec - gem 'braintree', '>= 3.0.0', '<= 3.0.1' + gem 'braintree', '>= 4.12.0' gem 'jose', '~> 1.1.3' gem 'jwe' gem 'mechanize' diff --git a/lib/active_merchant/billing/gateways/braintree_blue.rb b/lib/active_merchant/billing/gateways/braintree_blue.rb index 3cbe60d309f..0bc075e35af 100644 --- a/lib/active_merchant/billing/gateways/braintree_blue.rb +++ b/lib/active_merchant/billing/gateways/braintree_blue.rb @@ -835,54 +835,86 @@ def add_stored_credential_data(parameters, credit_card_or_vault_id, options) def add_payment_method(parameters, credit_card_or_vault_id, options) if credit_card_or_vault_id.is_a?(String) || credit_card_or_vault_id.is_a?(Integer) - if options[:payment_method_token] - parameters[:payment_method_token] = credit_card_or_vault_id - options.delete(:billing_address) - elsif options[:payment_method_nonce] - parameters[:payment_method_nonce] = credit_card_or_vault_id - else - parameters[:customer_id] = credit_card_or_vault_id - end + add_third_party_token(parameters, credit_card_or_vault_id, options) else parameters[:customer].merge!( first_name: credit_card_or_vault_id.first_name, last_name: credit_card_or_vault_id.last_name ) if credit_card_or_vault_id.is_a?(NetworkTokenizationCreditCard) - if credit_card_or_vault_id.source == :apple_pay - parameters[:apple_pay_card] = { - number: credit_card_or_vault_id.number, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - cardholder_name: credit_card_or_vault_id.name, - cryptogram: credit_card_or_vault_id.payment_cryptogram, - eci_indicator: credit_card_or_vault_id.eci - } - elsif credit_card_or_vault_id.source == :android_pay || credit_card_or_vault_id.source == :google_pay - Braintree::Version::Major < 3 ? pay_card = :android_pay_card : pay_card = :google_pay_card - parameters[pay_card] = { - number: credit_card_or_vault_id.number, - cryptogram: credit_card_or_vault_id.payment_cryptogram, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - google_transaction_id: credit_card_or_vault_id.transaction_id, - source_card_type: credit_card_or_vault_id.brand, - source_card_last_four: credit_card_or_vault_id.last_digits, - eci_indicator: credit_card_or_vault_id.eci - } + case credit_card_or_vault_id.source + when :apple_pay + add_apple_pay(parameters, credit_card_or_vault_id) + when :google_pay + add_google_pay(parameters, credit_card_or_vault_id) + else + add_network_tokenization_card(parameters, credit_card_or_vault_id) end else - parameters[:credit_card] = { - number: credit_card_or_vault_id.number, - cvv: credit_card_or_vault_id.verification_value, - expiration_month: credit_card_or_vault_id.month.to_s.rjust(2, '0'), - expiration_year: credit_card_or_vault_id.year.to_s, - cardholder_name: credit_card_or_vault_id.name - } + add_credit_card(parameters, credit_card_or_vault_id) end end end + def add_third_party_token(parameters, payment_method, options) + if options[:payment_method_token] + parameters[:payment_method_token] = payment_method + options.delete(:billing_address) + elsif options[:payment_method_nonce] + parameters[:payment_method_nonce] = payment_method + else + parameters[:customer_id] = payment_method + end + end + + def add_credit_card(parameters, payment_method) + parameters[:credit_card] = { + number: payment_method.number, + cvv: payment_method.verification_value, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name + } + end + + def add_apple_pay(parameters, payment_method) + parameters[:apple_pay_card] = { + number: payment_method.number, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name, + cryptogram: payment_method.payment_cryptogram, + eci_indicator: payment_method.eci + } + end + + def add_google_pay(parameters, payment_method) + Braintree::Version::Major < 3 ? pay_card = :android_pay_card : pay_card = :google_pay_card + parameters[pay_card] = { + number: payment_method.number, + cryptogram: payment_method.payment_cryptogram, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + google_transaction_id: payment_method.transaction_id, + source_card_type: payment_method.brand, + source_card_last_four: payment_method.last_digits, + eci_indicator: payment_method.eci + } + end + + def add_network_tokenization_card(parameters, payment_method) + parameters[:credit_card] = { + number: payment_method.number, + expiration_month: payment_method.month.to_s.rjust(2, '0'), + expiration_year: payment_method.year.to_s, + cardholder_name: payment_method.name, + network_tokenization_attributes: { + cryptogram: payment_method.payment_cryptogram, + ecommerce_indicator: payment_method.eci + } + } + end + def bank_account_errors(payment_method, options) if payment_method.validate.present? payment_method.validate diff --git a/test/remote/gateways/remote_braintree_blue_test.rb b/test/remote/gateways/remote_braintree_blue_test.rb index 0ec9a6c33f7..3cc5b8cb5c9 100644 --- a/test/remote/gateways/remote_braintree_blue_test.rb +++ b/test/remote/gateways/remote_braintree_blue_test.rb @@ -31,6 +31,12 @@ def setup }, ach_mandate: ach_mandate } + + @nt_credit_card = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + eci: '05', + source: :network_token, + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=') end def test_credit_card_details_on_store @@ -54,6 +60,13 @@ def test_successful_authorize assert_equal 'authorized', response.params['braintree_transaction']['status'] end + def test_successful_authorize_with_nt + assert response = @gateway.authorize(@amount, @nt_credit_card, @options) + assert_success response + assert_equal '1000 Approved', response.message + assert_equal 'authorized', response.params['braintree_transaction']['status'] + end + def test_successful_authorize_with_nil_and_empty_billing_address_options credit_card = credit_card('5105105105105100') options = { @@ -682,25 +695,6 @@ def test_authorize_and_capture_with_apple_pay_card assert_success capture end - def test_authorize_and_capture_with_android_pay_card - credit_card = network_tokenization_credit_card( - '4111111111111111', - payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', - month: '01', - year: '2024', - source: :android_pay, - transaction_id: '123456789', - eci: '05' - ) - - assert auth = @gateway.authorize(@amount, credit_card, @options) - assert_success auth - assert_equal '1000 Approved', auth.message - assert auth.authorization - assert capture = @gateway.capture(@amount, auth.authorization) - assert_success capture - end - def test_authorize_and_capture_with_google_pay_card credit_card = network_tokenization_credit_card( '4111111111111111', diff --git a/test/unit/gateways/braintree_blue_test.rb b/test/unit/gateways/braintree_blue_test.rb index 7a05622ddbe..da16bc25950 100644 --- a/test/unit/gateways/braintree_blue_test.rb +++ b/test/unit/gateways/braintree_blue_test.rb @@ -1011,7 +1011,7 @@ def test_apple_pay_card assert_equal 'transaction_id', response.authorization end - def test_android_pay_card + def test_google_pay_card Braintree::TransactionGateway.any_instance.expects(:sale). with( amount: '1.00', @@ -1038,7 +1038,7 @@ def test_android_pay_card brand: 'visa', eci: '05', payment_cryptogram: '111111111100cryptogram', - source: :android_pay, + source: :google_pay, transaction_id: '1234567890' ) @@ -1046,7 +1046,7 @@ def test_android_pay_card assert_equal 'transaction_id', response.authorization end - def test_google_pay_card + def test_network_token_card Braintree::TransactionGateway.any_instance.expects(:sale). with( amount: '1.00', @@ -1055,27 +1055,24 @@ def test_google_pay_card first_name: 'Longbob', last_name: 'Longsen' }, options: { store_in_vault: false, submit_for_settlement: nil, hold_in_escrow: nil }, custom_fields: nil, - google_pay_card: { + credit_card: { number: '4111111111111111', expiration_month: '09', expiration_year: (Time.now.year + 1).to_s, - cryptogram: '111111111100cryptogram', - google_transaction_id: '1234567890', - source_card_type: 'visa', - source_card_last_four: '1111', - eci_indicator: '05' + cardholder_name: 'Longbob Longsen', + network_tokenization_attributes: { + cryptogram: '111111111100cryptogram', + ecommerce_indicator: '05' + } } ). returns(braintree_result(id: 'transaction_id')) - credit_card = network_tokenization_credit_card( - '4111111111111111', - brand: 'visa', - eci: '05', - payment_cryptogram: '111111111100cryptogram', - source: :google_pay, - transaction_id: '1234567890' - ) + credit_card = network_tokenization_credit_card('4111111111111111', + brand: 'visa', + eci: '05', + source: :network_token, + payment_cryptogram: '111111111100cryptogram') response = @gateway.authorize(100, credit_card, test: true, order_id: '1') assert_equal 'transaction_id', response.authorization