lib/active_merchant/billing/gateways/realex.rb
require 'nokogiri'
require 'digest/sha1'
module ActiveMerchant
module Billing
# Realex is the leading CC gateway in Ireland
# see http://www.realexpayments.com
# Contributed by John Ward (john@ward.name)
# see http://thinedgeofthewedge.blogspot.com
#
# Realex works using the following
# login - The unique id of the merchant
# password - The secret is used to digitally sign the request
# account - This is an optional third part of the authentication process
# and is used if the merchant wishes do distinguish cc traffic from the different sources
# by using a different account. This must be created in advance
#
# the Realex team decided to make the orderid unique per request,
# so if validation fails you can not correct and resend using the
# same order id
class RealexGateway < Gateway
self.live_url = self.test_url = 'https://epage.payandshop.com/epage-remote.cgi'
CARD_MAPPING = {
'master' => 'MC',
'visa' => 'VISA',
'american_express' => 'AMEX',
'diners_club' => 'DINERS',
'maestro' => 'MC'
}
self.money_format = :cents
self.default_currency = 'EUR'
self.supported_cardtypes = %i[visa master american_express diners_club]
self.supported_countries = %w(IE GB FR BE NL LU IT US CA ES)
self.homepage_url = 'http://www.realexpayments.com/'
self.display_name = 'Realex'
SUCCESS, DECLINED = 'Successful', 'Declined'
BANK_ERROR = REALEX_ERROR = 'Gateway is in maintenance. Please try again later.'
ERROR = CLIENT_DEACTIVATED = 'Gateway Error'
def initialize(options = {})
requires!(options, :login, :password)
options[:refund_hash] = Digest::SHA1.hexdigest(options[:rebate_secret]) if options[:rebate_secret].present?
options[:credit_hash] = Digest::SHA1.hexdigest(options[:refund_secret]) if options[:refund_secret].present?
super
end
def purchase(money, credit_card, options = {})
requires!(options, :order_id)
request = build_purchase_or_authorization_request(:purchase, money, credit_card, options)
commit(request)
end
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
request = build_purchase_or_authorization_request(:authorization, money, creditcard, options)
commit(request)
end
def capture(money, authorization, options = {})
request = build_capture_request(money, authorization, options)
commit(request)
end
def refund(money, authorization, options = {})
request = build_refund_request(money, authorization, options)
commit(request)
end
def credit(money, creditcard, options = {})
request = build_credit_request(money, creditcard, options)
commit(request)
end
def void(authorization, options = {})
request = build_void_request(authorization, options)
commit(request)
end
def verify(credit_card, options = {})
requires!(options, :order_id)
request = build_verify_request(credit_card, options)
commit(request)
end
def supports_scrubbing
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((<number>)\d+(</number>))i, '\1[FILTERED]\2')
end
private
def commit(request)
response = parse(ssl_post(self.live_url, request))
Response.new(
(response[:result] == '00'),
message_from(response),
response,
test: (response[:message] =~ %r{\[ test system \]}),
authorization: authorization_from(response),
avs_result: AVSResult.new(code: response[:avspostcoderesponse]),
cvv_result: CVVResult.new(response[:cvnresult])
)
end
def parse(xml)
response = {}
doc = Nokogiri::XML(xml)
doc.xpath('//response/*').each do |node|
if node.elements.size == 0
response[node.name.downcase.to_sym] = normalize(node.text)
else
node.elements.each do |childnode|
name = "#{node.name.downcase}_#{childnode.name.downcase}"
response[name.to_sym] = normalize(childnode.text)
end
end
end unless doc.root.nil?
response
end
def authorization_from(parsed)
[parsed[:orderid], parsed[:pasref], parsed[:authcode]].join(';')
end
def build_purchase_or_authorization_request(action, money, credit_card, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'auth' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
add_amount(xml, money, options)
add_card(xml, credit_card)
xml.tag! 'autosettle', 'flag' => auto_settle_flag(action)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), credit_card.number)
if credit_card.is_a?(NetworkTokenizationCreditCard)
add_network_tokenization_card(xml, credit_card)
else
add_three_d_secure(xml, options)
end
add_stored_credential(xml, options)
add_comments(xml, options)
add_address_and_customer_info(xml, options)
end
xml.target!
end
def build_capture_request(money, authorization, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'settle' do
add_merchant_details(xml, options)
add_amount(xml, money, options)
add_transaction_identifiers(xml, authorization, options)
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), nil)
end
xml.target!
end
def build_refund_request(money, authorization, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'rebate' do
add_merchant_details(xml, options)
add_transaction_identifiers(xml, authorization, options)
xml.tag! 'amount', amount(money), 'currency' => options[:currency] || currency(money)
xml.tag! 'refundhash', @options[:refund_hash] if @options[:refund_hash]
xml.tag! 'autosettle', 'flag' => 1
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), nil)
end
xml.target!
end
def build_credit_request(money, credit_card, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'credit' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
add_amount(xml, money, options)
add_card(xml, credit_card)
xml.tag! 'refundhash', @options[:credit_hash] if @options[:credit_hash]
xml.tag! 'autosettle', 'flag' => 1
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), amount(money), (options[:currency] || currency(money)), credit_card.number)
end
xml.target!
end
def build_void_request(authorization, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'void' do
add_merchant_details(xml, options)
add_transaction_identifiers(xml, authorization, options)
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), nil, nil, nil)
end
xml.target!
end
# Verify initiates an OTB (Open To Buy) request
def build_verify_request(credit_card, options)
timestamp = new_timestamp
xml = Builder::XmlMarkup.new indent: 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'otb' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
add_card(xml, credit_card)
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], sanitize_order_id(options[:order_id]), credit_card.number)
end
xml.target!
end
def add_address_and_customer_info(xml, options)
billing_address = options[:billing_address] || options[:address]
shipping_address = options[:shipping_address]
ipv4_address = ipv4?(options[:ip]) ? options[:ip] : nil
return unless billing_address || shipping_address || options[:customer] || options[:invoice] || ipv4_address
xml.tag! 'tssinfo' do
xml.tag! 'custnum', options[:customer] if options[:customer]
xml.tag! 'prodid', options[:invoice] if options[:invoice]
xml.tag! 'custipaddress', options[:ip] if ipv4_address
if billing_address
xml.tag! 'address', 'type' => 'billing' do
xml.tag! 'code', format_address_code(billing_address)
xml.tag! 'country', billing_address[:country]
end
end
if shipping_address
xml.tag! 'address', 'type' => 'shipping' do
xml.tag! 'code', format_address_code(shipping_address)
xml.tag! 'country', shipping_address[:country]
end
end
end
end
def add_merchant_details(xml, options)
xml.tag! 'merchantid', @options[:login]
xml.tag! 'account', (options[:account] || @options[:account]) if options[:account] || @options[:account]
end
def add_transaction_identifiers(xml, authorization, options)
options[:order_id], pasref, authcode = authorization.split(';')
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
xml.tag! 'pasref', pasref
xml.tag! 'authcode', authcode
end
def add_comments(xml, options)
return unless options[:description]
xml.tag! 'comments' do
xml.tag! 'comment', options[:description], 'id' => 1
end
end
def add_amount(xml, money, options)
xml.tag! 'amount', amount(money), 'currency' => options[:currency] || currency(money)
end
def add_card(xml, credit_card)
xml.tag! 'card' do
xml.tag! 'number', credit_card.number
xml.tag! 'expdate', expiry_date(credit_card)
xml.tag! 'chname', credit_card.name
xml.tag! 'type', CARD_MAPPING[card_brand(credit_card).to_s]
xml.tag! 'issueno', ''
xml.tag! 'cvn' do
xml.tag! 'number', credit_card.verification_value
xml.tag! 'presind', (options['presind'] || (credit_card.verification_value? ? 1 : nil))
end
end
end
def add_network_tokenization_card(xml, payment)
xml.tag! 'mpi' do
xml.tag! 'cavv', payment.payment_cryptogram
xml.tag! 'eci', payment.eci
end
xml.tag! 'supplementarydata' do
xml.tag! 'item', 'type' => 'mobile' do
xml.tag! 'field01', payment.source.to_s.tr('_', '-')
end
end
end
def add_three_d_secure(xml, options)
return unless three_d_secure = options[:three_d_secure]
version = three_d_secure.fetch(:version, '')
xml.tag! 'mpi' do
if /^2/.match?(version)
xml.tag! 'authentication_value', three_d_secure[:cavv]
xml.tag! 'ds_trans_id', three_d_secure[:ds_transaction_id]
else
xml.tag! 'cavv', three_d_secure[:cavv]
xml.tag! 'xid', three_d_secure[:xid]
version = '1'
end
xml.tag! 'eci', three_d_secure[:eci]
xml.tag! 'message_version', version
end
end
def add_stored_credential(xml, options)
return unless stored_credential = options[:stored_credential]
xml.tag! 'storedcredential' do
xml.tag! 'type', stored_credential_type(stored_credential[:reason_type])
xml.tag! 'initiator', stored_credential[:initiator]
xml.tag! 'sequence', stored_credential[:initial_transaction] ? 'first' : 'subsequent'
xml.tag! 'srd', stored_credential[:network_transaction_id]
end
end
def stored_credential_type(reason)
return 'oneoff' if reason == 'unscheduled'
reason
end
def format_address_code(address)
code = [address[:zip].to_s, address[:address1].to_s + address[:address2].to_s]
code.collect { |e| e.gsub(/\D/, '') }.reject(&:empty?).join('|')
end
def new_timestamp
Time.now.strftime('%Y%m%d%H%M%S')
end
def add_signed_digest(xml, *values)
string = Digest::SHA1.hexdigest(values.join('.'))
xml.tag! 'sha1hash', Digest::SHA1.hexdigest([string, @options[:password]].join('.'))
end
def auto_settle_flag(action)
action == :authorization ? '0' : '1'
end
def expiry_date(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end
def message_from(response)
case response[:result]
when '00'
SUCCESS
when '101'
response[:message]
when '102', '103'
DECLINED
when /^2[0-9][0-9]/
BANK_ERROR
when /^3[0-9][0-9]/
REALEX_ERROR
when /^5[0-9][0-9]/
response[:message]
when '600', '601', '603'
ERROR
when '666'
CLIENT_DEACTIVATED
else
DECLINED
end
end
def sanitize_order_id(order_id)
order_id.to_s.gsub(/[^a-zA-Z0-9\-_]/, '')
end
def ipv4?(ip_address)
return false if ip_address.nil?
!ip_address.match(/\A\d+\.\d+\.\d+\.\d+\z/).nil?
end
end
end
end