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.

*** 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.

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)

  • :signature_algorithm – +“sha256”+ Defaults to +“sha1”+. (OPTIONAL)

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

Public Instance Methods

authorize(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 206
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
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 220
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, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 192
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
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 242
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
scrub(transcript) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 264
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((<DS_MERCHANT_PAN>)\d+(</DS_MERCHANT_PAN>))i, '\1[FILTERED]\2').
    gsub(%r((<DS_MERCHANT_CVV2>)\d+(</DS_MERCHANT_CVV2>))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((<DS_MERCHANT_CVV2>)(</DS_MERCHANT_CVV2>))i, '\1[BLANK]\2').
    gsub(%r((DS_MERCHANT_CVV2%3E)\++(%3C%2FDS_MERCHANT_CVV2))i, '\1[BLANK]\2').
    gsub(%r((<DS_MERCHANT_CVV2>)\s+(</DS_MERCHANT_CVV2>))i, '\1[BLANK]\2')
end
supports_scrubbing() click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 260
def supports_scrubbing
  true
end
verify(creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 253
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 231
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 281
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 285
def add_amount(data, money, options)
  data[:amount] = amount(money).to_s
  data[:currency] = currency_code(options[:currency] || currency(money))
end
add_order(data, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 290
def add_order(data, order_id)
  data[:order_id] = clean_order_id(order_id)
end
add_payment(data, card) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 298
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
build_authorization(params) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 452
def build_authorization(params)
  [params[:ds_order], params[:ds_amount], params[:ds_currency]].join('|')
end
build_merchant_data(xml, data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 377
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
build_sha1_xml_request(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 365
def build_sha1_xml_request(data)
  xml = Builder::XmlMarkup.new :indent => 2
  build_merchant_data(xml, data)
  xml.target!
end
build_sha256_xml_request(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 354
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
build_signature(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 332
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
clean_order_id(order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 481
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
commit(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 314
def commit(data)
  parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers))
end
currency_code(currency) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 461
def currency_code(currency)
  return currency if currency =~ /^\d+$/
  raise ArgumentError, "Unknown currency #{currency}" unless CURRENCY_CODES[currency]
  CURRENCY_CODES[currency]
end
encrypt(key, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 499
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
get_key(order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 529
def get_key(order_id)
  encrypt(@options[:secret_key], order_id)
end
headers() click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 318
def headers
  {
    'Content-Type' => 'application/x-www-form-urlencoded'
  }
end
is_success_response?(code) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 477
def is_success_response?(code)
  (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
end
mac256(key, data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 514
def mac256(key, data)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
end
merchant_data_xml(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 371
def merchant_data_xml(data)
  xml = Builder::XmlMarkup.new
  build_merchant_data(xml, data)
  xml.target!
end
parse(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 404
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 471
def response_text(code)
  code = code.to_i
  code = 0 if code < 100
  RESPONSE_TEXTS[code] || 'Unkown code, please check in manual'
end
sha256_authentication?() click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 490
def sha256_authentication?
  @options[:signature_algorithm] == 'sha256'
end
sign_request(xml_request_string, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 494
def sign_request(xml_request_string, order_id)
  key = encrypt(@options[:secret_key], order_id)
  Base64.strict_encode64(mac256(key, xml_request_string))
end
split_authorization(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 456
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 467
def transaction_code(type)
  SUPPORTED_TRANSACTIONS[type]
end
url() click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 294
def url
  test? ? test_url : live_url
end
validate_signature(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 432
def validate_signature(data)
  if sha256_authentication?
    sig = Base64.strict_encode64(mac256(get_key(data[:ds_order].to_s), xml_signed_fields(data)))
    sig.casecmp(data[:ds_signature].to_s).zero?
  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
xml_request_from(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 324
def xml_request_from(data)
  if sha256_authentication?
    build_sha256_xml_request(data)
  else
    build_sha1_xml_request(data)
  end
end
xml_signed_fields(data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys.rb, line 518
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