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

Authnet: accept.js support #3224

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/opaque_data_payment_token'
require 'active_merchant/billing/response'
require 'active_merchant/billing/gateways'
require 'active_merchant/billing/gateway'
19 changes: 17 additions & 2 deletions lib/active_merchant/billing/gateways/authorize_net.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -887,6 +898,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
Expand Down
17 changes: 17 additions & 0 deletions lib/active_merchant/billing/opaque_data_payment_token.rb
Original file line number Diff line number Diff line change
@@ -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
135 changes: 135 additions & 0 deletions test/remote/gateways/remote_authorize_net_opaque_data_test.rb
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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
1 change: 1 addition & 0 deletions test/remote/gateways/remote_authorize_net_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions test/unit/gateways/authorize_net_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)

Expand All @@ -286,6 +306,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?
Expand Down