require 'rexml/document'
require 'digest/sha1'
module ActiveMerchant
module Billing
# For more information on the Realex Payment Gateway visit their site {realexpayments.com}[http://realexpayments.com].
# Realex is the leading gateway in Ireland
#
# === Merchant ID and Password
#
# To be able to use this library you will need to obtain an account from Realex, you can find contact them
# via their website.
#
# === Caveats
#
# Realex requires that you specify the account to which your transactions are made.
#
# gateway = ActiveMerchant::Billing::Realex3dsGateway.new(:login => 'xxx', :password => 'xxx', :acction => 'xxx')
#
# If you wish to accept multiple currencies, you need to create an account per currency.
# This you would need to handle within your application logic.
# Again, contact Realex for more information.
#
# They also require accepting payment from a Diners card (Mastercard) go through a different account.
#
# Realex also requires that you send several (extra) required identifiers with credit and void methods
#
# * order_id
# * pasref
# * authorization
#
# The pasref can be accessed from the response params. i.e.
# response.params['pasref']
#
# === Testing
#
# Realex provide test card numbers on a per-account basis, you will need to request these.
# Then if you copy the fixtures file that comes with this library to ~/.active_merchant/fixtures.yml
# you can add in the required card number (and account) fixtures.
#
class Realex3dsGateway < Gateway
URL = 'https://epage.payandshop.com/epage-remote.cgi'
THREE_D_SECURE_URL = 'https://epage.payandshop.com/epage-3dsecure.cgi'
RECURRING_PAYMENTS_URL = "https://epage.payandshop.com/epage-remote-plugins.cgi"
CARD_MAPPING = {
'master' => 'MC',
'visa' => 'VISA',
'visa_delta' => 'VISA',
'visa_electron' => 'VISA',
'american_express' => 'AMEX',
'diners_club' => 'DINERS',
'switch' => 'SWITCH',
'solo' => 'SWITCH',
'laser' => 'LASER'
}
self.money_format = :cents
self.default_currency = 'EUR'
self.supported_cardtypes = [ :visa, :master, :american_express, :diners_club, :switch, :solo, :laser ]
self.supported_countries = [ 'IE', 'GB' ]
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.has_key?(:rebate_secret)
@options = options
super
end
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
# charge the card.
#
# ==== Parameters
#
# * money -- The amount to be authorized. Either an Integer value in cents or a Money object.
# * creditcard -- The CreditCard details for the transaction.
# * options -- A hash of optional parameters.
#
# ==== Options
#
# * :order_id -- The application generated order identifier. (REQUIRED)
#
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
if options[:three_d_secure]
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifyenrolled", money, creditcard, options)
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
return three_d_secure_response if three_d_secure_response.enrolled?
end
request = build_purchase_or_authorization_request(:authorization, money, creditcard, options)
commit(request)
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
#
# ==== Parameters
#
# * money -- The amount to be purchased. Either an Integer value in cents or a Money object.
# * creditcard -- The CreditCard details for the transaction.
# * options -- A hash of optional parameters.
#
# ==== Options
#
# * :order_id -- The application generated order identifier. (REQUIRED)
#
def purchase(money, creditcard, options = {})
requires!(options, :order_id)
if options[:three_d_secure_auth]
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifysig", money, creditcard, options)
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
result = three_d_secure_response.params['result']
if result == '00'
status = three_d_secure_response.params['threedsecure_status']
# success
if status == 'Y' || status == 'A'
# Y: 3d Secure complete.
# A: ACS service aknowledges.
# not-liable. continue
options[:three_d_secure_sig] = {}
options[:three_d_secure_sig][:eci] = three_d_secure_response.params['threedsecure_eci']
options[:three_d_secure_sig][:xid] = three_d_secure_response.params['threedsecure_xid']
options[:three_d_secure_sig][:cavv] = three_d_secure_response.params['threedsecure_cavv']
# TODO add option[:accept_liability_authentication_failed]
# TODO add option[:accept_liability_acs_failure]
elsif status == 'N'
# password entered incorrectly
# liable. abort?
return Response.new(false, "3DSecure password entered incorrectly. Aborting transaction.",{},{})
elsif status == 'U'
# Bank ACS service having dificulty.
# liable. abort?
return Response.new(false, "3DSecure Bank ACS service 500 errors. Aborting transaction.",{},{})
end
elsif result == "110"
# fail, message tampered with.
return Response.new(false, "3DSecure message tampered. Aborting transaction.",{},{})
else
return Response.new(false, "Unknown 3DSecure Error.",{},{})
end
end
if options[:three_d_secure] && !options[:three_d_secure_auth]
three_d_secure_request = build_3d_secure_verify_signature_or_enrolled_request("3ds-verifyenrolled", money, creditcard, options)
three_d_secure_response = commit(three_d_secure_request, :three_d_secure)
return three_d_secure_response if three_d_secure_response.enrolled?
end
request = build_purchase_or_authorization_request(:purchase, money, creditcard, options)
commit(request)
end
# Captures the funds from an authorized transaction.
#
# ==== Parameters
#
# * money -- The amount to be captured. Either an Integer value in cents or a Money object.
# * authorization -- The authorization returned from the previous authorize request.
#
# ==== Options
#
# * :order_id -- The application generated order identifier. (REQUIRED)
# * :pasref -- The realex payments reference of the original transaction. (REQUIRED)
#
def capture(money, authorization, options = {})
requires!(options, :pasref)
requires!(options, :order_id)
request = build_capture_request(authorization, options)
commit(request)
end
# Credit an account.
#
# This transaction is also referred to as a Refund (or Rebate) and indicates to the gateway that
# money should flow from the merchant to the customer.
#
# ==== Parameters
#
# * money -- The amount to be credited to the customer. Either an Integer value in cents or a Money object.
# * authorization - The authorization returned from the previous authorize request.
# * options -- A hash of parameters.
#
# ==== Options
#
# * :order_id -- The application generated order identifier. (REQUIRED)
# * :pasref -- The realex payments reference of the original transaction. (REQUIRED)
#
def credit(money, authorization, options = {})
requires!(options, :order_id)
requires!(options, :pasref)
request = build_credit_request(money, authorization, options)
commit(request)
end
# Void a previous transaction
#
# ==== Parameters
#
# * authorization - The authorization returned from the previous authorize request.
#
# ==== Options
#
# * :order_id -- The application generated order identifier. (REQUIRED)
# * :pasref -- The realex payments reference of the original transaction. (REQUIRED)
#
def void(authorization, options = {})
requires!(options, :order_id)
requires!(options, :pasref)
request = build_void_request(authorization, options)
commit(request)
end
# Recurring Payments
def recurring(money, credit_card, options = {})
requires!(options, :order_id)
request = build_receipt_in_request(money, credit_card, options)
commit(request, :recurring)
end
def store(credit_card, options = {})
requires!(options, :order_id)
request = build_new_card_request(credit_card, options)
commit(request, :recurring)
end
def unstore(creditcard, options = {})
request = build_cancel_card_request(creditcard, options)
commit(request, :recurring)
end
def store_user(options = {})
requires!(options, :order_id)
request = build_new_payee_request(options)
commit(request, :recurring)
end
private
def commit(request, endpoint=:default)
url = URL
url = THREE_D_SECURE_URL if endpoint == :three_d_secure
url = RECURRING_PAYMENTS_URL if endpoint == :recurring
response = ssl_post(url, request)
parsed = parse(response)
options = {
:test => parsed[:message] =~ /\[ test system \]/,
:authorization => parsed[:authcode],
:cvv_result => parsed[:cvnresult],
:body => response,
:avs_result => {
:street_match => parsed[:avsaddressresponse],
:postal_match => parsed[:avspostcoderesponse]
}
}
if endpoint == :three_d_secure
options.merge!({
:pa_req => parsed[:pareq],
:acs_url => parsed[:url],
:three_d_secure => true,
:xid => parsed[:xid],
:three_d_secure_enrolled => parsed[:enrolled] == "Y" ? true : false
})
end
Response.new(parsed[:result] == "00", message_from(parsed), parsed, options)
end
def parse(xml)
response = {}
xml = REXML::Document.new(xml)
return response unless xml.root
xml.elements.each('//response/*') 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
response
end
def build_purchase_or_authorization_request(action, money, credit_card, options)
timestamp = self.class.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_ammount(xml, money, options)
add_card(xml, credit_card)
xml.tag! 'autosettle', 'flag' => auto_settle_flag(action)
add_three_d_secure(xml, options) if options[:three_d_secure_sig]
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), credit_card.number)
add_comments(xml, options)
add_address_and_customer_info(xml, options)
end
xml.target!
end
def build_capture_request(authorization, options)
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'settle' do
add_merchant_details(xml, options)
add_transaction_identifiers(xml, authorization, options)
add_comments(xml, options)
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', '')
end
xml.target!
end
def build_credit_request(money, authorization, options)
timestamp = self.class.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], options[:order_id], amount(money), (options[:currency] || currency(money)), '')
end
xml.target!
end
def build_void_request(authorization, options)
timestamp = self.class.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], options[:order_id], '', '', '')
end
xml.target!
end
def build_cancel_card_request(creditcard, options = {})
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'card-cancel-card' do
add_merchant_details(xml, options)
xml.tag! 'card' do
xml.tag! 'ref', options[:payment_method]
xml.tag! 'payerref', options[:user][:id]
xml.tag! 'expdate', expiry_date(creditcard)
end
# TODO userid . card ref . expiry date
add_signed_digest(xml, timestamp, @options[:login], options[:user][:id], options[:payment_method])
end
end
def build_new_card_request(credit_card, options = {})
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'card-new' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
xml.tag! 'card' do
xml.tag! 'ref', options[:payment_method]
xml.tag! 'payerref', options[:user][:id]
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', credit_card.issue_number
xml.tag! 'cvn' do
xml.tag! 'number', credit_card.verification_value
xml.tag! 'presind', (options['presind'] || (credit_card.verification_value? ? 1 : nil))
end
end
# timestamp.merchantid.orderid.amount.currency.payerref.chname.(card)number
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', options[:user][:id], credit_card.name, credit_card.number)
end
xml.target!
end
def build_new_payee_request(options)
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'payer-new' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
xml.tag! 'payer', 'type' => 'Business', 'ref' => options[:user][:id] do
xml.tag! 'firstname', options[:user][:first_name]
xml.tag! 'surname', options[:user][:last_name]
end
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], '', '', options[:user][:id])
end
xml.target!
end
def build_3d_secure_verify_signature_or_enrolled_request(action, money, credit_card, options)
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => action do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
add_ammount(xml, money, options)
add_card(xml, credit_card)
xml.tag!('pares', options[:three_d_secure_auth][:pa_res]) if(action == '3ds-verifysig' && options[:three_d_secure_auth] )
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), credit_card.number)
add_comments(xml, options)
end
xml.target!
end
def add_three_d_secure(xml, options)
if options[:three_d_secure_sig]
xml.tag! 'mpi' do
xml.tag! 'cavv', options[:three_d_secure_sig][:cavv]
xml.tag! 'xid', options[:three_d_secure_sig][:xid]
xml.tag! 'eci', options[:three_d_secure_sig][:eci]
end
end
end
def build_receipt_in_request(money, credit_card, options)
timestamp = self.class.timestamp
xml = Builder::XmlMarkup.new :indent => 2
xml.tag! 'request', 'timestamp' => timestamp, 'type' => 'receipt-in' do
add_merchant_details(xml, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
add_ammount(xml, money, options)
xml.tag! 'payerref', options[:user][:id]
xml.tag! 'paymentmethod', options[:payment_method]
xml.tag! 'autosettle', 'flag' => '1'
add_signed_digest(xml, timestamp, @options[:login], options[:order_id], amount(money), (options[:currency] || currency(money)), options[:user][:id])
add_comments(xml, options)
add_address_and_customer_info(xml, options)
end
xml.target!
end
def add_address_and_customer_info(xml, options)
billing_address = options[:billing_address] || options[:address]
shipping_address = options[:shipping_address]
return unless billing_address || shipping_address || options[:customer] || options[:invoice] || options[:ip]
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 options[:ip]
# xml.tag! 'varref'
if billing_address
xml.tag! 'address', 'type' => 'billing' do
xml.tag! 'code', avs_input_code_or_zip( billing_address, options )
xml.tag! 'country', billing_address[:country]
end
end
if shipping_address
xml.tag! 'address', 'type' => 'shipping' do
xml.tag! 'code', shipping_address[:zip]
xml.tag! 'country', shipping_address[:country]
end
end
end
end
def avs_input_code_or_zip(address, options)
options[ :skip_avs_check ] ? address[ :zip ] : avs_input_code( address )
end
def add_merchant_details(xml, options)
xml.tag! 'merchantid', @options[:login]
if options[:account] || @options[:account]
xml.tag! 'account', options[:account] || @options[:account]
end
end
def add_transaction_identifiers(xml, authorization, options)
xml.tag! 'orderid', sanitize_order_id(options[:order_id])
xml.tag! 'pasref', options[:pasref]
xml.tag! 'authcode', authorization
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_ammount(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', credit_card.issue_number
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 avs_input_code(address)
address.values_at(:zip, :address1).map{ |v| extract_digits(v) }.join('|')
end
def extract_digits(string)
return "" if string.nil?
string.gsub(/[\D]/,'')
end
def stringify_values(values)
string = ""
values.each do |val|
string << "#{val}"
string << "." unless val.equal?(values.last)
end
string
end
def add_signed_digest(xml, *values)
string = stringify_values(values)
xml.tag! 'sha1hash', sha1from(string)
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 sha1from(string)
Digest::SHA1.hexdigest("#{Digest::SHA1.hexdigest(string)}.#{@options[:password]}")
end
def normalize(field)
case field
when "true" then true
when "false" then false
when "" then nil
when "null" then nil
else field
end
end
def message_from(response)
message = nil
case response[:result]
when "00"
message = SUCCESS
when "101"
message = response[:message]
when "102", "103"
message = DECLINED
when /^2[0-9][0-9]/
message = BANK_ERROR
when /^3[0-9][0-9]/
message = REALEX_ERROR
when /^5[0-9][0-9]/
message = response[:message]
when "600", "601", "603"
message = ERROR
when "666"
message = CLIENT_DEACTIVATED
else
message = DECLINED
end
end
def sanitize_order_id(order_id)
order_id.to_s.gsub(/[^a-zA-Z0-9\-_]/, '')
end
def self.timestamp
Time.now.strftime('%Y%m%d%H%M%S')
end
end
end
end