module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class SageGateway < Gateway
include Empty
self.display_name = 'http://www.sagepayments.com'
self.homepage_url = 'Sage Payment Solutions'
self.live_url = 'https://www.sagepayments.net/cgi-bin'
self.supported_countries = ['US', 'CA']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
TRANSACTIONS = {
purchase: '01',
authorization: '02',
capture: '11',
void: '04',
credit: '06',
refund: '10'
}
SOURCE_CARD = 'bankcard'
SOURCE_ECHECK = 'virtual_check'
def initialize(options = {})
requires!(options, :login, :password)
super
end
def authorize(money, credit_card, options = {})
post = {}
add_credit_card(post, credit_card)
add_transaction_data(post, money, options)
commit(:authorization, post, SOURCE_CARD)
end
def purchase(money, payment_method, options = {})
post = {}
if card_brand(payment_method) == 'check'
source = SOURCE_ECHECK
add_check(post, payment_method)
add_check_customer_data(post, options)
else
source = SOURCE_CARD
add_credit_card(post, payment_method)
end
add_transaction_data(post, money, options)
commit(:purchase, post, source)
end
# The +money+ amount is not used. The entire amount of the
# initial authorization will be captured.
def capture(money, reference, options = {})
post = {}
add_reference(post, reference)
commit(:capture, post, SOURCE_CARD)
end
def void(reference, options = {})
post = {}
add_reference(post, reference)
source = reference.split(';').last
commit(:void, post, source)
end
def credit(money, payment_method, options = {})
post = {}
if card_brand(payment_method) == 'check'
source = SOURCE_ECHECK
add_check(post, payment_method)
add_check_customer_data(post, options)
else
source = SOURCE_CARD
add_credit_card(post, payment_method)
end
add_transaction_data(post, money, options)
commit(:credit, post, source)
end
def refund(money, reference, options={})
post = {}
add_reference(post, reference)
add_transaction_data(post, money, options)
commit(:refund, post, SOURCE_CARD)
end
def store(credit_card, options = {})
vault.store(credit_card, options)
end
def unstore(identification, options = {})
vault.unstore(identification, options)
end
def supports_scrubbing?
true
end
def scrub(transcript)
force_utf8(transcript).
gsub(%r((M_id=)[^&]*), '\1[FILTERED]').
gsub(%r((M_key=)[^&]*), '\1[FILTERED]').
gsub(%r((C_cardnumber=)[^&]*), '\1[FILTERED]').
gsub(%r((C_cvv=)[^&]*), '\1[FILTERED]').
gsub(%r((C_rte=)[^&]*), '\1[FILTERED]').
gsub(%r((C_acct=)[^&]*), '\1[FILTERED]').
gsub(%r((C_ssn=)[^&]*), '\1[FILTERED]').
gsub(%r(().+()), '\1[FILTERED]\2').
gsub(%r(().+()), '\1[FILTERED]\2').
gsub(%r(().+()), '\1[FILTERED]\2')
end
private
# use the same method as in pay_conex
def force_utf8(string)
return nil unless string
binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?') # Needed for Ruby 2.0 since #encode is a no-op if the string is already UTF-8. It's not needed for Ruby 2.1 and up since it's not a no-op there.
binary.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
end
def add_credit_card(post, credit_card)
post[:C_name] = credit_card.name
post[:C_cardnumber] = credit_card.number
post[:C_exp] = expdate(credit_card)
post[:C_cvv] = credit_card.verification_value if credit_card.verification_value?
end
def add_check(post, check)
post[:C_first_name] = check.first_name
post[:C_last_name] = check.last_name
post[:C_rte] = check.routing_number
post[:C_acct] = check.account_number
post[:C_check_number] = check.number
post[:C_acct_type] = account_type(check)
end
def add_check_customer_data(post, options)
# Required Customer Type – (NACHA Transaction Class)
# CCD for Commercial, Merchant Initiated
# PPD for Personal, Merchant Initiated
# WEB for Internet, Consumer Initiated
# RCK for Returned Checks
# ARC for Account Receivable Entry
# TEL for TelephoneInitiated
post[:C_customer_type] = 'WEB'
# Optional 10 Digit Originator ID – Assigned By for each transaction class or business purpose. If not provided, the default Originator ID for the specific Customer Type will be applied.
post[:C_originator_id] = options[:originator_id]
# Optional Transaction Addenda
post[:T_addenda] = options[:addenda]
# Required Check Writer Social Security Number ( Numbers Only, No Dashes )
post[:C_ssn] = options[:ssn].to_s.gsub(/[^\d]/, '')
post[:C_dl_state_code] = options[:drivers_license_state]
post[:C_dl_number] = options[:drivers_license_number]
post[:C_dob] = format_birth_date(options[:date_of_birth])
end
def format_birth_date(date)
date.respond_to?(:strftime) ? date.strftime('%m/%d/%Y') : date
end
# DDA for Checking
# SAV for Savings
def account_type(check)
case check.account_type
when 'checking' then 'DDA'
when 'savings' then 'SAV'
else raise ArgumentError, "Unknown account type #{check.account_type}"
end
end
def parse(data, source)
source == SOURCE_ECHECK ? parse_check(data) : parse_credit_card(data)
end
def parse_check(data)
response = {}
response[:success] = data[1, 1]
response[:code] = data[2, 6].strip
response[:message] = data[8, 32].strip
response[:risk] = data[40, 2]
response[:reference] = data[42, 10]
extra_data = data[53...-1].split("\034")
response[:order_number] = extra_data[0]
response[:authentication_indicator] = extra_data[1]
response[:authentication_disclosure] = extra_data[2]
response
end
def parse_credit_card(data)
response = {}
response[:success] = data[1, 1]
response[:code] = data[2, 6]
response[:message] = data[8, 32].strip
response[:front_end] = data[40, 2]
response[:cvv_result] = data[42, 1]
response[:avs_result] = data[43, 1].strip
response[:risk] = data[44, 2]
response[:reference] = data[46, 10]
response[:order_number], response[:recurring] = data[57...-1].split("\034")
response
end
def add_invoice(post, options)
post[:T_ordernum] = (options[:order_id] || generate_unique_id).slice(0, 20)
post[:T_tax] = amount(options[:tax]) unless empty?(options[:tax])
post[:T_shipping] = amount(options[:shipping]) unless empty?(options[:shipping])
end
def add_reference(post, reference)
ref, _ = reference.to_s.split(';')
post[:T_reference] = ref
end
def add_amount(post, money)
post[:T_amt] = amount(money)
end
def add_customer_data(post, options)
post[:T_customer_number] = options[:customer] if Float(options[:customer]) rescue nil
end
def add_addresses(post, options)
billing_address = options[:billing_address] || options[:address] || {}
post[:C_address] = billing_address[:address1]
post[:C_city] = billing_address[:city]
post[:C_state] = empty?(billing_address[:state]) ? 'Outside of US' : billing_address[:state]
post[:C_zip] = billing_address[:zip]
post[:C_country] = billing_address[:country]
post[:C_telephone] = billing_address[:phone]
post[:C_fax] = billing_address[:fax]
post[:C_email] = options[:email]
if shipping_address = options[:shipping_address]
post[:C_ship_name] = shipping_address[:name]
post[:C_ship_address] = shipping_address[:address1]
post[:C_ship_city] = shipping_address[:city]
post[:C_ship_state] = shipping_address[:state]
post[:C_ship_zip] = shipping_address[:zip]
post[:C_ship_country] = shipping_address[:country]
end
end
def add_transaction_data(post, money, options)
add_amount(post, money)
add_invoice(post, options)
add_addresses(post, options)
add_customer_data(post, options)
end
def commit(action, params, source)
url = url(params, source)
response = parse(ssl_post(url, post_data(action, params)), source)
Response.new(success?(response), response[:message], response,
test: test?,
authorization: authorization_from(response, source),
avs_result: { code: response[:avs_result] },
cvv_result: response[:cvv_result]
)
end
def url(params, source)
if source == SOURCE_ECHECK
"#{live_url}/eftVirtualCheck.dll?transaction"
else
"#{live_url}/eftBankcard.dll?transaction"
end
end
def authorization_from(response, source)
"#{response[:reference]};#{source}"
end
def success?(response)
response[:success] == 'A'
end
def post_data(action, params = {})
params[:M_id] = @options[:login]
params[:M_key] = @options[:password]
params[:T_code] = TRANSACTIONS[action]
params.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
def vault
@vault ||= SageVault.new(@options, self)
end
class SageVault
def initialize(options, gateway)
@live_url = 'https://www.sagepayments.net/web_services/wsVault/wsVault.asmx'
@options = options
@gateway = gateway
end
def store(credit_card, options = {})
request = build_store_request(credit_card, options)
commit(:store, request)
end
def unstore(identification, options = {})
request = build_unstore_request(identification, options)
commit(:unstore, request)
end
private
# A valid request example, since the Sage docs have none:
#
#
#
#
#
# 279277516172
# O3I8G2H8V6A3
# 4111111111111111
# 0915
#
#
#
def build_store_request(credit_card, options)
xml = Builder::XmlMarkup.new
add_credit_card(xml, credit_card, options)
xml.target!
end
def build_unstore_request(identification, options)
xml = Builder::XmlMarkup.new
add_identification(xml, identification, options)
xml.target!
end
def add_customer_data(xml)
xml.tag! 'ns1:M_ID', @options[:login]
xml.tag! 'ns1:M_KEY', @options[:password]
end
def add_credit_card(xml, credit_card, options)
xml.tag! 'ns1:CARDNUMBER', credit_card.number
xml.tag! 'ns1:EXPIRATION_DATE', exp_date(credit_card)
end
def add_identification(xml, identification, options)
xml.tag! 'ns1:GUID', identification
end
def exp_date(credit_card)
year = sprintf('%.4i', credit_card.year)
month = sprintf('%.2i', credit_card.month)
"#{month}#{year[-2..-1]}"
end
def commit(action, request)
response = parse(
@gateway.ssl_post(
@live_url,
build_soap_request(action, request),
build_headers(action)
)
)
case action
when :store
success = response[:success] == 'true'
message = response[:message].downcase.capitalize if response[:message]
when :unstore
success = response[:delete_data_result] == 'true'
message = success ? 'Succeeded' : 'Failed'
end
Response.new(success, message, response,
authorization: response[:guid]
)
end
ENVELOPE_NAMESPACES = {
'xmlns:SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/',
'xmlns:ns1' => 'https://www.sagepayments.net/web_services/wsVault/wsVault'
}
ACTION_ELEMENTS = {
store: 'INSERT_CREDIT_CARD_DATA',
unstore: 'DELETE_DATA'
}
def build_soap_request(action, body)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.tag! 'SOAP-ENV:Envelope', ENVELOPE_NAMESPACES do
xml.tag! 'SOAP-ENV:Body' do
xml.tag! "ns1:#{ACTION_ELEMENTS[action]}" do
add_customer_data(xml)
xml << body
end
end
end
xml.target!
end
SOAP_ACTIONS = {
store: 'https://www.sagepayments.net/web_services/wsVault/wsVault/INSERT_CREDIT_CARD_DATA',
unstore: 'https://www.sagepayments.net/web_services/wsVault/wsVault/DELETE_DATA'
}
def build_headers(action)
{
'SOAPAction' => SOAP_ACTIONS[action],
'Content-Type' => 'text/xml; charset=utf-8'
}
end
def parse(body)
response = {}
hashify_xml!(body, response)
response
end
def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)
# Store
xml.elements.each('//Table1/*') do |node|
response[node.name.underscore.to_sym] = node.text
end
# Unstore
xml.elements.each('//DELETE_DATAResponse/*') do |node|
response[node.name.underscore.to_sym] = node.text
end
end
end
end
end
end