diff --git a/lib/active_merchant/billing.rb b/lib/active_merchant/billing.rb index 55838882995..71e9bdf2774 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/accept_js_token' require 'active_merchant/billing/response' require 'active_merchant/billing/gateways' require 'active_merchant/billing/gateway' diff --git a/lib/active_merchant/billing/accept_js_token.rb b/lib/active_merchant/billing/accept_js_token.rb new file mode 100644 index 00000000000..dbd464369c5 --- /dev/null +++ b/lib/active_merchant/billing/accept_js_token.rb @@ -0,0 +1,17 @@ +module ActiveMerchant + module Billing + class AcceptJsToken < PaymentToken + def type + 'accept_js' + end + + def opaque_data + payment_data[:opaque_data] + end + + def display_number + @metadata[:card_number] + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/authorize_net.rb b/lib/active_merchant/billing/gateways/authorize_net.rb index aa9ad534c34..199930626de 100644 --- a/lib/active_merchant/billing/gateways/authorize_net.rb +++ b/lib/active_merchant/billing/gateways/authorize_net.rb @@ -90,6 +90,7 @@ class AuthorizeNetGateway < Gateway }.freeze APPLE_PAY_DATA_DESCRIPTOR = 'COMMON.APPLE.INAPP.PAYMENT' + ACCEPT_JS_DATA_DESCRIPTOR = 'COMMON.ACCEPT.INAPP.PAYMENT' PAYMENT_METHOD_NOT_SUPPORTED_ERROR = '155' INELIGIBLE_FOR_ISSUING_CREDIT_ERROR = '54' @@ -176,18 +177,18 @@ def credit(amount, payment, options = {}) end end - def verify(credit_card, options = {}) + def verify(payment, options = {}) MultiResponse.run(:use_first_response) do |r| - r.process { authorize(100, credit_card, options) } + r.process { authorize(100, payment, options) } r.process(:ignore_result) { void(r.authorization, options) } end end - def store(credit_card, options = {}) + def store(payment, options = {}) if options[:customer_profile_id] - create_customer_payment_profile(credit_card, options) + create_customer_payment_profile(payment, options) else - create_customer_profile(credit_card, options) + create_customer_profile(payment, options) end end @@ -399,6 +400,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) == 'accept_js' + add_accept_js_token(xml, source) else add_credit_card(xml, source, action) end @@ -518,8 +521,17 @@ def add_apple_pay_payment_token(xml, apple_pay_payment_token) end end + def add_accept_js_token(xml, accept_js_token) + xml.payment do + xml.opaqueData do + xml.dataDescriptor(accept_js_token.opaque_data[:data_descriptor]) + xml.dataValue(accept_js_token.opaque_data[:data_value]) + 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) || %w[check apple_pay accept_js].include?(card_brand(payment)) if valid_track_data xml.retail do @@ -731,23 +743,17 @@ def add_subsequent_auth_information(xml, options) end end - def create_customer_payment_profile(credit_card, options) + def create_customer_payment_profile(payment_source, options) commit(:cim_store_update, options) do |xml| xml.customerProfileId options[:customer_profile_id] xml.paymentProfile do - add_billing_address(xml, credit_card, options) - xml.payment do - xml.creditCard do - xml.cardNumber(truncate(credit_card.number, 16)) - xml.expirationDate(format(credit_card.year, :four_digits) + '-' + format(credit_card.month, :two_digits)) - xml.cardCode(credit_card.verification_value) if credit_card.verification_value - end - end + add_billing_address(xml, payment_source, options) + add_payment_source(xml, payment_source, options) end end end - def create_customer_profile(credit_card, options) + def create_customer_profile(payment_source, options) commit(:cim_store, options) do |xml| xml.profile do xml.merchantCustomerId(truncate(options[:merchant_customer_id], 20) || SecureRandom.hex(10)) @@ -756,15 +762,9 @@ def create_customer_profile(credit_card, options) xml.paymentProfiles do xml.customerType('individual') - add_billing_address(xml, credit_card, options) + add_billing_address(xml, payment_source, options) add_shipping_address(xml, options, 'shipToList') - xml.payment do - xml.creditCard do - xml.cardNumber(truncate(credit_card.number, 16)) - xml.expirationDate(format(credit_card.year, :four_digits) + '-' + format(credit_card.month, :two_digits)) - xml.cardCode(credit_card.verification_value) if credit_card.verification_value - end - end + add_payment_source(xml, payment_source, options) end end end diff --git a/lib/active_merchant/billing/gateways/authorize_net_cim.rb b/lib/active_merchant/billing/gateways/authorize_net_cim.rb index 09eff729308..d325bbea3f5 100644 --- a/lib/active_merchant/billing/gateways/authorize_net_cim.rb +++ b/lib/active_merchant/billing/gateways/authorize_net_cim.rb @@ -758,8 +758,10 @@ def add_payment_profile(xml, payment_profile) xml.tag!('payment') do add_credit_card(xml, payment_profile[:payment][:credit_card]) if payment_profile[:payment].has_key?(:credit_card) add_bank_account(xml, payment_profile[:payment][:bank_account]) if payment_profile[:payment].has_key?(:bank_account) - add_drivers_license(xml, payment_profile[:payment][:drivers_license]) if payment_profile[:payment].has_key?(:drivers_license) # This element is only required for Wells Fargo SecureSource eCheck.Net merchants + add_drivers_license(xml, payment_profile[:payment][:drivers_license]) if payment_profile[:payment].has_key?(:drivers_license) + add_opaque_data(xml, payment_profile[:payment][:opaque_data]) if payment_profile[:payment].has_key?(:opaque_data) + # The customer's Social Security Number or Tax ID xml.tag!('taxId', payment_profile[:payment]) if payment_profile[:payment].has_key?(:tax_id) end @@ -850,6 +852,15 @@ def add_drivers_license(xml, drivers_license) end end + def add_opaque_data(xml, opaque_data) + return unless opaque_data + # The generic payment data type used to process tokenized payment information + xml.tag!('opaqueData') do + xml.tag!('dataDescriptor', opaque_data[:data_descriptor]) + xml.tag!('dataValue', opaque_data[:data_value]) + end + end + def commit(action, request) url = test? ? test_url : live_url xml = ssl_post(url, request, 'Content-Type' => 'text/xml') diff --git a/test/remote/gateways/remote_authorize_net_cim_test.rb b/test/remote/gateways/remote_authorize_net_cim_test.rb index 9fe51696f8c..f0459ac5869 100644 --- a/test/remote/gateways/remote_authorize_net_cim_test.rb +++ b/test/remote/gateways/remote_authorize_net_cim_test.rb @@ -1,16 +1,21 @@ require 'test_helper' +require 'support/authorize_helper' require 'pp' class AuthorizeNetCimTest < Test::Unit::TestCase + include AuthorizeHelper + def setup Base.mode = :test @gateway = AuthorizeNetCimGateway.new(fixtures(:authorize_net)) @amount = 100 + @customer_profile_id = nil @credit_card = credit_card('4242424242424242') @payment = { credit_card: @credit_card } + @address = address @profile = { merchant_customer_id: 'Up to 20 chars', # Optional description: 'Up to 255 Characters', # Optional @@ -72,10 +77,38 @@ def test_successful_profile_create_get_update_and_delete assert response.test? assert_success response assert_nil response.authorization - assert response = @gateway.get_customer_profile(customer_profile_id: @customer_profile_id) + assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert_nil response.params['profile']['merchant_customer_id'] + assert_equal 'Up to 255 Characters', response.params['profile']['description'] assert_equal 'new email address', response.params['profile']['email'] end + def test_successful_profile_create_with_acceptjs + @options[:profile][:payment_profiles].delete(:payment) + token = get_sandbox_acceptjs_token_for_credit_card(credit_card) + @options[:profile][:payment_profiles][:payment] = token.payment_data + + assert response = @gateway.create_customer_profile(@options) + @customer_profile_id = response.authorization + + assert_success response + assert response.test? + + assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert response.test? + assert_success response + assert_equal @customer_profile_id, response.authorization + assert_equal 'Successful.', response.message + assert response.params['profile']['payment_profiles']['customer_payment_profile_id'] =~ /\d+/, 'The customer_payment_profile_id should be a number' + assert_equal "XXXX#{@credit_card.last_digits}", response.params['profile']['payment_profiles']['payment']['credit_card']['card_number'], "The card number should contain the last 4 digits of the card we passed in #{@credit_card.last_digits}" + assert_equal @profile[:merchant_customer_id], response.params['profile']['merchant_customer_id'] + assert_equal @profile[:description], response.params['profile']['description'] + assert_equal @profile[:email], response.params['profile']['email'] + assert_equal @profile[:payment_profiles][:customer_type], response.params['profile']['payment_profiles']['customer_type'] + assert_equal @profile[:ship_to_list][:phone_number], response.params['profile']['ship_to_list']['phone_number'] + assert_equal @profile[:ship_to_list][:company], response.params['profile']['ship_to_list']['company'] + end + def test_get_customer_profile_with_unmasked_exp_date_and_issuer_info assert response = @gateway.create_customer_profile(@options) @customer_profile_id = response.authorization @@ -88,6 +121,7 @@ def test_get_customer_profile_with_unmasked_exp_date_and_issuer_info unmask_expiration_date: true, include_issuer_info: true ) + assert response.test? assert_success response assert_equal @customer_profile_id, response.authorization @@ -196,6 +230,36 @@ def test_successful_create_customer_payment_profile_request payment_profile: payment_profile ) + assert response.test? + assert_success response + assert customer_payment_profile_id = response.params['customer_payment_profile_id'] + assert customer_payment_profile_id =~ /\d+/, "The customerPaymentProfileId should be numeric. It was #{customer_payment_profile_id}" + end + + def test_get_token_for_credit_card + assert token = get_sandbox_acceptjs_token_for_credit_card(credit_card) + assert_not_nil token.opaque_data[:data_value] + assert token.opaque_data[:data_descriptor] == 'COMMON.ACCEPT.INAPP.PAYMENT' + end + + def test_successful_create_customer_payment_profile_request_with_acceptjs + @options[:profile].delete(:payment_profiles) + assert response = @gateway.create_customer_profile(@options) + @customer_profile_id = response.authorization + + assert response = @gateway.get_customer_profile(:customer_profile_id => @customer_profile_id) + assert_nil response.params['profile']['payment_profiles'] + + token = get_sandbox_acceptjs_token_for_credit_card(credit_card) + + assert response = @gateway.create_customer_payment_profile( + :customer_profile_id => @customer_profile_id, + :payment_profile => { + :customer_type => 'individual', + :payment => token.payment_data + } + ) + assert response.test? assert_success response assert_equal @customer_profile_id, response.authorization diff --git a/test/remote/gateways/remote_authorize_net_test.rb b/test/remote/gateways/remote_authorize_net_test.rb index 339776c9766..41787a23828 100644 --- a/test/remote/gateways/remote_authorize_net_test.rb +++ b/test/remote/gateways/remote_authorize_net_test.rb @@ -1,6 +1,9 @@ require 'test_helper' +require 'support/authorize_helper' class RemoteAuthorizeNetTest < Test::Unit::TestCase + include AuthorizeHelper + def setup @gateway = AuthorizeNetGateway.new(fixtures(:authorize_net)) @@ -150,6 +153,13 @@ def test_successful_purchase_with_customer assert_equal 'This transaction has been approved', response.message end + def test_successful_purchase_with_acceptjs_token + acceptjs_token = get_sandbox_acceptjs_token_for_credit_card(@credit_card) + response = @gateway.purchase(@amount, acceptjs_token, @options.merge(description: 'Accept.js Store Purchase')) + assert_success response + assert_equal 'This transaction has been approved', response.message + end + def test_failed_purchase response = @gateway.purchase(@amount, @declined_card, @options) assert_failure response @@ -383,6 +393,16 @@ def test_successful_authorization_with_moto_retail_type assert response.authorization end + def test_authorization_and_void_acceptjs + acceptjs_token = get_sandbox_acceptjs_token_for_credit_card(@credit_card) + assert authorization = @gateway.authorize(@amount, acceptjs_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_successful_verify response = @gateway.verify(@credit_card, @options) assert_success response @@ -405,6 +425,15 @@ def test_successful_store assert_equal '1', response.params['message_code'] end + def test_successful_store_acceptjs + acceptjs_token = get_sandbox_acceptjs_token_for_credit_card(@credit_card) + assert response = @gateway.store(acceptjs_token) + assert_success response + assert response.authorization + assert_equal 'Successful', response.message + assert_equal '1', response.params['message_code'] + end + def test_successful_store_new_payment_profile assert store = @gateway.store(@credit_card) assert_success store @@ -419,6 +448,20 @@ def test_successful_store_new_payment_profile assert_equal '1', response.params['message_code'] end + def test_successful_store_new_payment_profile_acceptjs + assert store = @gateway.store(@credit_card) + assert_success store + assert store.authorization + + customer_profile_id, _, _ = store.authorization.split('#') + acceptjs_token = get_sandbox_acceptjs_token_for_credit_card(@credit_card) + + assert response = @gateway.store(acceptjs_token, customer_profile_id: customer_profile_id) + assert_success response + assert_equal 'Successful', response.message + assert_equal '1', response.params['message_code'] + end + def test_failed_store_new_payment_profile assert store = @gateway.store(@credit_card) assert_success store @@ -722,6 +765,14 @@ def test_successful_credit assert response.authorization end + def test_successful_credit_acceptjs + acceptjs_token = get_sandbox_acceptjs_token_for_credit_card(@credit_card) + response = @gateway.credit(@amount, acceptjs_token, @options.merge(description: 'Accept.js Store Refund')) + assert_success response + assert_equal 'This transaction has been approved', response.message + assert response.authorization + end + def test_successful_echeck_credit response = @gateway.credit(@amount, @check, @options) assert_equal 'The transaction is currently under review', response.message diff --git a/test/support/authorize_helper.rb b/test/support/authorize_helper.rb new file mode 100644 index 00000000000..3a5ea25f060 --- /dev/null +++ b/test/support/authorize_helper.rb @@ -0,0 +1,126 @@ +require 'test_helper' + +module AuthorizeHelper + def acceptjs_token(options = {}) + defaults = { + opaque_data: { + data_value: '1234567890ABCDEF1111AAAA2222BBBB3333CCCC4444DDDD5555EEEE6666FFFF7777888899990000', + data_descriptor: 'COMMON.ACCEPT.INAPP.PAYMENT' + } + }.update(options) + + ActiveMerchant::Billing::AcceptJsToken.new(defaults) + end + + def get_sandbox_acceptjs_token_for_credit_card(credit_card) + sandbox_endpoint = 'https://apitest.authorize.net/xml/v1/request.api' + sandbox_credentials = fixtures(:authorize_net) + + AcceptJsTestToken.new( + credit_card, + endpoint: sandbox_endpoint, + credentials: sandbox_credentials + ).token + end + + class AcceptJsTestToken + attr_reader :credit_card, :options + + def initialize(credit_card, options = {}) + @credit_card = credit_card + @options = { + }.merge(options) + end + + def token + xml = build_xml + response = make_request xml + opaque_data = parse_response response + + ActiveMerchant::Billing::AcceptJsToken.new(opaque_data: opaque_data) + end + + private + + def build_xml + xml = Builder::XmlMarkup.new(:indent => 2) + xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') + xml.tag!('securePaymentContainerRequest', :xmlns => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd') do + xml.tag!('merchantAuthentication') do + creds = options[:credentials] + xml.tag!('name', creds[:login]) + xml.tag!('transactionKey', creds[:password]) + end + xml.tag!('refId', options[:ref_id]) if options[:ref_id] + xml.tag!('data') do + xml.tag!('type', 'TOKEN') + xml.tag!('id', SecureRandom.uuid) + xml.tag!('token') do + xml.tag!('cardNumber', credit_card.number) + xml.tag!('expirationDate', ('%02d' % credit_card.month) + credit_card.year.to_s) + xml.tag!('cardCode', credit_card.verification_value) + xml.tag!('fullName', "#{credit_card.first_name} #{credit_card.last_name}") + end + end + end + + xml + end + + def make_request(xml) + uri = URI.parse(options[:endpoint]) + req = Net::HTTP::Post.new(uri.path) + req.body = xml.target! + req.content_type = 'text/xml' + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + + resp = http.request(req) + + raise "HTTP #{resp.code} response code tokenizing test card" if resp.code != '200' + raise "HTTP incorrect content type #{resp.content_type} tokenizing test card" if resp.content_type != 'application/xml' + + resp + end + + def parse_response(response) + xml = REXML::Document.new(response.body) + root = REXML::XPath.first(xml, '//securePaymentContainerResponse') || + REXML::XPath.first(xml, '//ErrorResponse') + if root + response = parse_element(root) + end + + raise "Result code #{response['messages']['result_code']}: #{response['messages']['message']['code']}" if response['messages']['result_code'] != 'Ok' + + { + data_descriptor: response['opaque_data']['data_descriptor'], + data_value: response['opaque_data']['data_value'] + } + end + + def parse_element(node) + if node.has_elements? + response = {} + node.elements.each { |e| + key = e.name.underscore + value = parse_element(e) + if response.has_key?(key) + if response[key].is_a?(Array) + response[key].push(value) + else + response[key] = [response[key], value] + end + else + response[key] = parse_element(e) + end + } + else + response = node.text + end + + response + end + end +end diff --git a/test/unit/gateways/authorize_net_cim_test.rb b/test/unit/gateways/authorize_net_cim_test.rb index 9ce7d2288d6..f8b6b0bea2b 100644 --- a/test/unit/gateways/authorize_net_cim_test.rb +++ b/test/unit/gateways/authorize_net_cim_test.rb @@ -1,6 +1,8 @@ require 'test_helper' +require 'support/authorize_helper' class AuthorizeNetCimTest < Test::Unit::TestCase + include AuthorizeHelper include CommStub def setup @@ -10,6 +12,7 @@ def setup ) @amount = 100 @credit_card = credit_card + @accept_js_token = acceptjs_token @address = address @customer_profile_id = '3187' @customer_payment_profile_id = '7813' @@ -61,6 +64,18 @@ def test_should_create_customer_profile_request assert_equal 'Successful.', response.message end + def test_should_create_customer_profile_request_with_acceptjs + @gateway.expects(:ssl_post).returns(successful_create_customer_profile_response) + + @options[:profile][:payment_profiles][:payment] = acceptjs_token.payment_data + + assert response = @gateway.create_customer_profile(@options) + assert_instance_of Response, response + assert_success response + assert_equal @customer_profile_id, response.authorization + assert_equal 'Successful.', response.message + end + def test_should_create_customer_payment_profile_request @gateway.expects(:ssl_post).returns(successful_create_customer_payment_profile_response) @@ -79,6 +94,24 @@ def test_should_create_customer_payment_profile_request assert_equal 'This output is only present if the ValidationMode input parameter is passed with a value of testMode or liveMode', response.params['validation_direct_response'] end + def test_should_create_customer_payment_profile_request_with_acceptjs + @gateway.expects(:ssl_post).returns(successful_create_customer_payment_profile_response) + + assert response = @gateway.create_customer_payment_profile( + :customer_profile_id => @customer_profile_id, + :payment_profile => { + :customer_type => 'individual', + :bill_to => @address, + :payment => acceptjs_token.payment_data + }, + :validation_mode => :test + ) + assert_instance_of Response, response + assert_success response + assert_equal @customer_payment_profile_id, response.params['customer_payment_profile_id'] + assert_equal 'This output is only present if the ValidationMode input parameter is passed with a value of testMode or liveMode', response.params['validation_direct_response'] + end + def test_should_create_customer_shipping_address_request @gateway.expects(:ssl_post).returns(successful_create_customer_shipping_address_response) diff --git a/test/unit/gateways/authorize_net_test.rb b/test/unit/gateways/authorize_net_test.rb index 4e6a8ed3988..3788deceeb9 100644 --- a/test/unit/gateways/authorize_net_test.rb +++ b/test/unit/gateways/authorize_net_test.rb @@ -1,6 +1,8 @@ require 'test_helper' +require 'support/authorize_helper' class AuthorizeNetTest < Test::Unit::TestCase + include AuthorizeHelper include CommStub BAD_TRACK_DATA = '%B378282246310005LONGSONLONGBOB1705101130504392?' @@ -22,6 +24,7 @@ def setup payment_network: 'Visa', transaction_identifier: 'transaction123' ) + @acceptjs_token = acceptjs_token @options = { order_id: '1', @@ -325,6 +328,22 @@ def test_successful_purchase assert_equal 'CVV not processed', response.cvv_result['message'] end + def test_successful_acceptjs_purchase + response = stub_comms do + @gateway.purchase(@amount, @acceptjs_token) + end.check_request do |endpoint, data, headers| + parse(data) do |doc| + assert_equal @gateway.class::ACCEPT_JS_DATA_DESCRIPTOR, doc.at_xpath('//opaqueData/dataDescriptor').content + assert_equal @acceptjs_token.opaque_data[:data_value], doc.at_xpath('//opaqueData/dataValue').content + end + end.respond_with(successful_purchase_response) + + assert response + assert_instance_of Response, response + assert_success response + assert_equal '508141795', response.authorization.split('#')[0] + end + def test_successful_purchase_with_utf_character stub_comms do @gateway.purchase(@amount, credit_card('4000100011112224', last_name: 'Wåhlin')) @@ -859,6 +878,16 @@ def test_successful_store assert_equal '32506918', store.params['customer_payment_profile_id'] end + def test_successful_acceptjs_store + @gateway.expects(:ssl_post).returns(successful_store_response) + + store = @gateway.store(@acceptjs_token, @options) + assert_success store + assert_equal 'Successful', store.message + assert_equal '35959426', store.params['customer_profile_id'] + assert_equal '32506918', store.params['customer_payment_profile_id'] + end + def test_failed_store @gateway.expects(:ssl_post).returns(failed_store_response)