Skip to content

Commit

Permalink
add opaque data token type so accept.js payments can be accepted
Browse files Browse the repository at this point in the history
  • Loading branch information
dingels35 committed May 30, 2019
1 parent 55977ad commit 66b0ac4
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 2 deletions.
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'
15 changes: 13 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
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: '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
20 changes: 20 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 Down

0 comments on commit 66b0ac4

Please sign in to comment.