module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# The Mercury gateway integration by default requires that the Mercury
# account being used has tokenization turned. This enables the use of
# capture/refund/void without having to pass the credit card back in each
# time. Only the "OneTime" tokenization is used; there is no use of
# "Recurring" tokenization.
#
# If you don't wish to enable Mercury tokenization, you can pass
# :tokenization => false
as an option when creating the
# gateway. If you do so, then passing a +:credit_card+ option to +capture+
# and +refund+ will become mandatory.
class MercuryGateway < Gateway
URLS = {
test: 'https://w1.mercurycert.net/ws/ws.asmx',
live: 'https://w1.mercurypay.com/ws/ws.asmx'
}
self.homepage_url = 'http://www.mercurypay.com'
self.display_name = 'Mercury'
self.supported_countries = %w[US CA]
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
self.default_currency = 'USD'
STANDARD_ERROR_CODE_MAPPING = {
'100204' => STANDARD_ERROR_CODE[:invalid_number],
'100205' => STANDARD_ERROR_CODE[:invalid_expiry_date],
'000000' => STANDARD_ERROR_CODE[:card_declined]
}
def initialize(options = {})
requires!(options, :login, :password)
@use_tokenization = (!options.has_key?(:tokenization) || options[:tokenization])
super
end
def purchase(money, credit_card, options = {})
requires!(options, :order_id)
request = build_non_authorized_request('Sale', money, credit_card, options)
commit('Sale', request)
end
def credit(money, credit_card, options = {})
requires!(options, :order_id)
request = build_non_authorized_request('Return', money, credit_card, options)
commit('Return', request)
end
def authorize(money, credit_card, options = {})
requires!(options, :order_id)
request = build_non_authorized_request('PreAuth', money, credit_card, options.merge(authorized: money))
commit('PreAuth', request)
end
def capture(money, authorization, options = {})
requires!(options, :credit_card) unless @use_tokenization
request = build_authorized_request('PreAuthCapture', money, authorization, options[:credit_card], options.merge(authorized: money))
commit('PreAuthCapture', request)
end
def refund(money, authorization, options = {})
requires!(options, :credit_card) unless @use_tokenization
request = build_authorized_request('Return', money, authorization, options[:credit_card], options)
commit('Return', request)
end
def void(authorization, options={})
requires!(options, :credit_card) unless @use_tokenization
request = build_authorized_request('VoidSale', nil, authorization, options[:credit_card], options)
commit('VoidSale', request)
end
def store(credit_card, options={})
request = build_card_lookup_request(credit_card, options)
commit('CardLookup', request)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r(<), '<').
gsub(%r(>), '>').
gsub(%r(().*())i, '\1[FILTERED]\2').
gsub(%r(()(\d|x)*())i, '\1[FILTERED]\3').
gsub(%r(()\d*())i, '\1[FILTERED]\2')
end
private
def build_non_authorized_request(action, money, credit_card, options)
xml = Builder::XmlMarkup.new
xml.tag! 'TStream' do
xml.tag! 'Transaction' do
xml.tag! 'TranType', 'Credit'
xml.tag! 'TranCode', action
xml.tag! 'PartialAuth', 'Allow' if options[:allow_partial_auth] && %w[PreAuth Sale].include?(action)
add_invoice(xml, options[:order_id], nil, options)
add_reference(xml, 'RecordNumberRequested')
add_customer_data(xml, options)
add_amount(xml, money, options)
add_credit_card(xml, credit_card, action)
add_address(xml, options) unless credit_card.track_data.present?
end
end
xml = xml.target!
end
def build_authorized_request(action, money, authorization, credit_card, options)
xml = Builder::XmlMarkup.new
invoice_no, ref_no, auth_code, acq_ref_data, process_data, record_no, amount = split_authorization(authorization)
ref_no = '1' if ref_no.blank?
xml.tag! 'TStream' do
xml.tag! 'Transaction' do
xml.tag! 'TranType', 'Credit'
xml.tag! 'PartialAuth', 'Allow' if options[:allow_partial_auth] && (action == 'PreAuthCapture')
xml.tag! 'TranCode', (@use_tokenization ? (action + 'ByRecordNo') : action)
add_invoice(xml, invoice_no, ref_no, options)
add_reference(xml, record_no)
add_customer_data(xml, options)
add_amount(xml, (money || amount.to_i), options)
add_credit_card(xml, credit_card, action) if credit_card
add_address(xml, options)
xml.tag! 'TranInfo' do
xml.tag! 'AuthCode', auth_code
xml.tag! 'AcqRefData', acq_ref_data
xml.tag! 'ProcessData', process_data
end
end
end
xml = xml.target!
end
def build_card_lookup_request(credit_card, options)
xml = Builder::XmlMarkup.new
xml.tag! 'TStream' do
xml.tag! 'Transaction' do
xml.tag! 'TranType', 'CardLookup'
xml.tag! 'RecordNo', 'RecordNumberRequested'
xml.tag! 'Frequency', 'OneTime'
xml.tag! 'Memo', options[:description]
add_customer_data(xml, options)
add_credit_card(xml, credit_card, options)
end
end
xml.target!
end
def add_invoice(xml, invoice_no, ref_no, options)
xml.tag! 'InvoiceNo', invoice_no
xml.tag! 'RefNo', (ref_no || invoice_no)
xml.tag! 'OperatorID', options[:merchant] if options[:merchant]
xml.tag! 'Memo', options[:description] if options[:description]
end
def add_reference(xml, record_no)
if @use_tokenization
xml.tag! 'Frequency', 'OneTime'
xml.tag! 'RecordNo', record_no
end
end
def add_customer_data(xml, options)
xml.tag! 'IpAddress', options[:ip] if options[:ip]
if options[:customer]
xml.tag! 'TranInfo' do
xml.tag! 'CustomerCode', options[:customer]
end
end
xml.tag! 'MerchantID', @options[:login]
end
def add_amount(xml, money, options = {})
xml.tag! 'Amount' do
xml.tag! 'Purchase', amount(money)
xml.tag! 'Tax', options[:tax] if options[:tax]
xml.tag! 'Authorize', amount(options[:authorized]) if options[:authorized]
xml.tag! 'Gratuity', amount(options[:tip]) if options[:tip]
end
end
CARD_CODES = {
'visa' => 'VISA',
'master' => 'M/C',
'american_express' => 'AMEX',
'discover' => 'DCVR',
'diners_club' => 'DCLB',
'jcb' => 'JCB'
}
def add_credit_card(xml, credit_card, action)
xml.tag! 'Account' do
if credit_card.track_data.present?
# Track 1 has a start sentinel (STX) of '%' and track 2 is ';'
# Track 1 and 2 have identical end sentinels (ETX) of '?'
# Tracks may or may not have checksum (LRC) after the ETX
# If the track has no STX or is corrupt, we send it as track 1, to let Mercury
# handle with the validation error as it sees fit.
# Track 2 requires having the STX and ETX stripped. Track 1 does not.
# Max-length track 1s require having the STX and ETX stripped. Max is 79 bytes including LRC.
is_track_2 = credit_card.track_data[0] == ';'
etx_index = credit_card.track_data.rindex('?') || credit_card.track_data.length
is_max_track1 = etx_index >= 77
if is_track_2
xml.tag! 'Track2', credit_card.track_data[1...etx_index]
elsif is_max_track1
xml.tag! 'Track1', credit_card.track_data[1...etx_index]
else
xml.tag! 'Track1', credit_card.track_data
end
else
xml.tag! 'AcctNo', credit_card.number
xml.tag! 'ExpDate', expdate(credit_card)
end
end
xml.tag! 'CardType', CARD_CODES[credit_card.brand] if credit_card.brand
include_cvv = !%w(Return PreAuthCapture).include?(action) && !credit_card.track_data.present?
xml.tag! 'CVVData', credit_card.verification_value if include_cvv && credit_card.verification_value
end
def add_address(xml, options)
if billing_address = options[:billing_address] || options[:address]
xml.tag! 'AVS' do
xml.tag! 'Address', billing_address[:address1]
xml.tag! 'Zip', billing_address[:zip]
end
end
end
def parse(action, body)
response = {}
hashify_xml!(unescape_xml(body), response)
response
end
def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)
xml.elements.each('//CmdResponse/*') do |node|
response[node.name.underscore.to_sym] = node.text
end
xml.elements.each('//TranResponse/*') do |node|
if node.name.to_s == 'Amount'
node.elements.each do |amt|
response[amt.name.underscore.to_sym] = amt.text
end
else
response[node.name.underscore.to_sym] = node.text
end
end
end
def endpoint_url
URLS[test? ? :test : :live]
end
def build_soap_request(body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! 'soap:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'soap:Body' do
xml.tag! 'CreditTransaction', 'xmlns' => homepage_url do
xml.tag! 'tran' do
xml << escape_xml(body)
end
xml.tag! 'pw', @options[:password]
end
end
end
xml.target!
end
def build_header
{
'SOAPAction' => 'http://www.mercurypay.com/CreditTransaction',
'Content-Type' => 'text/xml; charset=utf-8'
}
end
SUCCESS_CODES = %w[Approved Success]
def commit(action, request)
response = parse(action, ssl_post(endpoint_url, build_soap_request(request), build_header))
success = SUCCESS_CODES.include?(response[:cmd_status])
message = success ? 'Success' : message_from(response)
Response.new(success, message, response,
test: test?,
authorization: authorization_from(response),
avs_result: { code: response[:avs_result] },
cvv_result: response[:cvv_result],
error_code: success ? nil : STANDARD_ERROR_CODE_MAPPING[response[:dsix_return_code]])
end
def message_from(response)
response[:text_response]
end
def authorization_from(response)
dollars, cents = (response[:purchase] || '').split('.').collect(&:to_i)
dollars ||= 0
cents ||= 0
[
response[:invoice_no],
response[:ref_no],
response[:auth_code],
response[:acq_ref_data],
response[:process_data],
response[:record_no],
((dollars * 100) + cents).to_s
].join(';')
end
def split_authorization(authorization)
invoice_no, ref_no, auth_code, acq_ref_data, process_data, record_no, amount = authorization.split(';')
[invoice_no, ref_no, auth_code, acq_ref_data, process_data, record_no, amount]
end
ENVELOPE_NAMESPACES = {
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
}
def escape_xml(xml)
"\n\n"
end
def unescape_xml(escaped_xml)
escaped_xml.gsub(/\>/, '>').gsub(/\</, '<')
end
end
end
end