class ActiveMerchant::Billing::RedsysGateway

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.

Constants

CURRENCY_CODES
RESPONSE_TEXTS

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.

SUPPORTED_TRANSACTIONS

The set of supported transactions for this gateway. More operations are supported by the gateway itself, but are not supported in this library.

Public Class Methods

new(options = {}) click to toggle source

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)

  • :testtrue or false. Defaults to false. (OPTIONAL)

Calls superclass method ActiveMerchant::Billing::Gateway::new
# File lib/active_merchant/billing/gateways/redsys.rb, line 156
def initialize(options = {})
  requires!(options, :login, :secret_key)
  options[:terminal] ||= 1
  super
end

Public Instance Methods

authorize(money, creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 175
def authorize(money, creditcard, options = {})
  requires!(options, :order_id)

  data = {}
  add_action(data, :authorize)
  add_amount(data, money, options)
  add_order(data, options[:order_id])
  add_creditcard(data, creditcard)
  data[:description] = options[:description]

  commit data
end
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 188
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
purchase(money, creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 162
def purchase(money, creditcard, options = {})
  requires!(options, :order_id)

  data = {}
  add_action(data, :purchase)
  add_amount(data, money, options)
  add_order(data, options[:order_id])
  add_creditcard(data, creditcard)
  data[:description] = options[:description]

  commit data
end
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 210
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
verify(creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 221
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
void(authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 199
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

Private Instance Methods

add_action(data, action) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 230
def add_action(data, action)
  data[:action] = transaction_code(action)
end
add_amount(data, money, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 234
def add_amount(data, money, options)
  data[:amount] = amount(money).to_s
  data[:currency] = currency_code(options[:currency] || currency(money))
end
add_creditcard(data, card) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 247
def add_creditcard(data, card)
  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
add_order(data, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 239
def add_order(data, order_id)
  data[:order_id] = clean_order_id(order_id)
end
build_authorization(params) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 352
def build_authorization(params)
  [params[:ds_order], params[:ds_amount], params[:ds_currency]].join("|")
end
build_signature(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 267
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]
  str << @options[:secret_key]

  Digest::SHA1.hexdigest(str)
end
build_xml_request(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 284
def build_xml_request(data)
  xml = Builder::XmlMarkup.new :indent => 2
  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)

    # 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]
    end
  end
  xml.target!
end
clean_order_id(order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 381
def clean_order_id(order_id)
  cleansed = order_id.gsub(/[^\da-zA-Z]/, '')
  if cleansed =~ /^\d{4}/
    cleansed[0..12]
  else
    "%04d%s" % [rand(0..9999), cleansed[0...8]]
  end
end
commit(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 259
def commit(data)
  headers = {
    'Content-Type' => 'application/x-www-form-urlencoded'
  }
  xml = build_xml_request(data)
  parse(ssl_post(url, "entrada=#{CGI.escape(xml)}", headers))
end
currency_code(currency) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 361
def currency_code(currency)
  return currency if currency =~ /^\d+$/
  raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency]
  CURRENCY_CODES[currency]
end
is_success_response?(code) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 377
def is_success_response?(code)
  (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
end
parse(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 309
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
response_text(code) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 371
def response_text(code)
  code = code.to_i
  code = 0 if code < 100
  RESPONSE_TEXTS[code] || "Unkown code, please check in manual"
end
split_authorization(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 356
def split_authorization(authorization)
  order_id, amount, currency = authorization.split("|")
  [order_id, amount.to_i, currency]
end
transaction_code(type) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 367
def transaction_code(type)
  SUPPORTED_TRANSACTIONS[type]
end
url() click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 243
def url
  test? ? test_url : live_url
end
validate_signature(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 337
def validate_signature(data)
  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