From de3e77891e760b4855c9c9e6d4e442f3e28276dc Mon Sep 17 00:00:00 2001 From: leila-alderman Date: Wed, 8 Jul 2020 11:37:20 -0400 Subject: [PATCH] Elavon: Upgrade gateway integration [WIP] This is the start of the work necessary to upgrade the Elavon gateway integration to move from the deprecated `process.do` endpoint to the more current XML endpoint `processxml.do`. This WIP commit includes the major updates to the gateway structure to use XML and to achieve a successful remote purchase request. Many of the tests still need to be updated, including updating all of the stubbed responses in the unit tests. CE-704 `ruby -Itest test/remote/gateways/remote_elavon_test.rb -n test_successful_purchase` 1 tests, 5 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed --- .../billing/gateways/elavon.rb | 453 +++++++++--------- 1 file changed, 224 insertions(+), 229 deletions(-) diff --git a/lib/active_merchant/billing/gateways/elavon.rb b/lib/active_merchant/billing/gateways/elavon.rb index 23f72638647..8a36f517f44 100644 --- a/lib/active_merchant/billing/gateways/elavon.rb +++ b/lib/active_merchant/billing/gateways/elavon.rb @@ -1,4 +1,5 @@ require 'active_merchant/billing/gateways/viaklix' +require 'nokogiri' module ActiveMerchant #:nodoc: module Billing #:nodoc: @@ -7,8 +8,8 @@ class ElavonGateway < Gateway class_attribute :test_url, :live_url, :delimiter, :actions - self.test_url = 'https://api.demo.convergepay.com/VirtualMerchantDemo/process.do' - self.live_url = 'https://api.convergepay.com/VirtualMerchant/process.do' + self.test_url = 'https://api.demo.convergepay.com/VirtualMerchantDemo/processxml.do' + self.live_url = 'https://api.convergepay.com/VirtualMerchant/processxml.do' self.display_name = 'Elavon MyVirtualMerchant' self.supported_countries = %w(US CA PR DE IE NO PL LU BE NL MX) @@ -35,115 +36,139 @@ def initialize(options = {}) end def purchase(money, payment_method, options = {}) - form = {} - add_salestax(form, options) - add_invoice(form, options) - if payment_method.is_a?(String) - add_token(form, payment_method) - else - add_creditcard(form, payment_method) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:purchase] + xml.ssl_amount money + + if payment_method.is_a?(String) + add_token(xml, payment_method) + else + add_creditcard(xml, payment_method) + end + + add_invoice(xml, options) + add_salestax(xml, options) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_auth_purchase_params(xml, options) + add_level_3_fields(xml, options) if options[:level_3_data] end - add_currency(form, money, options) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_ip(form, options) - add_auth_purchase_params(form, options) - add_level_3_fields(form, options) if options[:level_3_data] - commit(:purchase, money, form, options) + commit(request) end def authorize(money, creditcard, options = {}) - form = {} - add_salestax(form, options) - add_invoice(form, options) - add_creditcard(form, creditcard) - add_currency(form, money, options) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_ip(form, options) - add_auth_purchase_params(form, options) - add_level_3_fields(form, options) if options[:level_3_data] - commit(:authorize, money, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:authorize] + xml.ssl_amount money + + add_salestax(xml, options) + add_invoice(xml, options) + add_creditcard(xml, creditcard) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_ip(xml, options) + add_auth_purchase_params(xml, options) + add_level_3_fields(xml, options) if options[:level_3_data] + end + commit(request) end def capture(money, authorization, options = {}) - form = {} - if options[:credit_card] - action = :capture - add_salestax(form, options) - add_approval_code(form, authorization) - add_invoice(form, options) - add_creditcard(form, options[:credit_card]) - add_currency(form, money, options) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - else - action = :capture_complete - add_txn_id(form, authorization) - add_partial_shipment_flag(form, options) - add_test_mode(form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:capture] + + if options[:credit_card] + xml.ssl_transaction_type self.actions[:capture] + add_salestax(xml, options) + add_approval_code(xml, authorization) + add_invoice(xml, options) + add_creditcard(xml, options[:credit_card]) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + else + xml.ssl_transaction_type self.actions[:capture_complete] + add_txn_id(xml, authorization) + add_partial_shipment_flag(xml, options) + add_test_mode(xml, options) + end end - commit(action, money, form, options) + commit(request) end def refund(money, identification, options = {}) - form = {} - add_txn_id(form, identification) - add_test_mode(form, options) - commit(:refund, money, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:refund] + add_txn_id(xml, identification) + add_test_mode(xml, options) + end + commit(request) end def void(identification, options = {}) - form = {} - add_txn_id(form, identification) - add_test_mode(form, options) - commit(:void, nil, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:void] + + add_txn_id(xml, identification) + add_test_mode(xml, options) + end + commit(request) end def credit(money, creditcard, options = {}) raise ArgumentError, 'Reference credits are not supported. Please supply the original credit card or use the #refund method.' if creditcard.is_a?(String) - form = {} - add_invoice(form, options) - add_creditcard(form, creditcard) - add_currency(form, money, options) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - commit(:credit, money, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:credit] + add_invoice(xml, options) + add_creditcard(xml, creditcard) + add_currency(xml, money, options) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + end + commit(request) end def verify(credit_card, options = {}) - form = {} - add_creditcard(form, credit_card) - add_address(form, options) - add_test_mode(form, options) - add_ip(form, options) - commit(:verify, 0, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:verify] + add_creditcard(xml, credit_card) + add_address(xml, options) + add_test_mode(xml, options) + add_ip(xml, options) + end + commit(request) end def store(creditcard, options = {}) - form = {} - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - add_verification(form, options) - form[:add_token] = 'Y' - commit(:store, nil, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:store] + xml.add_token 'Y' + add_creditcard(xml, creditcard) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + add_verification(xml, options) + end + commit(request) end def update(token, creditcard, options = {}) - form = {} - add_token(form, token) - add_creditcard(form, creditcard) - add_address(form, options) - add_customer_data(form, options) - add_test_mode(form, options) - commit(:update, nil, form, options) + request = build_xml_request do |xml| + xml.ssl_transaction_type self.actions[:update] + add_token(xml, token) + add_creditcard(xml, creditcard) + add_address(xml, options) + add_customer_email(xml, options) + add_test_mode(xml, options) + end + commit(request) end def supports_scrubbing? @@ -159,210 +184,180 @@ def scrub(transcript) private - def add_invoice(form, options) - form[:invoice_number] = truncate((options[:order_id] || options[:invoice]), 10) - form[:description] = truncate(options[:description], 255) - end - - def add_approval_code(form, authorization) - form[:approval_code] = authorization.split(';').first + def add_invoice(xml, options) + xml.ssl_invoice_number truncate((options[:order_id] || options[:invoice]), 25) + xml.ssl_description truncate(options[:description], 255) end - def add_txn_id(form, authorization) - form[:txn_id] = authorization.split(';').last + def add_approval_code(xml, authorization) + xml.ssl_approval_code authorization.split(';').first end - def authorization_from(response) - [response['approval_code'], response['txn_id']].join(';') + def add_txn_id(xml, authorization) + xml.ssl_txn_id authorization.split(';').last end - def add_creditcard(form, creditcard) - form[:card_number] = creditcard.number - form[:exp_date] = expdate(creditcard) + def add_creditcard(xml, creditcard) + xml.ssl_card_number creditcard.number + xml.ssl_exp_date expdate(creditcard) - add_verification_value(form, creditcard) if creditcard.verification_value? + add_verification_value(xml, creditcard) if creditcard.verification_value? - form[:first_name] = truncate(creditcard.first_name, 20) - form[:last_name] = truncate(creditcard.last_name, 30) + xml.ssl_first_name truncate(creditcard.first_name, 20) + xml.ssl_last_name truncate(creditcard.last_name, 30) end - def add_currency(form, money, options) + def add_currency(xml, money, options) currency = options[:currency] || currency(money) - form[:transaction_currency] = currency if currency && (@options[:multi_currency] || options[:multi_currency]) + return unless currency && (@options[:multi_currency] || options[:multi_currency]) + + xml.ssl_transaction_currency currency end - def add_token(form, token) - form[:token] = token + def add_token(xml, token) + xml.ssl_token token end - def add_verification_value(form, creditcard) - form[:cvv2cvc2] = creditcard.verification_value - form[:cvv2cvc2_indicator] = '1' + def add_verification_value(xml, creditcard) + xml.ssl_cvv2cvc2 creditcard.verification_value + xml.ssl_cvv2cvc2_indicator 1 end - def add_customer_data(form, options) - form[:email] = truncate(options[:email], 100) unless empty?(options[:email]) - form[:customer_code] = truncate(options[:customer], 10) unless empty?(options[:customer]) - form[:customer_number] = options[:customer_number] unless empty?(options[:customer_number]) - options[:custom_fields]&.each do |key, value| - form[key.to_s] = value - end + def add_customer_email(xml, options) + xml.ssl_email truncate(options[:email], 100) unless empty?(options[:email]) end - def add_salestax(form, options) - form[:salestax] = options[:tax] if options[:tax].present? + def add_salestax(xml, options) + return unless options[:tax].present? + + xml.ssl_salestax options[:tax] end - def add_address(form, options) + def add_address(xml, options) billing_address = options[:billing_address] || options[:address] if billing_address - form[:avs_address] = truncate(billing_address[:address1], 30) - form[:address2] = truncate(billing_address[:address2], 30) - form[:avs_zip] = truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9) - form[:city] = truncate(billing_address[:city], 30) - form[:state] = truncate(billing_address[:state], 10) - form[:company] = truncate(billing_address[:company], 50) - form[:phone] = truncate(billing_address[:phone], 20) - form[:country] = truncate(billing_address[:country], 50) + xml.ssl_avs_address truncate(billing_address[:address1], 30) + xml.ssl_address2 truncate(billing_address[:address2], 30) + xml.ssl_avs_zip truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9) + xml.ssl_city truncate(billing_address[:city], 30) + xml.ssl_state truncate(billing_address[:state], 10) + xml.ssl_company truncate(billing_address[:company], 50) + xml.ssl_phone truncate(billing_address[:phone], 20) + xml.ssl_country truncate(billing_address[:country], 50) end if shipping_address = options[:shipping_address] - first_name, last_name = split_names(shipping_address[:name]) - form[:ship_to_first_name] = truncate(first_name, 20) - form[:ship_to_last_name] = truncate(last_name, 30) - form[:ship_to_address1] = truncate(shipping_address[:address1], 30) - form[:ship_to_address2] = truncate(shipping_address[:address2], 30) - form[:ship_to_city] = truncate(shipping_address[:city], 30) - form[:ship_to_state] = truncate(shipping_address[:state], 10) - form[:ship_to_company] = truncate(shipping_address[:company], 50) - form[:ship_to_country] = truncate(shipping_address[:country], 50) - form[:ship_to_zip] = truncate(shipping_address[:zip], 10) + xml.ssl_ship_to_country truncate(shipping_address[:country], 50) + xml.ssl_ship_to_zip truncate(shipping_address[:zip], 10) end end - def add_verification(form, options) - form[:verify] = 'Y' if options[:verify] + def add_verification(xml, options) + xml.ssl_verify 'Y' if options[:verify] end - def add_test_mode(form, options) - form[:test_mode] = 'TRUE' if options[:test_mode] + def add_test_mode(xml, options) + xml.ssl_test_mode 'TRUE' if options[:test_mode] end - def add_partial_shipment_flag(form, options) - form[:partial_shipment_flag] = 'Y' if options[:partial_shipment_flag] + def add_partial_shipment_flag(xml, options) + xml.ssl_partial_shipment_flag 'Y' if options[:partial_shipment_flag] end - def add_ip(form, options) - form[:cardholder_ip] = options[:ip] if options.has_key?(:ip) + def add_ip(xml, options) + xml.ssl_cardholder_ip options[:ip] if options.has_key?(:ip) end - def add_auth_purchase_params(form, options) - form[:dynamic_dba] = options[:dba] if options.has_key?(:dba) - form[:merchant_initiated_unscheduled] = options[:merchant_initiated_unscheduled] if options.has_key?(:merchant_initiated_unscheduled) + def add_auth_purchase_params(xml, options) + xml.ssl_dynamic_dba options[:dba] if options.has_key?(:dba) + xml.ssl_merchant_initiated_unscheduled options[:merchant_initiated_unscheduled] if options.has_key?(:merchant_initiated_unscheduled) end - def add_level_3_fields(form, options) + def add_level_3_fields(xml, options) level_3_data = options[:level_3_data] - form[:customer_code] = level_3_data[:customer_code] if level_3_data[:customer_code] - form[:salestax] = level_3_data[:salestax] if level_3_data[:salestax] - form[:salestax_indicator] = level_3_data[:salestax_indicator] if level_3_data[:salestax_indicator] - form[:level3_indicator] = level_3_data[:level3_indicator] if level_3_data[:level3_indicator] - form[:ship_to_zip] = level_3_data[:ship_to_zip] if level_3_data[:ship_to_zip] - form[:ship_to_country] = level_3_data[:ship_to_country] if level_3_data[:ship_to_country] - form[:shipping_amount] = level_3_data[:shipping_amount] if level_3_data[:shipping_amount] - form[:ship_from_postal_code] = level_3_data[:ship_from_postal_code] if level_3_data[:ship_from_postal_code] - form[:discount_amount] = level_3_data[:discount_amount] if level_3_data[:discount_amount] - form[:duty_amount] = level_3_data[:duty_amount] if level_3_data[:duty_amount] - form[:national_tax_indicator] = level_3_data[:national_tax_indicator] if level_3_data[:national_tax_indicator] - form[:national_tax_amount] = level_3_data[:national_tax_amount] if level_3_data[:national_tax_amount] - form[:order_date] = level_3_data[:order_date] if level_3_data[:order_date] - form[:other_tax] = level_3_data[:other_tax] if level_3_data[:other_tax] - form[:summary_commodity_code] = level_3_data[:summary_commodity_code] if level_3_data[:summary_commodity_code] - form[:merchant_vat_number] = level_3_data[:merchant_vat_number] if level_3_data[:merchant_vat_number] - form[:customer_vat_number] = level_3_data[:customer_vat_number] if level_3_data[:customer_vat_number] - form[:freight_tax_amount] = level_3_data[:freight_tax_amount] if level_3_data[:freight_tax_amount] - form[:vat_invoice_number] = level_3_data[:vat_invoice_number] if level_3_data[:vat_invoice_number] - form[:tracking_number] = level_3_data[:tracking_number] if level_3_data[:tracking_number] - form[:shipping_company] = level_3_data[:shipping_company] if level_3_data[:shipping_company] - form[:other_fees] = level_3_data[:other_fees] if level_3_data[:other_fees] - add_line_items(form, level_3_data) if level_3_data[:line_items] - end - - def add_line_items(form, level_3_data) - items = [] - level_3_data[:line_items].each do |line_item| - item = {} - line_item.each do |key, value| - prefixed_key = "ssl_line_Item_#{key}" - item[prefixed_key.to_sym] = value + xml.ssl_customer_code level_3_data[:customer_code] if level_3_data[:customer_code] + xml.ssl_salestax level_3_data[:salestax] if level_3_data[:salestax] + xml.ssl_salestax_indicator level_3_data[:salestax_indicator] if level_3_data[:salestax_indicator] + xml.ssl_level3_indicator level_3_data[:level3_indicator] if level_3_data[:level3_indicator] + xml.ssl_ship_to_zip level_3_data[:ship_to_zip] if level_3_data[:ship_to_zip] + xml.ssl_ship_to_country level_3_data[:ship_to_country] if level_3_data[:ship_to_country] + xml.ssl_shipping_amount level_3_data[:shipping_amount] if level_3_data[:shipping_amount] + xml.ssl_ship_from_postal_code level_3_data[:ship_from_postal_code] if level_3_data[:ship_from_postal_code] + xml.ssl_discount_amount level_3_data[:discount_amount] if level_3_data[:discount_amount] + xml.ssl_duty_amount level_3_data[:duty_amount] if level_3_data[:duty_amount] + xml.ssl_national_tax_indicator level_3_data[:national_tax_indicator] if level_3_data[:national_tax_indicator] + xml.ssl_national_tax_amount level_3_data[:national_tax_amount] if level_3_data[:national_tax_amount] + xml.ssl_order_data level_3_data[:order_date] if level_3_data[:order_date] + xml.ssl_other_tax level_3_data[:other_tax] if level_3_data[:other_tax] + xml.ssl_summary_commodity_code level_3_data[:summary_commodity_code] if level_3_data[:summary_commodity_code] + xml.ssl_merchant_vat_number level_3_data[:merchant_vat_number] if level_3_data[:merchant_vat_number] + xml.ssl_customer_vat_number level_3_data[:customer_vat_number] if level_3_data[:customer_vat_number] + xml.ssl_freight_tax_amount level_3_data[:freight_tax_amount] if level_3_data[:freight_tax_amount] + xml.ssl_vat_invoice_number level_3_data[:vat_invoice_number] if level_3_data[:vat_invoice_number] + xml.ssl_tracking_number level_3_data[:tracking_number] if level_3_data[:tracking_number] + xml.ssl_shipping_company level_3_data[:shipping_company] if level_3_data[:shipping_company] + xml.ssl_other_fees level_3_data[:other_fees] if level_3_data[:other_fees] + add_line_items(xml, level_3_data) if level_3_data[:line_items] + end + + def add_line_items(xml, level_3_data) + xml.LineItemProducts { + level_3_data[:line_items].each do |line_item| + xml.product { + line_item.each do |key, value| + prefixed_key = "ssl_line_Item_#{key}" + xml.send(prefixed_key, value) + end + } end - items << item - end - form[:LineItemProducts] = { product: items } + } end - def message_from(response) - success?(response) ? response['result_message'] : response['errorMessage'] - end + def build_xml_request + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.txn do + xml.ssl_merchant_id @options[:login] + xml.ssl_user_id @options[:user] + xml.ssl_pin @options[:password] + yield(xml) + end + end - def success?(response) - !response.has_key?('errorMessage') + builder.to_xml.gsub("\n", '') end - def commit(action, money, parameters, options) - parameters[:amount] = amount(money) - parameters[:transaction_type] = self.actions[action] - - response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(parameters, options))) + def commit(request) + response = parse(ssl_post(test? ? self.test_url : self.live_url, request, headers)) - Response.new(response['result'] == '0', message_from(response), response, + Response.new( + response[:result] == '0', + response[:result_message], + response, test: @options[:test] || test?, authorization: authorization_from(response), - avs_result: { code: response['avs_response'] }, - cvv_result: response['cvv2_response'] + error_code: response[:result], + avs_result: { code: response[:avs_response] }, + cvv_result: response[:cvv2_response] ) end - def post_data(parameters, options) - result = preamble - result.merge!(parameters) - result.collect { |key, value| post_data_string(key, value, options) }.join('&') - end - - def post_data_string(key, value, options) - if custom_field?(key, options) || key == :LineItemProducts - "#{key}=#{CGI.escape(value.to_s)}" - else - "ssl_#{key}=#{CGI.escape(value.to_s)}" - end - end - - def custom_field?(field_name, options) - return true if options[:custom_fields]&.include?(field_name.to_sym) - - field_name == :customer_number + def headers + { + 'Accept' => 'application/xml', + 'Content-type' => 'application/x-www-form-urlencoded' + } end - def preamble - result = { - 'merchant_id' => @options[:login], - 'pin' => @options[:password], - 'show_form' => 'false', - 'result_format' => 'ASCII' - } + def parse(body) + xml = Nokogiri::XML(body) + response = Hash.from_xml(xml.to_s)['txn'] - result['user_id'] = @options[:user] unless empty?(@options[:user]) - result + response.deep_transform_keys { |key| key.gsub('ssl_', '').to_sym } end - def parse(msg) - resp = {} - msg.split(self.delimiter).collect { |li| - key, value = li.split('=') - resp[key.to_s.strip.gsub(/^ssl_/, '')] = value.to_s.strip - } - resp + def authorization_from(response) + [response[:approval_code], response[:txn_id]].join(';') end end end