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