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
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
orfalse
. Defaults tofalse
. (OPTIONAL) -
:signature_algorithm
– +“sha256”+ Defaults to +“sha1”+. (OPTIONAL)
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
# 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
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 260 def supports_scrubbing true end
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 281 def add_action(data, action) data[:action] = transaction_code(action) end
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 529 def get_key(order_id) encrypt(@options[:secret_key], order_id) end
# File lib/active_merchant/billing/gateways/redsys.rb, line 318 def headers { 'Content-Type' => 'application/x-www-form-urlencoded' } end
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 514 def mac256(key, data) OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, data) end
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 490 def sha256_authentication? @options[:signature_algorithm] == 'sha256' end
# 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
# File lib/active_merchant/billing/gateways/redsys.rb, line 467 def transaction_code(type) SUPPORTED_TRANSACTIONS[type] end
# File lib/active_merchant/billing/gateways/redsys.rb, line 294 def url test? ? test_url : live_url end
# 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
# 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
# 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