class ActiveMerchant::Billing::RedsysRestGateway

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.

Written by Piers Chambers (Varyonic.com)

*** SHA256 Authentication Update ***

Redsys has dropped support for the SHA1 authentication method. Developer documentation: pagosonline.redsys.es/desarrolladores.html

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.

THREEDS_EXEMPTIONS
THREE_DS_V2

Expected values as per documentation

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_rest.rb, line 185
def initialize(options = {})
  requires!(options, :login, :secret_key)
  options[:terminal] ||= 1
  options[:signature_algorithm] = 'sha256'
  super
end

Public Instance Methods

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

  post = {}
  add_action(post, :authorize, options)
  add_amount(post, money, options)
  add_stored_credentials(post, options)
  add_threeds_exemption_data(post, options)
  add_order(post, options[:order_id])
  add_payment(post, payment)
  add_description(post, options)
  add_direct_payment(post, options)
  add_threeds(post, options)

  commit(post, options)
end
capture(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 226
def capture(money, authorization, options = {})
  post = {}
  add_action(post, :capture)
  add_amount(post, money, options)
  order_id, = split_authorization(authorization)
  add_order(post, order_id)
  add_description(post, options)

  commit(post, options)
end
purchase(money, payment, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 192
def purchase(money, payment, options = {})
  requires!(options, :order_id)

  post = {}
  add_action(post, :purchase, options)
  add_amount(post, money, options)
  add_stored_credentials(post, options)
  add_threeds_exemption_data(post, options)
  add_order(post, options[:order_id])
  add_payment(post, payment)
  add_description(post, options)
  add_direct_payment(post, options)
  add_threeds(post, options)

  commit(post, options)
end
refund(money, authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 250
def refund(money, authorization, options = {})
  requires!(options, :order_id)

  post = {}
  add_action(post, :refund)
  add_amount(post, money, options)
  order_id, = split_authorization(authorization)
  add_order(post, order_id)
  add_description(post, options)

  commit(post, options)
end
scrub(transcript) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 282
def scrub(transcript)
  transcript.
    gsub(%r((PAN\"=>\")(\d+)), '\1[FILTERED]').
    gsub(%r((CVV2\"=>\")(\d+)), '\1[FILTERED]')
end
supports_scrubbing?() click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 278
def supports_scrubbing?
  true
end
verify(creditcard, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 263
def verify(creditcard, options = {})
  requires!(options, :order_id)

  post = {}
  add_action(post, :verify, options)
  add_amount(post, 0, options)
  add_order(post, options[:order_id])
  add_payment(post, creditcard)
  add_description(post, options)
  add_direct_payment(post, options)
  add_threeds(post, options)

  commit(post, options)
end
void(authorization, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 237
def void(authorization, options = {})
  requires!(options, :order_id)

  post = {}
  add_action(post, :cancel)
  order_id, amount, currency = split_authorization(authorization)
  add_amount(post, amount, currency: currency)
  add_order(post, order_id)
  add_description(post, options)

  commit(post, options)
end

Private Instance Methods

add_action(post, action, options = {}) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 324
def add_action(post, action, options = {})
  post[:DS_MERCHANT_TRANSACTIONTYPE] = transaction_code(action)
end
add_amount(post, money, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 328
def add_amount(post, money, options)
  post[:DS_MERCHANT_AMOUNT] = amount(money).to_s
  post[:DS_MERCHANT_CURRENCY] = currency_code(options[:currency] || currency(money))
end
add_authentication(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 389
def add_authentication(post, options)
  post[:DS_MERCHANT_TERMINAL] = options[:terminal] || @options[:terminal]
  post[:DS_MERCHANT_MERCHANTCODE] = @options[:login]
end
add_browser_info(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 308
def add_browser_info(post, options)
  return unless browser_info = options.dig(:three_ds_2, :browser_info)

  {
    browserAcceptHeader: browser_info[:accept_header],
    browserUserAgent: browser_info[:user_agent],
    browserJavaEnabled: browser_info[:java],
    browserJavascriptEnabled: browser_info[:java],
    browserLanguage: browser_info[:language],
    browserColorDepth: browser_info[:depth],
    browserScreenHeight: browser_info[:height],
    browserScreenWidth: browser_info[:width],
    browserTZ: browser_info[:timezone]
  }
end
add_description(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 333
def add_description(post, options)
  post[:DS_MERCHANT_PRODUCTDESCRIPTION] = CGI.escape(options[:description]) if options[:description]
end
add_direct_payment(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 290
def add_direct_payment(post, options)
  # Direct payment skips 3DS authentication. We should only apply this if execute_threed is false
  # or authentication data is not present. Authentication data support to be added in the future.
  return if options[:execute_threed] || options[:authentication_data] || options[:three_ds_exemption_type] == 'moto'

  post[:DS_MERCHANT_DIRECTPAYMENT] = true
end
add_order(post, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 337
def add_order(post, order_id)
  post[:DS_MERCHANT_ORDER] = clean_order_id(order_id)
end
add_payment(post, card) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 341
def add_payment(post, card)
  name = [card.first_name, card.last_name].join(' ').slice(0, 60)
  year = sprintf('%.4i', card.year)
  month = sprintf('%.2i', card.month)
  post['DS_MERCHANT_TITULAR'] = CGI.escape(name)
  post['DS_MERCHANT_PAN'] = card.number
  post['DS_MERCHANT_EXPIRYDATE'] = "#{year[2..3]}#{month}"
  post['DS_MERCHANT_CVV2'] = card.verification_value if card.verification_value.present?
end
add_stored_credentials(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 394
def add_stored_credentials(post, options)
  return unless stored_credential = options[:stored_credential]

  post[:DS_MERCHANT_COF_INI] = stored_credential[:initial_transaction] ? 'S' : 'N'

  post[:DS_MERCHANT_COF_TYPE] = case stored_credential[:reason_type]
                                when 'recurring'
                                  'R'
                                when 'installment'
                                  'I'
                                else
                                  'C'
                                end

  post[:DS_MERCHANT_IDENTIFIER] = 'REQUIRED' if stored_credential[:initiator] == 'cardholder'
  post[:DS_MERCHANT_COF_TXNID] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
end
add_threeds(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 298
def add_threeds(post, options)
  return unless options[:execute_threed] || options[:three_ds_2]

  post[:DS_MERCHANT_EMV3DS] = if options[:execute_threed]
                                { threeDSInfo: 'CardData' }
                              else
                                add_browser_info(post, options)
                              end
end
add_threeds_exemption_data(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 412
def add_threeds_exemption_data(post, options)
  return unless options[:three_ds_exemption_type]

  if options[:three_ds_exemption_type] == 'moto'
    post[:DS_MERCHANT_DIRECTPAYMENT] = 'MOTO'
  else
    exemption = options[:three_ds_exemption_type].to_sym
    post[:DS_MERCHANT_EXCEP_SCA] = THREEDS_EXEMPTIONS[exemption]
  end
end
authorization_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 451
def authorization_from(response)
  # Need to get updated for 3DS support
  [response[:ds_order], response[:ds_amount], response[:ds_currency]].join('|')
end
clean_order_id(order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 472
def clean_order_id(order_id)
  cleansed = order_id.gsub(/[^\da-zA-Z]/, '')
  if /^\d{4}/.match?(cleansed)
    cleansed[0..11]
  else
    '%04d' % [rand(0..9999)] + cleansed[0...8]
  end
end
commit(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 357
def commit(post, options)
  url = (test? ? test_url : live_url)
  action = determine_action(options)
  raw_response = parse(ssl_post(url + action, post_data(post, options)))
  payload = raw_response['Ds_MerchantParameters']
  return Response.new(false, "#{raw_response['errorCode']} ERROR") unless payload

  response = JSON.parse(Base64.decode64(payload)).transform_keys!(&:downcase).with_indifferent_access
  return Response.new(false, 'Unable to verify response') unless validate_signature(payload, raw_response['Ds_Signature'], response[:ds_order])

  succeeded = success_from(response, options)
  Response.new(
    succeeded,
    message_from(response),
    response,
    authorization: authorization_from(response),
    test: test?,
    error_code: succeeded ? nil : response[:ds_response]
  )
end
currency_code(currency) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.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
determine_action(options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 351
def determine_action(options)
  # If execute_threed is true, we need to use iniciaPeticionREST to set up authentication
  # Otherwise we are skipping 3DS or we should have 3DS authentication results
  options[:execute_threed] ? 'iniciaPeticionREST' : 'trataPeticionREST'
end
encrypt(key, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 488
def encrypt(key, order_id)
  block_length = 8
  cipher = OpenSSL::Cipher.new('DES3')
  cipher.encrypt

  cipher.key = Base64.urlsafe_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

  cipher.update(order_id) + cipher.final
end
mac256(key, data) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 502
def mac256(key, data)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data)
end
message_from(response) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 438
def message_from(response)
  return response.dig(:ds_emv3ds, :threeDSInfo) if response[:ds_emv3ds]

  code = response[:ds_response]&.to_i
  code = 0 if code < 100
  RESPONSE_TEXTS[code] || 'Unknown code, please check in manual'
end
parse(body) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 423
def parse(body)
  JSON.parse(body)
end
post_data(post, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 378
def post_data(post, options)
  add_authentication(post, options)
  merchant_parameters = JSON.generate(post)
  encoded_parameters = Base64.strict_encode64(merchant_parameters)
  post_data = PostData.new
  post_data['Ds_SignatureVersion'] = 'HMAC_SHA256_V1'
  post_data['Ds_MerchantParameters'] = encoded_parameters
  post_data['Ds_Signature'] = sign_request(encoded_parameters, post[:DS_MERCHANT_ORDER])
  post_data.to_post_data
end
sign_request(encoded_parameters, order_id) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 481
def sign_request(encoded_parameters, order_id)
  raise(ArgumentError, 'missing order_id') unless order_id

  key = encrypt(@options[:secret_key], order_id)
  Base64.strict_encode64(mac256(key, encoded_parameters))
end
split_authorization(authorization) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 456
def split_authorization(authorization)
  order_id, amount, currency = authorization.split('|')
  [order_id, amount.to_i, currency]
end
success_from(response, options) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 427
def success_from(response, options)
  return true if response[:ds_emv3ds] && options[:execute_threed]

  # Need to get updated for 3DS support
  if code = response[:ds_response]
    (code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
  else
    false
  end
end
transaction_code(type) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 468
def transaction_code(type)
  SUPPORTED_TRANSACTIONS[type]
end
validate_signature(data, signature, order_number) click to toggle source
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 446
def validate_signature(data, signature, order_number)
  key = encrypt(@options[:secret_key], order_number)
  Base64.urlsafe_encode64(mac256(key, data)) == signature
end