lib/active_merchant/billing/gateways/authorize_net.rb in activemerchant-1.50.0 vs lib/active_merchant/billing/gateways/authorize_net.rb in activemerchant-1.51.0
- old
+ new
@@ -54,101 +54,71 @@
2 => /\A;(?<pan>[\d]{1,19}+)=(?<expiration>[\d]{0,4}|=)(?<service_code>[\d]{0,3}|=)(?<discretionary_data>.*)\?\Z/
}.freeze
APPLE_PAY_DATA_DESCRIPTOR = "COMMON.APPLE.INAPP.PAYMENT"
+ PAYMENT_METHOD_NOT_SUPPORTED_ERROR = "155"
+
def initialize(options={})
requires!(options, :login, :password)
super
end
def purchase(amount, payment, options = {})
- commit("AUTH_CAPTURE") do |xml|
- add_order_id(xml, options)
- xml.transactionRequest do
- xml.transactionType('authCaptureTransaction')
- xml.amount(amount(amount))
-
- add_payment_source(xml, payment)
- add_invoice(xml, options)
- add_customer_data(xml, payment, options)
- add_market_type(xml, payment)
- add_settings(xml, payment, options)
- add_user_fields(xml, amount, options)
+ if payment.is_a?(String)
+ commit(:cim_purchase) 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={})
- commit("AUTH_ONLY") do |xml|
- add_order_id(xml, options)
- xml.transactionRequest do
- xml.transactionType('authOnlyTransaction')
- xml.amount(amount(amount))
-
- add_payment_source(xml, payment)
- add_invoice(xml, options)
- add_customer_data(xml, payment, options)
- add_market_type(xml, payment)
- add_settings(xml, payment, options)
- add_user_fields(xml, amount, options)
+ if payment.is_a?(String)
+ commit(:cim_authorize) 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={})
- commit("PRIOR_AUTH_CAPTURE") do |xml|
- add_order_id(xml, options)
- xml.transactionRequest do
- xml.transactionType('priorAuthCaptureTransaction')
- xml.amount(amount(amount))
- xml.refTransId(split_authorization(authorization)[0])
-
- add_invoice(xml, options)
- add_user_fields(xml, amount, options)
- end
+ if auth_was_for_cim?(authorization)
+ cim_capture(amount, authorization, options)
+ else
+ normal_capture(amount, authorization, options)
end
end
def refund(amount, authorization, options={})
- transaction_id, card_number = split_authorization(authorization)
- commit("CREDIT") do |xml|
- xml.transactionRequest do
- xml.transactionType('refundTransaction')
- xml.amount(amount.nil? ? 0 : amount(amount))
- xml.payment do
- xml.creditCard do
- xml.cardNumber(card_number || options[:card_number])
- xml.expirationDate('XXXX')
- end
- end
- xml.refTransId(transaction_id)
-
- add_customer_data(xml, nil, options)
- add_user_fields(xml, amount, options)
- end
+ if auth_was_for_cim?(authorization)
+ cim_refund(amount, authorization, options)
+ else
+ normal_refund(amount, authorization, options)
end
end
def void(authorization, options={})
- commit("VOID") do |xml|
- add_order_id(xml, options)
- xml.transactionRequest do
- xml.transactionType('voidTransaction')
- xml.refTransId(split_authorization(authorization)[0])
-
- add_user_fields(xml, nil, options)
- end
+ if auth_was_for_cim?(authorization)
+ cim_void(authorization, options)
+ else
+ normal_void(authorization, options)
end
end
def credit(amount, payment, options={})
if payment.is_a?(String)
raise ArgumentError, "Reference credits are not supported. Please supply the original credit card or use the #refund method."
end
- commit("CREDIT") do |xml|
+ commit(:credit) do |xml|
add_order_id(xml, options)
xml.transactionRequest do
xml.transactionType('refundTransaction')
xml.amount(amount(amount))
@@ -166,36 +136,194 @@
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
+ def store(credit_card, options = {})
+ commit(:cim_store) 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")
+ 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
+ end
+ end
+ end
+ end
+
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
+ gsub(%r((<transactionKey>).+(</transactionKey>)), '\1[FILTERED]\2').
gsub(%r((<cardNumber>).+(</cardNumber>)), '\1[FILTERED]\2').
- gsub(%r((<cardCode>).+(</cardCode>)), '\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(%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_source(xml, payment)
+ add_invoice(xml, options)
+ add_customer_data(xml, payment, options)
+ add_market_type(xml, payment)
+ add_settings(xml, payment, options)
+ add_user_fields(xml, amount, 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_payment_source(xml, payment)
+ add_invoice(xml, options)
+ end
+ end
+ end
+
+ def cim_capture(amount, authorization, options)
+ commit(:cim_capture) do |xml|
+ add_order_id(xml, options)
+ xml.transaction do
+ xml.profileTransPriorAuthCapture do
+ xml.amount(amount(amount))
+ xml.transId(transaction_id_from(authorization))
+ end
+ end
+ 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))
+ xml.refTransId(transaction_id_from(authorization))
+ add_invoice(xml, 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) do |xml|
+ add_order_id(xml, options)
+ xml.transaction do
+ xml.profileTransRefund do
+ xml.amount(amount(amount))
+ xml.creditCardNumberMasked(card_number)
+ add_invoice(xml, options)
+ xml.transId(transaction_id)
+ end
+ end
+ 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
+ xml.creditCard do
+ xml.cardNumber(card_number || options[:card_number])
+ xml.expirationDate('XXXX')
+ end
+ end
+ xml.refTransId(transaction_id)
+
+ add_invoice(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) do |xml|
+ add_order_id(xml, options)
+ xml.transaction do
+ xml.profileTransVoid do
+ xml.transId(transaction_id_from(authorization))
+ end
+ end
+ 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))
+ add_user_fields(xml, nil, options)
+ end
+ end
+ end
+
def add_payment_source(xml, source)
return unless source
- if card_brand(source) == 'check'
+ if source.is_a?(String)
+ add_token_payment_method(xml, source)
+ elsif card_brand(source) == 'check'
add_check(xml, source)
elsif card_brand(source) == 'apple_pay'
add_apple_pay_payment_token(xml, source)
else
add_credit_card(xml, source)
end
end
def add_settings(xml, source, options)
xml.transactionSettings do
- if card_brand(source) == "check" && options[:recurring]
+ if !source.is_a?(String) && card_brand(source) == "check" && options[:recurring]
xml.setting do
xml.settingName("recurringBilling")
xml.settingValue("true")
end
end
@@ -262,22 +390,27 @@
end
end
end
end
- # http://developer.authorize.net/api/reference/#apple-pay-transactions
+ def add_token_payment_method(xml, token)
+ customer_profile_id, customer_payment_profile_id, _ = split_authorization(token)
+ xml.customerProfileId(customer_profile_id)
+ xml.customerPaymentProfileId(customer_payment_profile_id)
+ end
+
def add_apple_pay_payment_token(xml, apple_pay_payment_token)
xml.payment do
xml.opaqueData do
xml.dataDescriptor(APPLE_PAY_DATA_DESCRIPTOR)
xml.dataValue(Base64.strict_encode64(apple_pay_payment_token.payment_data.to_json))
end
end
end
def add_market_type(xml, payment)
- return if card_brand(payment) == 'check' or card_brand(payment) == 'apple_pay'
+ return if payment.is_a?(String) || card_brand(payment) == 'check' || card_brand(payment) == 'apple_pay'
if valid_track_data
xml.retail do
xml.marketType(MARKET_TYPE[:retail])
end
elsif payment.manual_entry
@@ -303,58 +436,67 @@
end
end
end
def add_customer_data(xml, payment_source, options)
- billing_address = options[:billing_address] || options[:address] || {}
- shipping_address = options[:shipping_address] || options[:address] || {}
-
xml.customer do
xml.id(options[:customer]) unless empty?(options[:customer]) || options[:customer] !~ /^\d+$/
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])
+
+ xml.cardholderAuthentication do
+ xml.authenticationIndicator(options[:authentication_indicator])
+ xml.cardholderAuthenticationValue(options[:cardholder_authentication_value])
+ 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, billing_address, options)
+ first_name, last_name = names_from(payment_source, address, options)
xml.firstName(truncate(first_name, 50)) unless empty?(first_name)
xml.lastName(truncate(last_name, 50)) unless empty?(last_name)
- xml.company(truncate(billing_address[:company], 50)) unless empty?(billing_address[:company])
- xml.address(truncate(billing_address[:address1], 60))
- xml.city(truncate(billing_address[:city], 40))
- xml.state(empty?(billing_address[:state]) ? 'n/a' : truncate(billing_address[:state], 40))
- xml.zip(truncate((billing_address[:zip] || options[:zip]), 20))
- xml.country(truncate(billing_address[:country], 60))
- xml.phoneNumber(truncate(billing_address[:phone], 25)) unless empty?(billing_address[:phone])
- xml.faxNumber(truncate(billing_address[:fax], 25)) unless empty?(billing_address[:fax])
+ xml.company(truncate(address[:company], 50)) unless empty?(address[:company])
+ xml.address(truncate(address[:address1], 60))
+ xml.city(truncate(address[:city], 40))
+ xml.state(empty?(address[:state]) ? 'n/a' : truncate(address[:state], 40))
+ xml.zip(truncate((address[:zip] || options[:zip]), 20))
+ xml.country(truncate(address[:country], 60))
+ xml.phoneNumber(truncate(address[:phone], 25)) unless empty?(address[:phone])
+ xml.faxNumber(truncate(address[:fax], 25)) unless empty?(address[:fax])
end
+ end
- unless shipping_address.blank?
- xml.shipTo do
- (first_name, last_name) = if shipping_address[:name]
- shipping_address[:name].split
- else
- [shipping_address[:first_name], shipping_address[:last_name]]
- end
- xml.firstName(truncate(first_name, 50)) unless empty?(first_name)
- xml.lastName(truncate(last_name, 50)) unless empty?(last_name)
+ def add_shipping_address(xml, options, root_node="shipTo")
+ address = options[:shipping_address] || options[:address]
+ return unless address
- xml.company(truncate(shipping_address[:company], 50)) unless empty?(shipping_address[:company])
- xml.address(truncate(shipping_address[:address1], 60))
- xml.city(truncate(shipping_address[:city], 40))
- xml.state(truncate(shipping_address[:state], 40))
- xml.zip(truncate(shipping_address[:zip], 20))
- xml.country(truncate(shipping_address[:country], 60))
+ xml.send(root_node) do
+ first_name, last_name = if address[:name]
+ split_names(address[:name])
+ else
+ [address[:first_name], address[:last_name]]
end
- end
- xml.customerIP(options[:ip]) unless empty?(options[:ip])
+ xml.firstName(truncate(first_name, 50)) unless empty?(first_name)
+ xml.lastName(truncate(last_name, 50)) unless empty?(last_name)
- xml.cardholderAuthentication do
- xml.authenticationIndicator(options[:authentication_indicator])
- xml.cardholderAuthenticationValue(options[:cardholder_authentication_value])
+ xml.company(truncate(address[:company], 50)) unless empty?(address[:company])
+ xml.address(truncate(address[:address1], 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_order_id(xml, options)
xml.refId(truncate(options[:order_id], 20))
end
@@ -365,54 +507,95 @@
xml.description(truncate(options[:description], 255))
end
end
def names_from(payment_source, address, options)
- if payment_source && !payment_source.is_a?(PaymentToken)
- first_name, last_name = (address[:name] || "").split
+ 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 split_names(full_name)
+ names = (full_name || "").split
+ last_name = names.pop
+ first_name = names.join(" ")
+ [first_name, last_name]
+ end
+
+ def headers
+ { 'Content-Type' => 'text/xml' }
+ end
+
+ def url
+ test? ? test_url : live_url
+ end
+
+ def parse(action, raw_response)
+ if is_cim_action?(action)
+ parse_cim(raw_response)
+ else
+ parse_normal(action, raw_response)
+ end
+ end
+
def commit(action, &payload)
- url = (test? ? test_url : live_url)
- response = parse(action, ssl_post(url, post_data(&payload), 'Content-Type' => 'text/xml'))
+ raw_response = ssl_post(url, post_data(action, &payload), headers)
+ response = parse(action, raw_response)
avs_result = AVSResult.new(code: response[: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(response),
- message_from(response, avs_result, cvv_result),
+ success_from(action, response),
+ message_from(action, response, avs_result, cvv_result),
response,
- authorization: authorization_from(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 post_data
- Nokogiri::XML::Builder.new do |xml|
- xml.createTransactionRequest('xmlns' => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd') do
- xml.merchantAuthentication do
- xml.name(@options[:login])
- xml.transactionKey(@options[:password])
- end
+ def is_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 parse(action, body)
+ def root_for(action)
+ if action == :cim_store
+ "createCustomerProfileRequest"
+ elsif is_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}
@@ -463,48 +646,151 @@
end
response
end
- def success_from(response)
- (
- response[:response_code] == APPROVED &&
- TRANSACTION_ALREADY_ACTIONED.exclude?(response[:response_reason_code])
- )
+ def parse_cim(body)
+ 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[:direct_response] = if(element = doc.at_xpath("//directResponse"))
+ (empty?(element.content) ? nil : element.content)
+ end
+
+ response.merge!(parse_direct_response_elements(response))
+
+ response
end
- def message_from(response, avs_result, cvv_result)
+ def success_from(action, response)
+ if action == :cim_store
+ response[:result_code] == "Ok"
+ else
+ response[:response_code] == APPROVED && 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[:response_reason_text] || response[:message_text]
end
- def authorization_from(response)
- [response[:transaction_id], response[:account_number]].join("#")
+ def authorization_from(action, response)
+ if action == :cim_store
+ [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)
- transaction_id, card_number = authorization.split("#")
- [transaction_id, card_number]
+ authorization.split("#")
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.to_s << response_reason_code.to_s]
+ STANDARD_ERROR_CODE_MAPPING["#{response_code}#{response_reason_code}"]
end
+
+ def auth_was_for_cim?(authorization)
+ _, _, action = split_authorization(authorization)
+ action && is_cim_action?(action)
+ end
+
+ def parse_direct_response_elements(response)
+ params = response[:direct_response]
+ return {} unless params
+
+ parts = params.split(',')
+ {
+ 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