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
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)
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
# 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
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 278 def supports_scrubbing? true end
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 423 def parse(body) JSON.parse(body) end
# 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
# 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
# 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
# File lib/active_merchant/billing/gateways/redsys_rest.rb, line 468 def transaction_code(type) SUPPORTED_TRANSACTIONS[type] end
# 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