lib/active_merchant/billing/gateways/authorize_net.rb
require 'nokogiri'
module ActiveMerchant
module Billing
class AuthorizeNetGateway < Gateway
include Empty
self.test_url = 'https://apitest.authorize.net/xml/v1/request.api'
self.live_url = 'https://api2.authorize.net/xml/v1/request.api'
self.supported_countries = %w(AU CA US)
self.default_currency = 'USD'
self.money_format = :dollars
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro]
self.homepage_url = 'http://www.authorize.net/'
self.display_name = 'Authorize.Net'
# Authorize.net has slightly different definitions for returned AVS codes
# that have been mapped to the closest equivalent AM standard AVSResult codes
# Authorize.net's descriptions noted below
STANDARD_AVS_CODE_MAPPING = {
'A' => 'A', # Street Address: Match -- First 5 Digits of ZIP: No Match
'B' => 'I', # Address not provided for AVS check or street address match, postal code could not be verified
'E' => 'E', # AVS Error
'G' => 'G', # Non U.S. Card Issuing Bank
'N' => 'N', # Street Address: No Match -- First 5 Digits of ZIP: No Match
'P' => 'I', # AVS not applicable for this transaction
'R' => 'R', # Retry, System Is Unavailable
'S' => 'S', # AVS Not Supported by Card Issuing Bank
'U' => 'U', # Address Information For This Cardholder Is Unavailable
'W' => 'W', # Street Address: No Match -- All 9 Digits of ZIP: Match
'X' => 'X', # Street Address: Match -- All 9 Digits of ZIP: Match
'Y' => 'Y', # Street Address: Match - First 5 Digits of ZIP: Match
'Z' => 'Z' # Street Address: No Match - First 5 Digits of ZIP: Match
}
STANDARD_ERROR_CODE_MAPPING = {
'2127' => STANDARD_ERROR_CODE[:incorrect_address],
'22' => STANDARD_ERROR_CODE[:card_declined],
'227' => STANDARD_ERROR_CODE[:incorrect_address],
'23' => STANDARD_ERROR_CODE[:card_declined],
'2315' => STANDARD_ERROR_CODE[:invalid_number],
'2316' => STANDARD_ERROR_CODE[:invalid_expiry_date],
'2317' => STANDARD_ERROR_CODE[:expired_card],
'235' => STANDARD_ERROR_CODE[:processing_error],
'237' => STANDARD_ERROR_CODE[:invalid_number],
'24' => STANDARD_ERROR_CODE[:pickup_card],
'244' => STANDARD_ERROR_CODE[:incorrect_cvc],
'300' => STANDARD_ERROR_CODE[:config_error],
'3153' => STANDARD_ERROR_CODE[:processing_error],
'3155' => STANDARD_ERROR_CODE[:unsupported_feature],
'36' => STANDARD_ERROR_CODE[:incorrect_number],
'37' => STANDARD_ERROR_CODE[:invalid_expiry_date],
'378' => STANDARD_ERROR_CODE[:invalid_cvc],
'38' => STANDARD_ERROR_CODE[:expired_card],
'384' => STANDARD_ERROR_CODE[:config_error]
}
MARKET_TYPE = {
moto: '1',
retail: '2'
}
DEVICE_TYPE = {
unknown: '1',
unattended_terminal: '2',
self_service_terminal: '3',
electronic_cash_register: '4',
personal_computer_terminal: '5',
airpay: '6',
wireless_pos: '7',
website: '8',
dial_terminal: '9',
virtual_terminal: '10'
}
class_attribute :duplicate_window
APPROVED, DECLINED, ERROR, FRAUD_REVIEW = 1, 2, 3, 4
TRANSACTION_ALREADY_ACTIONED = %w(310 311)
CARD_CODE_ERRORS = %w(N S)
AVS_ERRORS = %w(A E I N R W Z)
AVS_REASON_CODES = %w(27 45)
TRACKS = {
1 => /^%(?<format_code>.)(?<pan>[\d]{1,19}+)\^(?<name>.{2,26})\^(?<expiration>[\d]{0,4}|\^)(?<service_code>[\d]{0,3}|\^)(?<discretionary_data>.*)\?\Z/,
2 => /\A;(?<pan>[\d]{1,19}+)=(?<expiration>[\d]{0,4}|=)(?<service_code>[\d]{0,3}|=)(?<discretionary_data>.*)\?\Z/
}.freeze
PAYMENT_METHOD_NOT_SUPPORTED_ERROR = '155'
INELIGIBLE_FOR_ISSUING_CREDIT_ERROR = '54'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def purchase(amount, payment, options = {})
if payment.is_a?(String)
commit(:cim_purchase, options) do |xml|
add_cim_auth_purchase(xml, 'profileTransAuthCapture', amount, payment, options)
end
else
commit(:purchase) do |xml|
add_auth_purchase(xml, 'authCaptureTransaction', amount, payment, options)
end
end
end
def authorize(amount, payment, options = {})
if payment.is_a?(String)
commit(:cim_authorize, options) do |xml|
add_cim_auth_purchase(xml, 'profileTransAuthOnly', amount, payment, options)
end
else
commit(:authorize) do |xml|
add_auth_purchase(xml, 'authOnlyTransaction', amount, payment, options)
end
end
end
def capture(amount, authorization, options = {})
if auth_was_for_cim?(authorization)
cim_capture(amount, authorization, options)
else
normal_capture(amount, authorization, options)
end
end
def refund(amount, authorization, options = {})
response =
if auth_was_for_cim?(authorization)
cim_refund(amount, authorization, options)
else
normal_refund(amount, authorization, options)
end
return response if response.success?
return response unless options[:force_full_refund_if_unsettled]
if response.params['response_reason_code'] == INELIGIBLE_FOR_ISSUING_CREDIT_ERROR
void(authorization, options)
else
response
end
end
def void(authorization, options = {})
if auth_was_for_cim?(authorization)
cim_void(authorization, options)
else
normal_void(authorization, options)
end
end
def credit(amount, payment, options = {})
raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' if payment.is_a?(String)
commit(:credit) do |xml|
add_order_id(xml, options)
xml.transactionRequest do
xml.transactionType('refundTransaction')
xml.amount(amount(amount))
add_payment_method(xml, payment, options, :credit)
xml.refTransId(transaction_id_from(options[:transaction_id])) if options[:transaction_id]
add_invoice(xml, 'refundTransaction', options)
add_customer_data(xml, payment, options)
add_settings(xml, payment, options)
add_user_fields(xml, amount, options)
end
end
end
def verify(payment_method, options = {})
amount = amount_for_verify(options)
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(amount, payment_method, options) }
r.process(:ignore_result) { void(r.authorization, options) } unless amount == 0
end
rescue ArgumentError => e
Response.new(false, e.message)
end
def amount_for_verify(options)
return 100 unless options[:verify_amount].present?
amount = options[:verify_amount]
raise ArgumentError.new 'verify_amount value must be an integer' unless amount.is_a?(Integer) && !amount.negative? || amount.is_a?(String) && amount.match?(/^\d+$/) && !amount.to_i.negative?
raise ArgumentError.new 'Billing address including zip code is required for a 0 amount verify' if amount.to_i.zero? && !validate_billing_address_values?(options)
amount.to_i
end
def validate_billing_address_values?(options)
options.dig(:billing_address, :zip).present? && options.dig(:billing_address, :address1).present?
end
def store(credit_card, options = {})
if options[:customer_profile_id]
create_customer_payment_profile(credit_card, options)
else
create_customer_profile(credit_card, options)
end
end
def unstore(authorization)
customer_profile_id, = split_authorization(authorization)
delete_customer_profile(customer_profile_id)
end
def verify_credentials
response = commit(:verify_credentials) {}
response.success?
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((<transactionKey>).+(</transactionKey>)), '\1[FILTERED]\2').
gsub(%r((<cardNumber>).+(</cardNumber>)), '\1[FILTERED]\2').
gsub(%r((<cardCode>).+(</cardCode>)), '\1[FILTERED]\2').
gsub(%r((<track1>).+(</track1>)), '\1[FILTERED]\2').
gsub(%r((<track2>).+(</track2>)), '\1[FILTERED]\2').
gsub(/(<routingNumber>).+(<\/routingNumber>)/, '\1[FILTERED]\2').
gsub(/(<accountNumber>).+(<\/accountNumber>)/, '\1[FILTERED]\2').
gsub(%r((<cryptogram>).+(</cryptogram>)), '\1[FILTERED]\2')
end
def supports_network_tokenization?
card = Billing::NetworkTokenizationCreditCard.new({
number: '4111111111111111',
month: 12,
year: 20,
first_name: 'John',
last_name: 'Smith',
brand: 'visa',
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk='
})
request = post_data(:authorize) do |xml|
add_auth_purchase(xml, 'authOnlyTransaction', 1, card, {})
end
raw_response = ssl_post(url, request, headers)
response = parse(:authorize, raw_response)
response[:response_reason_code].to_s != PAYMENT_METHOD_NOT_SUPPORTED_ERROR
end
private
def add_auth_purchase(xml, transaction_type, amount, payment, options)
add_order_id(xml, options)
xml.transactionRequest do
xml.transactionType(transaction_type)
xml.amount(amount(amount))
add_payment_method(xml, payment, options)
add_invoice(xml, transaction_type, options)
add_tax_fields(xml, options)
add_duty_fields(xml, options)
add_shipping_fields(xml, options)
add_tax_exempt_status(xml, options)
add_po_number(xml, options)
add_customer_data(xml, payment, options)
add_market_type_device_type(xml, payment, options)
add_settings(xml, payment, options)
add_user_fields(xml, amount, options)
add_surcharge_fields(xml, options)
add_ship_from_address(xml, options)
add_processing_options(xml, options)
add_subsequent_auth_information(xml, options)
end
end
def add_cim_auth_purchase(xml, transaction_type, amount, payment, options)
add_order_id(xml, options)
xml.transaction do
xml.send(transaction_type) do
xml.amount(amount(amount))
add_tax_fields(xml, options)
add_shipping_fields(xml, options)
add_duty_fields(xml, options)
add_payment_method(xml, payment, options)
add_invoice(xml, transaction_type, options)
add_surcharge_fields(xml, options)
add_tax_exempt_status(xml, options)
end
end
add_extra_options_for_cim(xml, options)
end
def cim_capture(amount, authorization, options)
commit(:cim_capture, options) do |xml|
add_order_id(xml, options)
xml.transaction do
xml.profileTransPriorAuthCapture do
xml.amount(amount(amount))
add_tax_fields(xml, options)
add_shipping_fields(xml, options)
add_duty_fields(xml, options)
xml.transId(transaction_id_from(authorization))
end
end
add_extra_options_for_cim(xml, options)
end
end
def normal_capture(amount, authorization, options)
commit(:capture) do |xml|
add_order_id(xml, options)
xml.transactionRequest do
xml.transactionType('priorAuthCaptureTransaction')
xml.amount(amount(amount))
add_tax_fields(xml, options)
add_duty_fields(xml, options)
add_shipping_fields(xml, options)
add_tax_exempt_status(xml, options)
add_po_number(xml, options)
xml.refTransId(transaction_id_from(authorization))
add_invoice(xml, 'capture', options)
add_user_fields(xml, amount, options)
end
end
end
def cim_refund(amount, authorization, options)
transaction_id, card_number, = split_authorization(authorization)
commit(:cim_refund, options) do |xml|
add_order_id(xml, options)
xml.transaction do
xml.profileTransRefund do
xml.amount(amount(amount))
add_tax_fields(xml, options)
add_shipping_fields(xml, options)
add_duty_fields(xml, options)
xml.creditCardNumberMasked(card_number)
add_invoice(xml, 'profileTransRefund', options)
xml.transId(transaction_id)
end
end
add_extra_options_for_cim(xml, options)
end
end
def normal_refund(amount, authorization, options)
transaction_id, card_number, = split_authorization(authorization)
commit(:refund) do |xml|
xml.transactionRequest do
xml.transactionType('refundTransaction')
xml.amount(amount.nil? ? 0 : amount(amount))
xml.payment do
if options[:routing_number]
xml.bankAccount do
xml.accountType(options[:account_type])
xml.routingNumber(options[:routing_number])
xml.accountNumber(options[:account_number])
xml.nameOnAccount(truncate("#{options[:first_name]} #{options[:last_name]}", 22))
end
else
xml.creditCard do
xml.cardNumber(card_number || options[:card_number])
xml.expirationDate('XXXX')
end
end
end
xml.refTransId(transaction_id)
add_invoice(xml, 'refundTransaction', options)
add_tax_fields(xml, options)
add_duty_fields(xml, options)
add_shipping_fields(xml, options)
add_tax_exempt_status(xml, options)
add_po_number(xml, options)
add_customer_data(xml, nil, options)
add_user_fields(xml, amount, options)
end
end
end
def cim_void(authorization, options)
commit(:cim_void, options) do |xml|
add_order_id(xml, options)
xml.transaction do
xml.profileTransVoid do
xml.transId(transaction_id_from(authorization))
end
end
add_extra_options_for_cim(xml, options)
end
end
def normal_void(authorization, options)
commit(:void) do |xml|
add_order_id(xml, options)
xml.transactionRequest do
xml.transactionType('voidTransaction')
xml.refTransId(transaction_id_from(authorization))
end
end
end
def add_payment_method(xml, payment_method, options, action = nil)
return unless payment_method
case payment_method
when String
add_token_payment_method(xml, payment_method, options)
when Check
add_check(xml, payment_method)
else
if network_token?(payment_method, options, action)
add_network_token(xml, payment_method)
else
add_credit_card(xml, payment_method, action)
end
end
end
def network_token?(payment_method, options, action)
payment_method.class == NetworkTokenizationCreditCard && action != :credit && options[:turn_on_nt_flow]
end
def camel_case_lower(key)
String(key).split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join
end
def add_settings(xml, source, options)
xml.transactionSettings do
if options[:recurring] || subsequent_recurring_transaction?(options)
xml.setting do
xml.settingName('recurringBilling')
xml.settingValue('true')
end
end
if options[:disable_partial_auth]
xml.setting do
xml.settingName('allowPartialAuth')
xml.settingValue('false')
end
end
if options[:duplicate_window]
set_duplicate_window(xml, options[:duplicate_window])
elsif self.class.duplicate_window
ActiveMerchant.deprecated 'Using the duplicate_window class_attribute is deprecated. Use the transaction options hash instead.'
set_duplicate_window(xml, self.class.duplicate_window)
end
if options.key?(:email_customer)
xml.setting do
xml.settingName('emailCustomer')
xml.settingValue(options[:email_customer] ? 'true' : 'false')
end
end
if options[:header_email_receipt]
xml.setting do
xml.settingName('headerEmailReceipt')
xml.settingValue(options[:header_email_receipt])
end
end
if options[:test_request]
xml.setting do
xml.settingName('testRequest')
xml.settingValue('1')
end
end
end
end
def set_duplicate_window(xml, value)
xml.setting do
xml.settingName('duplicateWindow')
xml.settingValue(value)
end
end
def add_user_fields(xml, amount, options)
xml.userFields do
if currency = (options[:currency] || currency(amount))
xml.userField do
xml.name('x_currency_code')
xml.value(currency)
end
end
if application_id.present?
xml.userField do
xml.name('x_solution_id')
xml.value(application_id)
end
end
end
end
def add_credit_card(xml, credit_card, action)
if credit_card.track_data
add_swipe_data(xml, credit_card)
else
xml.payment do
xml.creditCard do
xml.cardNumber(truncate(credit_card.number, 16))
xml.expirationDate(format(credit_card.month, :two_digits) + '/' + format(credit_card.year, :four_digits))
xml.cardCode(credit_card.verification_value) if credit_card.valid_card_verification_value?(credit_card.verification_value, credit_card.brand)
xml.cryptogram(credit_card.payment_cryptogram) if credit_card.is_a?(NetworkTokenizationCreditCard) && action != :credit
end
end
end
end
def add_swipe_data(xml, credit_card)
TRACKS.each do |key, regex|
if regex.match?(credit_card.track_data)
@valid_track_data = true
xml.payment do
xml.trackData do
xml.public_send(:"track#{key}", credit_card.track_data)
end
end
end
end
end
def add_token_payment_method(xml, token, options)
customer_profile_id, customer_payment_profile_id, = split_authorization(token)
customer_profile_id = options[:customer_profile_id] if options[:customer_profile_id]
customer_payment_profile_id = options[:customer_payment_profile_id] if options[:customer_payment_profile_id]
xml.customerProfileId(customer_profile_id)
xml.customerPaymentProfileId(customer_payment_profile_id)
end
def add_network_token(xml, payment_method)
xml.payment do
xml.creditCard do
xml.cardNumber(truncate(payment_method.number, 16))
xml.expirationDate(format(payment_method.month, :two_digits) + '/' + format(payment_method.year, :four_digits))
xml.isPaymentToken(true)
xml.cryptogram(payment_method.payment_cryptogram)
end
end
end
def add_market_type_device_type(xml, payment, options)
return unless payment.is_a?(CreditCard)
return if payment.is_a?(NetworkTokenizationCreditCard)
if valid_track_data
xml.retail do
xml.marketType(options[:market_type] || MARKET_TYPE[:retail])
xml.deviceType(options[:device_type] || DEVICE_TYPE[:wireless_pos])
end
elsif payment.manual_entry
xml.retail do
xml.marketType(options[:market_type] || MARKET_TYPE[:moto])
end
else
if options[:market_type]
xml.retail do
xml.marketType(options[:market_type])
end
end
end
end
def valid_track_data
@valid_track_data ||= false
end
def add_check(xml, check)
xml.payment do
xml.bankAccount do
xml.accountType(check.account_type)
xml.routingNumber(check.routing_number)
xml.accountNumber(check.account_number)
xml.nameOnAccount(truncate(check.name, 22))
xml.bankName(check.bank_name)
xml.checkNumber(check.number)
end
end
end
def add_customer_data(xml, payment_source, options)
xml.customer do
xml.id(options[:customer]) unless empty?(options[:customer]) || options[:customer] !~ /^\w+$/
xml.email(options[:email]) unless empty?(options[:email])
end
add_billing_address(xml, payment_source, options)
add_shipping_address(xml, options)
xml.customerIP(options[:ip]) unless empty?(options[:ip])
if !empty?(options.fetch(:three_d_secure, {})) || options[:authentication_indicator] || options[:cardholder_authentication_value]
xml.cardholderAuthentication do
three_d_secure = options.fetch(:three_d_secure, {})
xml.authenticationIndicator(
options[:authentication_indicator] || three_d_secure[:eci]
)
xml.cardholderAuthenticationValue(
options[:cardholder_authentication_value] || three_d_secure[:cavv]
)
end
end
end
def add_billing_address(xml, payment_source, options)
address = options[:billing_address] || options[:address] || {}
xml.billTo do
first_name, last_name = names_from(payment_source, address, options)
state = state_from(address, options)
full_address = "#{address[:address1]} #{address[:address2]}".strip
phone = address[:phone] || address[:phone_number] || ''
xml.firstName(truncate(first_name, 50)) unless empty?(first_name)
xml.lastName(truncate(last_name, 50)) unless empty?(last_name)
xml.company(truncate(address[:company], 50)) unless empty?(address[:company])
xml.address(truncate(full_address, 60))
xml.city(truncate(address[:city], 40))
xml.state(truncate(state, 40))
xml.zip(truncate((address[:zip] || options[:zip]), 20))
xml.country(truncate(address[:country], 60))
xml.phoneNumber(truncate(phone, 25)) unless empty?(phone)
xml.faxNumber(truncate(address[:fax], 25)) unless empty?(address[:fax])
end
end
def add_shipping_address(xml, options, root_node = 'shipTo')
address = options[:shipping_address] || options[:address]
return unless address
xml.send(root_node) do
first_name, last_name =
if address[:name]
split_names(address[:name])
else
[address[:first_name], address[:last_name]]
end
full_address = "#{address[:address1]} #{address[:address2]}".strip
xml.firstName(truncate(first_name, 50)) unless empty?(first_name)
xml.lastName(truncate(last_name, 50)) unless empty?(last_name)
xml.company(truncate(address[:company], 50)) unless empty?(address[:company])
xml.address(truncate(full_address, 60))
xml.city(truncate(address[:city], 40))
xml.state(truncate(address[:state], 40))
xml.zip(truncate(address[:zip], 20))
xml.country(truncate(address[:country], 60))
end
end
def add_ship_from_address(xml, options, root_node = 'shipFrom')
address = options[:ship_from_address]
return unless address
xml.send(root_node) do
xml.zip(truncate(address[:zip], 20)) unless empty?(address[:zip])
xml.country(truncate(address[:country], 60)) unless empty?(address[:country])
end
end
def add_order_id(xml, options)
xml.refId(truncate(options[:order_id], 20))
end
def add_invoice(xml, transaction_type, options)
xml.order do
xml.invoiceNumber(truncate(options[:order_id], 20))
xml.description(truncate(options[:description], 255))
xml.purchaseOrderNumber(options[:po_number]) if options[:po_number] && transaction_type.start_with?('profileTrans')
xml.summaryCommodityCode(truncate(options[:summary_commodity_code], 4)) if options[:summary_commodity_code] && !transaction_type.start_with?('profileTrans')
end
# Authorize.net API requires lineItems to be placed directly after order tag
if options[:line_items]
xml.lineItems do
options[:line_items].each do |line_item|
xml.lineItem do
line_item.each do |key, value|
xml.send(camel_case_lower(key), value)
end
end
end
end
end
end
def add_tax_fields(xml, options)
tax = options[:tax]
if tax.is_a?(Hash)
xml.tax do
xml.amount(amount(tax[:amount].to_i))
xml.name(tax[:name])
xml.description(tax[:description])
end
end
end
def add_duty_fields(xml, options)
duty = options[:duty]
if duty.is_a?(Hash)
xml.duty do
xml.amount(amount(duty[:amount].to_i))
xml.name(duty[:name])
xml.description(duty[:description])
end
end
end
def add_surcharge_fields(xml, options)
surcharge = options[:surcharge] if options[:surcharge]
if surcharge.is_a?(Hash)
xml.surcharge do
xml.amount(amount(surcharge[:amount].to_i)) if surcharge[:amount]
xml.description(surcharge[:description]) if surcharge[:description]
end
end
end
def add_shipping_fields(xml, options)
shipping = options[:shipping]
if shipping.is_a?(Hash)
xml.shipping do
xml.amount(amount(shipping[:amount].to_i))
xml.name(shipping[:name])
xml.description(shipping[:description])
end
end
end
def add_tax_exempt_status(xml, options)
xml.taxExempt(options[:tax_exempt]) if options[:tax_exempt]
end
def add_po_number(xml, options)
xml.poNumber(options[:po_number]) if options[:po_number]
end
def add_extra_options_for_cim(xml, options)
xml.extraOptions("x_delim_char=#{options[:delimiter]}") if options[:delimiter]
end
def add_processing_options(xml, options)
return unless options[:stored_credential]
xml.processingOptions do
if options[:stored_credential][:initial_transaction] && options[:stored_credential][:reason_type] == 'recurring'
xml.isFirstRecurringPayment 'true'
elsif options[:stored_credential][:initial_transaction]
xml.isFirstSubsequentAuth 'true'
elsif options[:stored_credential][:initiator] == 'cardholder'
xml.isStoredCredentials 'true'
else
xml.isSubsequentAuth 'true'
end
end
end
def add_subsequent_auth_information(xml, options)
return unless options.dig(:stored_credential, :initiator) == 'merchant'
xml.subsequentAuthInformation do
xml.reason options[:stored_credential_reason_type_override] if options[:stored_credential_reason_type_override]
xml.originalNetworkTransId options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id]
end
end
def create_customer_payment_profile(credit_card, options)
commit(:cim_store_update, options) do |xml|
xml.customerProfileId options[:customer_profile_id]
xml.paymentProfile do
add_billing_address(xml, credit_card, options)
add_credit_card(xml, credit_card, :cim_store_update)
end
end
end
def create_customer_profile(credit_card, options)
commit(:cim_store, options) do |xml|
xml.profile do
xml.merchantCustomerId(truncate(options[:merchant_customer_id], 20) || SecureRandom.hex(10))
xml.description(truncate(options[:description], 255)) unless empty?(options[:description])
xml.email(options[:email]) unless empty?(options[:email])
xml.paymentProfiles do
xml.customerType('individual')
add_billing_address(xml, credit_card, options)
add_shipping_address(xml, options, 'shipToList')
add_credit_card(xml, credit_card, :cim_store)
end
end
end
end
def delete_customer_profile(customer_profile_id)
commit(:cim_store_delete_customer, options) do |xml|
xml.customerProfileId(customer_profile_id)
end
end
def names_from(payment_source, address, options)
if payment_source && !payment_source.is_a?(PaymentToken) && !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
[options[:first_name], options[:last_name]]
end
end
def state_from(address, options)
if %w[US CA].include?(address[:country])
address[:state] || 'NC'
else
address[:state] || 'n/a'
end
end
def subsequent_recurring_transaction?(options)
options.dig(:stored_credential, :reason_type) == 'recurring' && !options.dig(:stored_credential, :initial_transaction)
end
def headers
{ 'Content-Type' => 'text/xml' }
end
def url
test? ? test_url : live_url
end
def parse(action, raw_response, options = {})
if cim_action?(action) || action == :verify_credentials
parse_cim(raw_response, options)
else
parse_normal(action, raw_response)
end
end
def commit(action, options = {}, &payload)
raw_response = ssl_post(url, post_data(action, &payload), headers)
response = parse(action, raw_response, options)
avs_result_code = response[:avs_result_code].upcase if response[:avs_result_code]
avs_result = AVSResult.new(code: STANDARD_AVS_CODE_MAPPING[avs_result_code])
cvv_result = CVVResult.new(response[:card_code])
if using_live_gateway_in_test_mode?(response)
Response.new(false, 'Using a live Authorize.net account in Test Mode is not permitted.')
else
Response.new(
success_from(action, response),
message_from(action, response, avs_result, cvv_result),
response,
authorization: authorization_from(action, response),
test: test?,
avs_result: avs_result,
cvv_result: cvv_result,
fraud_review: fraud_review?(response),
error_code: map_error_code(response[:response_code], response[:response_reason_code])
)
end
end
def cim_action?(action)
action.to_s.start_with?('cim')
end
def post_data(action)
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.send(root_for(action), 'xmlns' => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd') do
add_authentication(xml)
yield(xml)
end
end.to_xml(indent: 0)
end
def root_for(action)
if action == :cim_store
'createCustomerProfileRequest'
elsif action == :cim_store_update
'createCustomerPaymentProfileRequest'
elsif action == :cim_store_delete_customer
'deleteCustomerProfileRequest'
elsif action == :verify_credentials
'authenticateTestRequest'
elsif cim_action?(action)
'createCustomerProfileTransactionRequest'
else
'createTransactionRequest'
end
end
def add_authentication(xml)
xml.merchantAuthentication do
xml.name(@options[:login])
xml.transactionKey(@options[:password])
end
end
def parse_normal(action, body)
doc = Nokogiri::XML(body)
doc.remove_namespaces!
response = { action: action }
response[:response_code] = if (element = doc.at_xpath('//transactionResponse/responseCode'))
empty?(element.content) ? nil : element.content.to_i
end
if (element = doc.at_xpath('//errors/error'))
response[:response_reason_code] = element.at_xpath('errorCode').content[/0*(\d+)$/, 1]
response[:response_reason_text] = element.at_xpath('errorText').content.chomp('.')
elsif (element = doc.at_xpath('//transactionResponse/messages/message'))
response[:response_reason_code] = element.at_xpath('code').content[/0*(\d+)$/, 1]
response[:response_reason_text] = element.at_xpath('description').content.chomp('.')
elsif (element = doc.at_xpath('//messages/message'))
response[:response_reason_code] = element.at_xpath('code').content[/0*(\d+)$/, 1]
response[:response_reason_text] = element.at_xpath('text').content.chomp('.')
else
response[:response_reason_code] = nil
response[:response_reason_text] = ''
end
response[:avs_result_code] =
if (element = doc.at_xpath('//avsResultCode'))
empty?(element.content) ? nil : element.content
end
response[:transaction_id] =
if element = doc.at_xpath('//transId')
empty?(element.content) ? nil : element.content
end
response[:card_code] =
if element = doc.at_xpath('//cvvResultCode')
empty?(element.content) ? nil : element.content
end
response[:authorization_code] =
if element = doc.at_xpath('//authCode')
empty?(element.content) ? nil : element.content
end
response[:cardholder_authentication_code] =
if element = doc.at_xpath('//cavvResultCode')
empty?(element.content) ? nil : element.content
end
response[:account_number] =
if element = doc.at_xpath('//accountNumber')
empty?(element.content) ? nil : element.content[-4..-1]
end
response[:test_request] =
if element = doc.at_xpath('//testRequest')
empty?(element.content) ? nil : element.content
end
response[:full_response_code] =
if element = doc.at_xpath('//messages/message/code')
empty?(element.content) ? nil : element.content
end
response[:network_trans_id] =
if element = doc.at_xpath('//networkTransId')
empty?(element.content) ? nil : element.content
end
response
end
def parse_cim(body, options)
response = {}
doc = Nokogiri::XML(body).remove_namespaces!
if element = doc.at_xpath('//messages/message')
response[:message_code] = element.at_xpath('code').content[/0*(\d+)$/, 1]
response[:message_text] = element.at_xpath('text').content.chomp('.')
end
response[:result_code] =
if element = doc.at_xpath('//messages/resultCode')
empty?(element.content) ? nil : element.content
end
response[:test_request] =
if element = doc.at_xpath('//testRequest')
empty?(element.content) ? nil : element.content
end
response[:customer_profile_id] =
if element = doc.at_xpath('//customerProfileId')
empty?(element.content) ? nil : element.content
end
response[:customer_payment_profile_id] =
if element = doc.at_xpath('//customerPaymentProfileIdList/numericString')
empty?(element.content) ? nil : element.content
end
response[:customer_payment_profile_id] =
if element = doc.at_xpath('//customerPaymentProfileIdList/numericString') ||
doc.at_xpath('//customerPaymentProfileId')
empty?(element.content) ? nil : element.content
end
response[:direct_response] =
if element = doc.at_xpath('//directResponse')
empty?(element.content) ? nil : element.content
end
response.merge!(parse_direct_response_elements(response, options))
response
end
def success_from(action, response)
if cim?(action) || (action == :verify_credentials)
response[:result_code] == 'Ok'
else
[APPROVED, FRAUD_REVIEW].include?(response[:response_code]) && TRANSACTION_ALREADY_ACTIONED.exclude?(response[:response_reason_code])
end
end
def message_from(action, response, avs_result, cvv_result)
if response[:response_code] == DECLINED
if CARD_CODE_ERRORS.include?(cvv_result.code)
return cvv_result.message
elsif AVS_REASON_CODES.include?(response[:response_reason_code]) && AVS_ERRORS.include?(avs_result.code)
return avs_result.message
end
end
response[:response_reason_text] || response[:message_text]
end
def authorization_from(action, response)
if cim?(action)
[response[:customer_profile_id], response[:customer_payment_profile_id], action].join('#')
else
[response[:transaction_id], response[:account_number], action].join('#')
end
end
def split_authorization(authorization)
authorization.split('#')
end
def cim?(action)
(action == :cim_store) || (action == :cim_store_update) || (action == :cim_store_delete_customer)
end
def transaction_id_from(authorization)
transaction_id, = split_authorization(authorization)
transaction_id
end
def fraud_review?(response)
(response[:response_code] == FRAUD_REVIEW)
end
def using_live_gateway_in_test_mode?(response)
!test? && response[:test_request] == '1'
end
def map_error_code(response_code, response_reason_code)
STANDARD_ERROR_CODE_MAPPING["#{response_code}#{response_reason_code}"]
end
def auth_was_for_cim?(authorization)
_, _, action = split_authorization(authorization)
action && cim_action?(action)
end
def parse_direct_response_elements(response, options)
params = response[:direct_response]&.tr('"', '')
return {} unless params
parts = params.split(options[:delimiter] || ',')
{
response_code: parts[0].to_i,
response_subcode: parts[1],
response_reason_code: parts[2],
response_reason_text: parts[3],
approval_code: parts[4],
avs_result_code: parts[5],
transaction_id: parts[6],
invoice_number: parts[7],
order_description: parts[8],
amount: parts[9],
method: parts[10],
transaction_type: parts[11],
customer_id: parts[12],
first_name: parts[13],
last_name: parts[14],
company: parts[15],
address: parts[16],
city: parts[17],
state: parts[18],
zip_code: parts[19],
country: parts[20],
phone: parts[21],
fax: parts[22],
email_address: parts[23],
ship_to_first_name: parts[24],
ship_to_last_name: parts[25],
ship_to_company: parts[26],
ship_to_address: parts[27],
ship_to_city: parts[28],
ship_to_state: parts[29],
ship_to_zip_code: parts[30],
ship_to_country: parts[31],
tax: parts[32],
duty: parts[33],
freight: parts[34],
tax_exempt: parts[35],
purchase_order_number: parts[36],
md5_hash: parts[37],
card_code: parts[38],
cardholder_authentication_verification_response: parts[39],
account_number: parts[50] || '',
card_type: parts[51] || '',
split_tender_id: parts[52] || '',
requested_amount: parts[53] || '',
balance_on_card: parts[54] || ''
}
end
end
end
end