Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorize.Net: Support Accept.js PCI Tokens #4406

Closed
1 change: 1 addition & 0 deletions lib/active_merchant/billing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 17 additions & 0 deletions lib/active_merchant/billing/accept_js_token.rb
Original file line number Diff line number Diff line change
@@ -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
48 changes: 24 additions & 24 deletions lib/active_merchant/billing/gateways/authorize_net.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
13 changes: 12 additions & 1 deletion lib/active_merchant/billing/gateways/authorize_net_cim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
66 changes: 65 additions & 1 deletion test/remote/gateways/remote_authorize_net_cim_test.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions test/remote/gateways/remote_authorize_net_test.rb
Original file line number Diff line number Diff line change
@@ -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))

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading