From 55977ad695a3ce63741a1f49fb18ce01846e8392 Mon Sep 17 00:00:00 2001 From: Cory Dingels Date: Mon, 13 May 2019 13:16:53 -0500 Subject: [PATCH 1/3] parse card_type from authorize.net response --- lib/active_merchant/billing/gateways/authorize_net.rb | 4 ++++ test/remote/gateways/remote_authorize_net_test.rb | 1 + test/unit/gateways/authorize_net_test.rb | 2 ++ 3 files changed, 7 insertions(+) diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index ccc8794a05a..9abb7b6489a 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -887,6 +887,10 @@ def parse_normal(action, body) (empty?(element.content) ? nil : element.content[-4..-1]) end + response[:card_type] = if(element = doc.at_xpath('//accountType')) + (empty?(element.content) ? nil : element.content) + end + response[:test_request] = if(element = doc.at_xpath('//testRequest')) (empty?(element.content) ? nil : element.content) end diff --git a/test/remote/gateways/remote_authorize_net_test.rb b/test/remote/gateways/remote_authorize_net_test.rb index 2b239ad18bb..ac99b2c7066 100644 --- a/test/remote/gateways/remote_authorize_net_test.rb +++ b/test/remote/gateways/remote_authorize_net_test.rb @@ -567,6 +567,7 @@ def test_bad_login authorization_code avs_result_code card_code + card_type cardholder_authentication_code full_response_code response_code diff --git a/test/unit/gateways/authorize_net_test.rb b/test/unit/gateways/authorize_net_test.rb index 2cdfde5abcd..f82cd95a883 100644 --- a/test/unit/gateways/authorize_net_test.rb +++ b/test/unit/gateways/authorize_net_test.rb @@ -286,6 +286,8 @@ def test_successful_authorization assert_equal 'M', response.cvv_result['code'] assert_equal 'CVV matches', response.cvv_result['message'] assert_equal 'I00001', response.params['full_response_code'] + assert_equal '0015', response.params['account_number'] + assert_equal 'MasterCard', response.params['card_type'] assert_equal '508141794', response.authorization.split('#')[0] assert response.test? From 66b0ac46fbc8f43f042de10cd37062aa79547f4d Mon Sep 17 00:00:00 2001 From: Cory Dingels Date: Mon, 13 May 2019 14:14:40 -0500 Subject: [PATCH 2/3] add opaque data token type so accept.js payments can be accepted --- lib/active_merchant/billing.rb | 1 + .../billing/gateways/authorize_net.rb | 15 +- .../billing/opaque_data_payment_token.rb | 17 +++ .../remote_authorize_net_opaque_data_test.rb | 135 ++++++++++++++++++ test/unit/gateways/authorize_net_test.rb | 20 +++ 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 lib/active_merchant/billing/opaque_data_payment_token.rb create mode 100644 test/remote/gateways/remote_authorize_net_opaque_data_test.rb diff --git a/lib/active_merchant/billing.rb b/lib/active_merchant/billing.rb index ea3108597c8..b511882d70c 100644 --- a/lib/active_merchant/billing.rb +++ b/lib/active_merchant/billing.rb @@ -10,6 +10,7 @@ require 'active_merchant/billing/check' require 'active_merchant/billing/payment_token' require 'active_merchant/billing/apple_pay_payment_token' +require 'active_merchant/billing/opaque_data_payment_token' require 'active_merchant/billing/response' require 'active_merchant/billing/gateways' require 'active_merchant/billing/gateway' diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index 9abb7b6489a..e448c53f4e8 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -397,6 +397,8 @@ def add_payment_source(xml, source, options, action = nil) add_check(xml, source) elsif card_brand(source) == 'apple_pay' add_apple_pay_payment_token(xml, source) + elsif card_brand(source) == 'opaque_data' + add_opaque_data_payment_method(xml, source) else add_credit_card(xml, source, action) end @@ -520,8 +522,17 @@ def add_apple_pay_payment_token(xml, apple_pay_payment_token) end end + def add_opaque_data_payment_method(xml, opaque_data_payment_token) + xml.payment do + xml.opaqueData do + xml.dataDescriptor opaque_data_payment_token.data_descriptor + xml.dataValue opaque_data_payment_token.payment_data + end + end + end + def add_market_type_device_type(xml, payment, options) - return if payment.is_a?(String) || card_brand(payment) == 'check' || card_brand(payment) == 'apple_pay' + return if payment.is_a?(String) || card_brand(payment) == 'check' || card_brand(payment) == 'apple_pay' || card_brand(payment) == 'opaque_data' if valid_track_data xml.retail do xml.marketType(options[:market_type] || MARKET_TYPE[:retail]) @@ -747,7 +758,7 @@ def delete_customer_profile(customer_profile_id) end def names_from(payment_source, address, options) - if payment_source && !payment_source.is_a?(PaymentToken) && !payment_source.is_a?(String) + if payment_source && !payment_source.is_a?(ApplePayPaymentToken) && !payment_source.is_a?(String) first_name, last_name = split_names(address[:name]) [(payment_source.first_name || first_name), (payment_source.last_name || last_name)] else diff --git a/lib/active_merchant/billing/opaque_data_payment_token.rb b/lib/active_merchant/billing/opaque_data_payment_token.rb new file mode 100644 index 00000000000..2f58bf16409 --- /dev/null +++ b/lib/active_merchant/billing/opaque_data_payment_token.rb @@ -0,0 +1,17 @@ +module ActiveMerchant + module Billing + class OpaqueDataPaymentToken < PaymentToken + attr_reader :data_descriptor, :first_name, :last_name + + def initialize(payment_data, options = {}) + super + @data_descriptor = @metadata[:data_descriptor] + raise ArgumentError, 'data_descriptor is required' unless @data_descriptor + end + + def type + 'opaque_data' + end + end + end +end diff --git a/test/remote/gateways/remote_authorize_net_opaque_data_test.rb b/test/remote/gateways/remote_authorize_net_opaque_data_test.rb new file mode 100644 index 00000000000..59063b0eb14 --- /dev/null +++ b/test/remote/gateways/remote_authorize_net_opaque_data_test.rb @@ -0,0 +1,135 @@ +require 'test_helper' + +class RemoteAuthorizeNetOpaqueDataTest < Test::Unit::TestCase + def setup + @gateway = AuthorizeNetGateway.new(fixtures(:authorize_net)) + + @amount = 100 + @opaque_data_payment_token = generate_opaque_data_payment_token + + @options = { + order_id: '1', + email: 'anet@example.com', + duplicate_window: 0, + billing_address: address, + description: 'Store Purchase' + } + end + + def test_successful_opaque_data_authorization + response = @gateway.authorize(5, @opaque_data_payment_token, @options) + assert_success response + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + + def test_successful_opaque_data_authorization_and_capture + assert authorization = @gateway.authorize(@amount, @opaque_data_payment_token, @options) + assert_success authorization + + assert capture = @gateway.capture(@amount, authorization.authorization) + assert_success capture + assert_equal 'This transaction has been approved', capture.message + end + + def test_successful_opaque_data_authorization_and_void + assert authorization = @gateway.authorize(@amount, @opaque_data_payment_token, @options) + assert_success authorization + + assert void = @gateway.void(authorization.authorization) + assert_success void + assert_equal 'This transaction has been approved', void.message + end + + def test_failed_opaque_data_authorization + opaque_data_payment_token = OpaqueDataPaymentToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') + response = @gateway.authorize(@amount, opaque_data_payment_token, @options) + assert_failure response + assert_equal "OTS Service Error 'Field validation error.'", response.message + assert_equal '117', response.params['response_reason_code'] + end + + def test_failed_opaque_data_purchase + opaque_data_payment_token = OpaqueDataPaymentToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') + response = @gateway.purchase(@amount, opaque_data_payment_token, @options) + assert_failure response + assert_equal "OTS Service Error 'Field validation error.'", response.message + assert_equal '117', response.params['response_reason_code'] + end + + private + + def accept_js_gateway + @accept_js_gateway ||= AcceptJsGateway.new(fixtures(:authorize_net)) + end + + def fetch_public_client_key + @fetch_public_client_key ||= accept_js_gateway.public_client_key + end + + def generate_opaque_data_payment_token + cc = credit_card('4000100011112224') + options = { public_client_key: fetch_public_client_key, name: address[:name] } + opaque_data = accept_js_gateway.accept_js_token(cc, options) + OpaqueDataPaymentToken.new(opaque_data[:data_value], data_descriptor: opaque_data[:data_descriptor]) + end + + class AcceptJsGateway < ActiveMerchant::Billing::AuthorizeNetGateway + # API calls to get a payment nonce from Authorize.net should only originate from javascript, usign authnet's accept.js library. + # This gateway implements the API calls necessary to replicate accept.js client behavior, so that we can test authorizations and purchases using an accept.js payment nonce. + # https://developer.authorize.net/api/reference/features/acceptjs.html + + def public_client_key + response = commit(:merchant_details) {} + response.params.dig('getMerchantDetailsResponse', 'publicClientKey') + end + + def accept_js_token(credit_card, options={}) + request = accept_js_request_body(credit_card, options) + raw_response = ssl_post(url, request, headers) + opaque_data = parse(:accept_js_token_request, raw_response).dig('securePaymentContainerResponse', 'opaqueData') + { + data_descriptor: opaque_data['dataDescriptor'], + data_value: opaque_data['dataValue'] + } + end + + private + + def accept_js_request_body(credit_card, options={}) + Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.send('securePaymentContainerRequest', 'xmlns' => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd') do + xml.merchantAuthentication do + xml.name(@options[:login]) + xml.clientKey(options[:public_client_key]) + end + xml.data do + xml.type('TOKEN') + xml.id(SecureRandom.uuid) + xml.token do + xml.cardNumber(truncate(credit_card.number, 16)) + xml.expirationDate(format(credit_card.month, :two_digits) + '/' + format(credit_card.year, :four_digits)) + xml.fullName options[:name] + end + end + end + end.to_xml(indent: 0) + end + + def root_for(action) + if action == :merchant_details + 'getMerchantDetailsRequest' + else + super + end + end + + def parse_normal(action, body) + doc = Nokogiri::XML(body) + doc.remove_namespaces! + Hash.from_xml(doc.to_s) + end + + end + +end diff --git a/test/unit/gateways/authorize_net_test.rb b/test/unit/gateways/authorize_net_test.rb index f82cd95a883..992b948e763 100644 --- a/test/unit/gateways/authorize_net_test.rb +++ b/test/unit/gateways/authorize_net_test.rb @@ -22,6 +22,10 @@ def setup payment_network: 'Visa', transaction_identifier: 'transaction123' ) + @opaque_data_payment_token = ActiveMerchant::Billing::OpaqueDataPaymentToken.new( + 'eyJjb2RlIjoiNTBfMl8wNjAwMDUzMTg1MEFCNDg3Mzc3OTkyRUI4RUJGMzJGNDFDQUVDM0U4OTlERTU5MzJBQzIyNzdBM0E0MEUwQ0I5MTI0NEQ1QzcwMUU5OEU3RURBQzAyODE2QjcwMUZCNDE1QzlDNzQzIiwidG9rZW4iOiI5NTQ5NDkzNDM2Mzc4ODAyMDA0NjAzIiwidiI6IjEuMSJ9', + data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT' + ) @options = { order_id: '1', @@ -277,6 +281,22 @@ def test_successful_apple_pay_purchase assert_equal '508141795', response.authorization.split('#')[0] end + def test_successful_accept_js_authorization + response = stub_comms do + @gateway.authorize(@amount, @opaque_data_payment_token) + end.check_request do |endpoint, data, headers| + parse(data) do |doc| + assert_equal @opaque_data_payment_token.data_descriptor, doc.at_xpath('//opaqueData/dataDescriptor').content + assert_equal @opaque_data_payment_token.payment_data, doc.at_xpath('//opaqueData/dataValue').content + end + end.respond_with(successful_authorize_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '508141794', response.authorization.split('#')[0] + end + def test_successful_authorization @gateway.expects(:ssl_post).returns(successful_authorize_response) From 1785c3283c88fd23e7a161dd417c4a49ab613157 Mon Sep 17 00:00:00 2001 From: Cory Dingels Date: Mon, 5 Aug 2019 10:41:49 -0500 Subject: [PATCH 3/3] refactor: Billing::OpaqueDataPaymentToken -> Gateways::AuthorizeNet::PaymentToken --- lib/active_merchant/billing.rb | 1 - .../billing/gateways/authorize_net.rb | 14 ++++++++++++++ .../billing/opaque_data_payment_token.rb | 17 ----------------- .../remote_authorize_net_opaque_data_test.rb | 6 +++--- 4 files changed, 17 insertions(+), 21 deletions(-) delete mode 100644 lib/active_merchant/billing/opaque_data_payment_token.rb diff --git a/lib/active_merchant/billing.rb b/lib/active_merchant/billing.rb index b511882d70c..ea3108597c8 100644 --- a/lib/active_merchant/billing.rb +++ b/lib/active_merchant/billing.rb @@ -10,7 +10,6 @@ require 'active_merchant/billing/check' require 'active_merchant/billing/payment_token' require 'active_merchant/billing/apple_pay_payment_token' -require 'active_merchant/billing/opaque_data_payment_token' require 'active_merchant/billing/response' require 'active_merchant/billing/gateways' require 'active_merchant/billing/gateway' diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index e448c53f4e8..8126ce39fe8 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -1065,6 +1065,20 @@ def parse_direct_response_elements(response, options) } end + class OpaqueDataToken < PaymentToken + attr_reader :data_descriptor, :first_name, :last_name + + def initialize(payment_data, options = {}) + super + @data_descriptor = @metadata[:data_descriptor] + raise ArgumentError, 'data_descriptor is required' unless @data_descriptor + end + + def type + 'opaque_data' + end + end + end end end diff --git a/lib/active_merchant/billing/opaque_data_payment_token.rb b/lib/active_merchant/billing/opaque_data_payment_token.rb deleted file mode 100644 index 2f58bf16409..00000000000 --- a/lib/active_merchant/billing/opaque_data_payment_token.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveMerchant - module Billing - class OpaqueDataPaymentToken < PaymentToken - attr_reader :data_descriptor, :first_name, :last_name - - def initialize(payment_data, options = {}) - super - @data_descriptor = @metadata[:data_descriptor] - raise ArgumentError, 'data_descriptor is required' unless @data_descriptor - end - - def type - 'opaque_data' - end - end - end -end diff --git a/test/remote/gateways/remote_authorize_net_opaque_data_test.rb b/test/remote/gateways/remote_authorize_net_opaque_data_test.rb index 59063b0eb14..46d8c44f0cc 100644 --- a/test/remote/gateways/remote_authorize_net_opaque_data_test.rb +++ b/test/remote/gateways/remote_authorize_net_opaque_data_test.rb @@ -42,7 +42,7 @@ def test_successful_opaque_data_authorization_and_void end def test_failed_opaque_data_authorization - opaque_data_payment_token = OpaqueDataPaymentToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') + opaque_data_payment_token = ActiveMerchant::Billing::AuthorizeNetGateway::OpaqueDataToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') response = @gateway.authorize(@amount, opaque_data_payment_token, @options) assert_failure response assert_equal "OTS Service Error 'Field validation error.'", response.message @@ -50,7 +50,7 @@ def test_failed_opaque_data_authorization end def test_failed_opaque_data_purchase - opaque_data_payment_token = OpaqueDataPaymentToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') + opaque_data_payment_token = ActiveMerchant::Billing::AuthorizeNetGateway::OpaqueDataToken.new('garbage', data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT') response = @gateway.purchase(@amount, opaque_data_payment_token, @options) assert_failure response assert_equal "OTS Service Error 'Field validation error.'", response.message @@ -71,7 +71,7 @@ def generate_opaque_data_payment_token cc = credit_card('4000100011112224') options = { public_client_key: fetch_public_client_key, name: address[:name] } opaque_data = accept_js_gateway.accept_js_token(cc, options) - OpaqueDataPaymentToken.new(opaque_data[:data_value], data_descriptor: opaque_data[:data_descriptor]) + ActiveMerchant::Billing::AuthorizeNetGateway::OpaqueDataToken.new(opaque_data[:data_value], data_descriptor: opaque_data[:data_descriptor]) end class AcceptJsGateway < ActiveMerchant::Billing::AuthorizeNetGateway