lib/census/payments/redsys_integration.rb
# frozen_string_literal: true
module Census
module Payments
class RedsysIntegration < Rectify::Form
LIVE_URL = "https://sis.sermepa.es/sis/realizarPago"
TEST_URL = "https://sis-t.redsys.es:25443/sis/realizarPago"
SIGNATURE_VERSION = "HMAC_SHA256_V1"
CURRENCY_CODES_BACK = ActiveMerchant::Billing::RedsysGateway::CURRENCY_CODES.invert.freeze
LANGUAGES = { es: "001", en: "002", ca: "003", fr: "004", de: "005", nl: "006", it: "007", sv: "008",
pt: "009", pl: "011", gl: "012", eu: "013" }.freeze
attribute :merchant_name, String
attribute :merchant_code, String
attribute :terminal, String
attribute :secret_key, String
attribute :test, Boolean, default: true
attribute :transaction_type, String, default: "0"
attribute :notification_url, String
attribute :return_url, String
attribute :order_id, Integer
attribute :product_description, String
attribute :amount, Integer
attribute :currency, String
attribute :language, Symbol
attribute :response_code, String
attribute :document_literal_style, Boolean
validates :merchant_name, :merchant_code, :terminal, :secret_key, :test, :transaction_type, presence: true
validates :notification_url, :return_url, presence: true
validates :order_id, :amount, presence: true
def form
return nil if invalid?
{
action: test ? TEST_URL : LIVE_URL,
fields: {
Ds_MerchantParameters: params,
Ds_SignatureVersion: SIGNATURE_VERSION,
Ds_Signature: mac256(order_key, params)
}
}
end
def parse(response, date_span)
# By default use raw response as response code
self.response_code = response
response_parts = parse_response(response)
return nil unless response_parts
request = response_parts[:request]
self.order_unique_id = request["Ds_Order"]
self.amount = request["Ds_Amount"].to_i
self.currency_code = request["Ds_Currency"]
self.product_description = request["Ds_MerchantData"]
self.merchant_code = request["Ds_MerchantCode"]
self.terminal = request["Ds_Terminal"]
return nil unless valid_datetime?(request, date_span) && valid_signature?(response_parts[:message]["Signature"], response_parts[:raw_request])
self.response_code = request["Ds_Response"]
return { raw_response: request } unless success?
{
authorization_token: request["Ds_Merchant_Identifier"],
expiration_year: "20#{request["Ds_ExpiryDate"][0..1]}".to_i,
expiration_month: request["Ds_ExpiryDate"][2..3].to_i,
raw_response: request
}
end
def format_response(force_error)
return nil if response_code.blank? # only can be used to respond parsed responses
@success = false if force_error
envelope(response_message)
end
def success?
return nil unless response_code&.to_i
@success ||= response_code.to_i <= 100 || %w(0400 0481 0500 0900).include?(response_code)
end
private
def params
Base64.urlsafe_encode64(
JSON.generate(
DS_MERCHANT_AMOUNT: amount.to_s,
DS_MERCHANT_CONSUMERLANGUAGE: language_code,
DS_MERCHANT_CURRENCY: currency_code,
DS_MERCHANT_IDENTIFIER: "REQUIRED",
DS_MERCHANT_MERCHANTCODE: merchant_code,
DS_MERCHANT_MERCHANTNAME: merchant_name,
DS_MERCHANT_MERCHANTURL: notification_url,
DS_MERCHANT_ORDER: order_unique_id,
DS_MERCHANT_PRODUCTDESCRIPTION: product_description,
DS_MERCHANT_MERCHANTDATA: product_description,
DS_MERCHANT_TERMINAL: terminal,
DS_MERCHANT_TRANSACTIONTYPE: transaction_type,
DS_MERCHANT_URLKO: url_ko,
DS_MERCHANT_URLOK: url_ok
)
)
end
def parse_response(response)
envelope = Hash.from_xml(response)
envelope = envelope.dig("Envelope", "Body") if envelope.present?
return nil unless envelope
self.document_literal_style = envelope["notificacion"].present?
raw_message = document_literal_style ? envelope.dig("notificacion", "datoEntrada") : envelope.dig("procesaNotificacionSIS", "XML")
return nil unless raw_message
message = Hash.from_xml(raw_message)
{
message: message["Message"],
raw_request: raw_message.match("<Request(.*)</Request>").to_s,
request: message.dig("Message", "Request")
}
rescue REXML::ParseException
nil
end
def response_message
xml = Builder::XmlMarkup.new
response = xml.Response Ds_Version: "0.0" do
xml.Ds_Response_Merchant(success? ? "OK" : "KO")
end
signature = mac256(order_key, response)
xml = Builder::XmlMarkup.new
xml.Message do
xml.Response Ds_Version: "0.0" do
xml.Ds_Response_Merchant(success? ? "OK" : "KO")
end
xml.Signature(signature)
end
end
def envelope(message)
xml = Builder::XmlMarkup.new
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
xml.tag! "SOAP-ENV:Envelope", "xmlns:SOAP-ENV" => "http://schemas.xmlsoap.org/soap/envelope/",
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema" do
xml.tag! "SOAP-ENV:Body" do
if document_literal_style
xml.notificacionResponse xmlns: "http://notificador.webservice.sis.redsys.es" do
xml.notificacionReturn message, xmlns: "http://notificador.webservice.sis.redsys.es"
end
else
xml.ns1 :procesaNotificacionSIS, "xmlns:ns1" => "InotificacionSIS", "SOAP-ENV:encodingStyle" => "http://schemas.xmlsoap.org/soap/encoding/" do
xml.return message, "xsi:type" => "xsd:string"
end
end
end
end
end
def currency_code
ActiveMerchant::Billing::RedsysGateway::CURRENCY_CODES[currency&.upcase] ||
ActiveMerchant::Billing::RedsysGateway::CURRENCY_CODES["EUR"]
end
def currency_code=(value)
self.currency = CURRENCY_CODES_BACK[value]
end
def language_code
LANGUAGES[language&.downcase] || LANGUAGES[:es]
end
def order_unique_id
@order_unique_id ||= SecureRandom.random_number(10_000).to_s + order_id.to_s(36).rjust(8, "0")
end
def order_unique_id=(value)
@order_unique_id = value
@order_id = value[4..11].to_i(36)
end
def url_ok
@url_ok ||= return_url.sub("__RESULT__", "ok")
end
def url_ko
@url_ko ||= return_url.sub("__RESULT__", "ko")
end
def valid_datetime?(response, date_span)
Time.zone.parse("#{response["Fecha"]} #{response["Hora"]} #{Time.current.zone}").between? date_span.ago, date_span.from_now
end
def valid_signature?(signature, response_data)
signature == mac256(order_key, response_data)
end
def order_key
encrypt(Base64.strict_decode64(secret_key), order_unique_id)
end
def mac256(key, data)
Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", key, data))
end
def encrypt(key, data)
cipher = OpenSSL::Cipher.new("DES3")
cipher.encrypt
cipher.key = key
cipher.padding = 0
data_copy = data
data_copy += "\0" until (data_copy.bytesize % 8).zero? # Pad with zeros
cipher.update(data_copy) + cipher.final
end
end
end
end