# coding: utf-8
require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# = Redsys Merchant Gateway
#
# Gateway support for the Spanish "Redsys" payment gateway system. This is
# used by many banks in Spain and is particularly well supported by
# Catalunya Caixa's ecommerce department.
#
# Redsys requires an order_id be provided with each transaction and it must
# follow a specific format. The rules are as follows:
#
# * First 4 digits must be numerical
# * Remaining 8 digits may be alphanumeric
# * Max length: 12
#
# If an invalid order_id is provided, we do our best to clean it up.
#
# Much of the code for this library is based on the active_merchant_sermepa
# integration gateway which uses essentially the same API but with the
# banks own payment screen.
#
# Written by Samuel Lown for Cabify. For implementation questions, or
# test access details please get in touch: sam@cabify.com.
#
# *** SHA256 Authentication Update ***
#
# Redsys is dropping support for the SHA1 authentication method. This
# adapter has been updated to work with the new SHA256 authentication
# method, however in your initialization options hash you will need to
# specify the key/value :signature_algorithm => "sha256" to use the
# SHA256 method. Otherwise it will default to using the SHA1.
#
#
class RedsysGateway < Gateway
self.live_url = 'https://sis.sermepa.es/sis/operaciones'
self.test_url = 'https://sis-t.redsys.es:25443/sis/operaciones'
self.supported_countries = ['ES']
self.default_currency = 'EUR'
self.money_format = :cents
# Not all card types may be activated by the bank!
self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :diners_club]
self.homepage_url = 'http://www.redsys.es/'
self.display_name = 'Redsys'
CURRENCY_CODES = {
'AED' => '784',
'ARS' => '32',
'AUD' => '36',
'BRL' => '986',
'BOB' => '68',
'CAD' => '124',
'CHF' => '756',
'CLP' => '152',
'CNY' => '156',
'COP' => '170',
'CRC' => '188',
'CZK' => '203',
'DKK' => '208',
'DOP' => '214',
'EUR' => '978',
'GBP' => '826',
'GTQ' => '320',
'HUF' => '348',
'IDR' => '360',
'INR' => '356',
'JPY' => '392',
'KRW' => '410',
'MYR' => '458',
'MXN' => '484',
'NOK' => '578',
'NZD' => '554',
'PEN' => '604',
'PLN' => '985',
'RUB' => '643',
'SAR' => '682',
'SEK' => '752',
'SGD' => '702',
'THB' => '764',
'TWD' => '901',
'USD' => '840',
'UYU' => '858'
}
# The set of supported transactions for this gateway.
# More operations are supported by the gateway itself, but
# are not supported in this library.
SUPPORTED_TRANSACTIONS = {
:purchase => 'A',
:authorize => '1',
:capture => '2',
:refund => '3',
:cancel => '9'
}
# These are the text meanings sent back by the acquirer when
# a card has been rejected. Syntax or general request errors
# are not covered here.
RESPONSE_TEXTS = {
0 => 'Transaction Approved',
400 => 'Cancellation Accepted',
481 => 'Cancellation Accepted',
500 => 'Reconciliation Accepted',
900 => 'Refund / Confirmation approved',
101 => 'Card expired',
102 => 'Card blocked temporarily or under susciption of fraud',
104 => 'Transaction not permitted',
107 => 'Contact the card issuer',
109 => 'Invalid identification by merchant or POS terminal',
110 => 'Invalid amount',
114 => 'Card cannot be used to the requested transaction',
116 => 'Insufficient credit',
118 => 'Non-registered card',
125 => 'Card not effective',
129 => 'CVV2/CVC2 Error',
167 => 'Contact the card issuer: suspected fraud',
180 => 'Card out of service',
181 => 'Card with credit or debit restrictions',
182 => 'Card with credit or debit restrictions',
184 => 'Authentication error',
190 => 'Refusal with no specific reason',
191 => 'Expiry date incorrect',
201 => 'Card expired',
202 => 'Card blocked temporarily or under suspicion of fraud',
204 => 'Transaction not permitted',
207 => 'Contact the card issuer',
208 => 'Lost or stolen card',
209 => 'Lost or stolen card',
280 => 'CVV2/CVC2 Error',
290 => 'Declined with no specific reason',
480 => 'Original transaction not located, or time-out exceeded',
501 => 'Original transaction not located, or time-out exceeded',
502 => 'Original transaction not located, or time-out exceeded',
503 => 'Original transaction not located, or time-out exceeded',
904 => 'Merchant not registered at FUC',
909 => 'System error',
912 => 'Issuer not available',
913 => 'Duplicate transmission',
916 => 'Amount too low',
928 => 'Time-out exceeded',
940 => 'Transaction cancelled previously',
941 => 'Authorization operation already cancelled',
942 => 'Original authorization declined',
943 => 'Different details from origin transaction',
944 => 'Session error',
945 => 'Duplicate transmission',
946 => 'Cancellation of transaction while in progress',
947 => 'Duplicate tranmission while in progress',
949 => 'POS Inoperative',
950 => 'Refund not possible',
9064 => 'Card number incorrect',
9078 => 'No payment method available',
9093 => 'Non-existent card',
9218 => 'Recursive transaction in bad gateway',
9253 => 'Check-digit incorrect',
9256 => 'Preauth not allowed for merchant',
9257 => 'Preauth not allowed for card',
9261 => 'Operating limit exceeded',
9912 => 'Issuer not available',
9913 => 'Confirmation error',
9914 => 'KO Confirmation'
}
# Creates a new instance
#
# Redsys requires a login and secret_key, and optionally also accepts a
# non-default terminal.
#
# ==== Options
#
# * :login -- The Redsys Merchant ID (REQUIRED)
# * :secret_key -- The Redsys Secret Key. (REQUIRED)
# * :terminal -- The Redsys Terminal. Defaults to 1. (OPTIONAL)
# * :test -- +true+ or +false+. Defaults to +false+. (OPTIONAL)
# * :signature_algorithm -- +"sha256"+ Defaults to +"sha1"+. (OPTIONAL)
def initialize(options = {})
requires!(options, :login, :secret_key)
options[:terminal] ||= 1
options[:signature_algorithm] ||= 'sha1'
super
end
def purchase(money, payment, options = {})
requires!(options, :order_id)
data = {}
add_action(data, :purchase)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
data[:description] = options[:description]
data[:store_in_vault] = options[:store]
commit data
end
def authorize(money, payment, options = {})
requires!(options, :order_id)
data = {}
add_action(data, :authorize)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
data[:description] = options[:description]
data[:store_in_vault] = options[:store]
commit data
end
def capture(money, authorization, options = {})
data = {}
add_action(data, :capture)
add_amount(data, money, options)
order_id, _, _ = split_authorization(authorization)
add_order(data, order_id)
data[:description] = options[:description]
commit data
end
def void(authorization, options = {})
data = {}
add_action(data, :cancel)
order_id, amount, currency = split_authorization(authorization)
add_amount(data, amount, :currency => currency)
add_order(data, order_id)
data[:description] = options[:description]
commit data
end
def refund(money, authorization, options = {})
data = {}
add_action(data, :refund)
add_amount(data, money, options)
order_id, _, _ = split_authorization(authorization)
add_order(data, order_id)
data[:description] = options[:description]
commit data
end
def verify(creditcard, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, creditcard, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
def supports_scrubbing
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((%3CDS_MERCHANT_PAN%3E)\d+(%3C%2FDS_MERCHANT_PAN%3E))i, '\1[FILTERED]\2').
gsub(%r((%3CDS_MERCHANT_CVV2%3E)\d+(%3C%2FDS_MERCHANT_CVV2%3E))i, '\1[FILTERED]\2').
gsub(%r(()\d+())i, '\1[FILTERED]\2').
gsub(%r(()\d+())i, '\1[FILTERED]\2').
gsub(%r((DS_MERCHANT_CVV2)%2F%3E%0A%3C%2F)i, '\1[BLANK]').
gsub(%r((DS_MERCHANT_CVV2)%2F%3E%3C)i, '\1[BLANK]').
gsub(%r((DS_MERCHANT_CVV2%3E)(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2').
gsub(%r(()())i, '\1[BLANK]\2').
gsub(%r((DS_MERCHANT_CVV2%3E)\++(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2').
gsub(%r(()\s+())i, '\1[BLANK]\2')
end
private
def add_action(data, action)
data[:action] = transaction_code(action)
end
def add_amount(data, money, options)
data[:amount] = amount(money).to_s
data[:currency] = currency_code(options[:currency] || currency(money))
end
def add_order(data, order_id)
data[:order_id] = clean_order_id(order_id)
end
def url
test? ? test_url : live_url
end
def add_payment(data, card)
if card.is_a?(String)
data[:credit_card_token] = card
else
name = [card.first_name, card.last_name].join(' ').slice(0, 60)
year = sprintf('%.4i', card.year)
month = sprintf('%.2i', card.month)
data[:card] = {
:name => name,
:pan => card.number,
:date => "#{year[2..3]}#{month}",
:cvv => card.verification_value
}
end
end
def commit(data)
parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers))
end
def headers
{
'Content-Type' => 'application/x-www-form-urlencoded'
}
end
def xml_request_from(data)
if sha256_authentication?
build_sha256_xml_request(data)
else
build_sha1_xml_request(data)
end
end
def build_signature(data)
str = data[:amount] +
data[:order_id].to_s +
@options[:login].to_s +
data[:currency]
if card = data[:card]
str << card[:pan]
str << card[:cvv] if card[:cvv]
end
str << data[:action]
if data[:store_in_vault]
str << 'REQUIRED'
elsif data[:credit_card_token]
str << data[:credit_card_token]
end
str << @options[:secret_key]
Digest::SHA1.hexdigest(str)
end
def build_sha256_xml_request(data)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.REQUEST do
build_merchant_data(xml, data)
xml.DS_SIGNATUREVERSION 'HMAC_SHA256_V1'
xml.DS_SIGNATURE sign_request(merchant_data_xml(data), data[:order_id])
end
xml.target!
end
def build_sha1_xml_request(data)
xml = Builder::XmlMarkup.new :indent => 2
build_merchant_data(xml, data)
xml.target!
end
def merchant_data_xml(data)
xml = Builder::XmlMarkup.new
build_merchant_data(xml, data)
xml.target!
end
def build_merchant_data(xml, data)
xml.DATOSENTRADA do
# Basic elements
xml.DS_Version 0.1
xml.DS_MERCHANT_CURRENCY data[:currency]
xml.DS_MERCHANT_AMOUNT data[:amount]
xml.DS_MERCHANT_ORDER data[:order_id]
xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description]
xml.DS_MERCHANT_TERMINAL @options[:terminal]
xml.DS_MERCHANT_MERCHANTCODE @options[:login]
xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication?
# Only when card is present
if data[:card]
xml.DS_MERCHANT_TITULAR data[:card][:name]
xml.DS_MERCHANT_PAN data[:card][:pan]
xml.DS_MERCHANT_EXPIRYDATE data[:card][:date]
xml.DS_MERCHANT_CVV2 data[:card][:cvv]
xml.DS_MERCHANT_IDENTIFIER 'REQUIRED' if data[:store_in_vault]
elsif data[:credit_card_token]
xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token]
xml.DS_MERCHANT_DIRECTPAYMENT 'true'
end
end
end
def parse(data)
params = {}
success = false
message = ''
options = @options.merge(:test => test?)
xml = Nokogiri::XML(data)
code = xml.xpath('//RETORNOXML/CODIGO').text
if code == '0'
op = xml.xpath('//RETORNOXML/OPERACION')
op.children.each do |element|
params[element.name.downcase.to_sym] = element.text
end
if validate_signature(params)
message = response_text(params[:ds_response])
options[:authorization] = build_authorization(params)
success = is_success_response?(params[:ds_response])
else
message = 'Response failed validation check'
end
else
# Some kind of programmer error with the request!
message = "#{code} ERROR"
end
Response.new(success, message, params, options)
end
def validate_signature(data)
if sha256_authentication?
sig = Base64.strict_encode64(mac256(get_key(data[:ds_order].to_s), xml_signed_fields(data)))
sig.upcase == data[:ds_signature].to_s.upcase
else
str = data[:ds_amount] +
data[:ds_order].to_s +
data[:ds_merchantcode] +
data[:ds_currency] +
data[:ds_response] +
data[:ds_cardnumber].to_s +
data[:ds_transactiontype].to_s +
data[:ds_securepayment].to_s +
@options[:secret_key]
sig = Digest::SHA1.hexdigest(str)
data[:ds_signature].to_s.downcase == sig
end
end
def build_authorization(params)
[params[:ds_order], params[:ds_amount], params[:ds_currency]].join('|')
end
def split_authorization(authorization)
order_id, amount, currency = authorization.split('|')
[order_id, amount.to_i, currency]
end
def currency_code(currency)
return currency if currency =~ /^\d+$/
raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency]
CURRENCY_CODES[currency]
end
def transaction_code(type)
SUPPORTED_TRANSACTIONS[type]
end
def response_text(code)
code = code.to_i
code = 0 if code < 100
RESPONSE_TEXTS[code] || 'Unkown code, please check in manual'
end
def is_success_response?(code)
(code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
end
def clean_order_id(order_id)
cleansed = order_id.gsub(/[^\da-zA-Z]/, '')
if cleansed =~ /^\d{4}/
cleansed[0..11]
else
'%04d%s' % [rand(0..9999), cleansed[0...8]]
end
end
def sha256_authentication?
@options[:signature_algorithm] == 'sha256'
end
def sign_request(xml_request_string, order_id)
key = encrypt(@options[:secret_key], order_id)
Base64.strict_encode64(mac256(key, xml_request_string))
end
def encrypt(key, order_id)
block_length = 8
cipher = OpenSSL::Cipher.new('DES3')
cipher.encrypt
cipher.key = Base64.strict_decode64(key)
# The OpenSSL default of an all-zeroes ("\\0") IV is used.
cipher.padding = 0
order_id += "\0" until order_id.bytesize % block_length == 0 # Pad with zeros
output = cipher.update(order_id) + cipher.final
output
end
def mac256(key, data)
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
end
def xml_signed_fields(data)
xml_signed_fields = data[:ds_amount] + data[:ds_order] + data[:ds_merchantcode] +
data[:ds_currency] + data[:ds_response]
if data[:ds_cardnumber]
xml_signed_fields += data[:ds_cardnumber]
end
xml_signed_fields += data[:ds_transactiontype] + data[:ds_securepayment]
end
def get_key(order_id)
encrypt(@options[:secret_key], order_id)
end
end
end
end