An example of integrating Robocassa with Rails

Recently, a friend asked if I had integration code with a cash desk for Rails, and it so happened that I had one. I shared and thought that maybe someone else might need this code, that's why I decided to create this topic here.
Controller
Let's start with the controller, here we need 4 methods:
- pay - this is the page where the "pay" button will be located
- result is a callback that calls the cash desk to report the result of the operation, which must either accept it by returning “OK” + “InvId”, or, if the results of the transaction did not suit us, return “FAIL”.
- success - the page to which the cash desk redirects the user, in case of successful payment
- fail - the page to which the ticket office redirects the user, in case something is wrong.
class PaymentsController < ApplicationController
def pay
# назодим заказ который надо оплатить, например так:
@order = Order.find(params[:id])
unless @order.blank? && @order.payment.blank?
# заполняем параметры формы
@pay_desc = Hash.new
@pay_desc['mrh_url'] = Payment::MERCHANT_URL
@pay_desc['mrh_login'] = Payment::MERCHANT_LOGIN
@pay_desc['mrh_pass1'] = Payment::MERCHANT_PASS_1
@pay_desc['inv_id'] = 0
@pay_desc['inv_desc'] = @order.payment.desc
@pay_desc['out_summ'] = @order.payment.price.to_s
@pay_desc['shp_item'] = @order.id
@pay_desc['in_curr'] = "WMRM"
@pay_desc['culture'] = "ru"
@pay_desc['encoding'] = "utf-8"
# расчет контрольной суммы
@pay_desc['crc'] = Payment.get_hash(@pay_desc['mrh_login'],
@pay_desc['out_summ'],
@pay_desc['inv_id'],
@pay_desc['mrh_pass1'],
"Shp_item=#{@pay_desc['shp_item']}")
end
end
def result
crc = Payment.get_hash(params['OutSum'],
params['InvId'],
Payment::MERCHANT_PASS_2,
"Shp_item=#{params['Shp_item']}")
@result = "FAIL"
begin
# проверяем контрольную сумму, чтобы нас не похекали
break if params['SignatureValue'].blank? || crc.casecmp(params['SignatureValue']) != 0
@order = Order.where(:id => params['Shp_item']).first
# ищем заказ
break if @order.blank? || @order.payment.price != params['OutSum'].to_f
# делаем с заказом то что нам нужно
@order.payment.invid = params['InvId'].to_i
@order.payment.status = Payment::STATUS_OK
@order.payment.save
# ...
# говорим робокассе, что все хорошо
@result = "OK#{params['InvId']}"
end while false
end
def success
# тут говорим пользователю что все ОК
end
def fail
# тут говорим, что есть проблемы...
end
end
Views
Form for the pay method:
Here it is worth paying attention to the fact that I do not use rails methods to generate forms, this is done with the intention that there is still little sense from them, but there may be problems, although I haven’t encountered so far. Rails, as a rule, still generates hidden fields for protection against CSRF, but since we send them to the cash desk, it is unlikely to be needed.
Cap, could not resist and decided to add a view to the result method here:
<%= @result %>
success and fail can be any.
Model
In this model, I use mondoid, but I think that the problem is not great to remake it under any other ORM.
Also in this model are methods for working with XML interfaces of robocash desk. At the time of writing the code, I did not find adequate SOAP clients under rails, so I scored and decided to write simply on Nokogiri.
require 'open-uri'
require 'digest/md5'
class Payment
include Mongoid::Document
include Mongoid::Timestamps
field :status, :type => Integer
field :invid, :type => Integer
field :price, :type => Float
field :desc
# ...
MERCHANT_URL = 'https://merchant.roboxchange.com/Index.aspx'
# test interface: http://test.robokassa.ru/Index.aspx
SERVICES_URL = 'https://merchant.roboxchange.com/WebService/Service.asmx'
#test interface: http://test.robokassa.ru/Webservice/Service.asmx
MERCHANT_LOGIN = 'login'
MERCHANT_PASS_1 = 'your_pass_1'
MERCHANT_PASS_2 = 'your_pass_2'
def self.get_currencies(lang = "ru")
svc_url = "#{SERVICES_URL}/GetCurrencies?MerchantLogin=#{MERCHANT_LOGIN}&Language=#{lang}"
doc = Nokogiri::XML(open(svc_url))
doc.xpath("//xmlns:Group").map {|g|{
'code' => g['Code'],
'desc' => g['Description'],
'items' => g.xpath('.//xmlns:Currency').map {|c| {
'label' => c['Label'],
'name' => c['Name']
}}
}} if doc.xpath("//xmlns:Result/xmlns:Code").text.to_i == 0
end
def self.get_payment_methods(lang = "ru")
svc_url = "#{SERVICES_URL}/GetPaymentMethods?MerchantLogin=#{MERCHANT_LOGIN}&Language=#{lang}"
doc = Nokogiri::XML(open(svc_url))
doc.xpath("//xmlns:Method").map {|g| {
'code' => g['Code'],
'desc' => g['Description']
}} if doc.xpath("//xmlns:Result/xmlns:Code").text.to_i == 0
end
def self.get_rates(sum = 1, curr = '', lang="ru")
svc_url = "#{SERVICES_URL}/GetRates?MerchantLogin=#{MERCHANT_LOGIN}&IncCurrLabel=#{curr}&OutSum=#{sum}&Language=#{lang}"
doc = Nokogiri::XML(open(svc_url))
doc.xpath("//xmlns:Group").map {|g| {
'code' => g['Code'],
'desc' => g['Description'],
'items' => g.xpath('.//xmlns:Currency').map {|c| {
'label' => c['Label'],
'name' => c['Name'],
'rate' => c.xpath('./xmlns:Rate')[0]['IncSum']
}}
}} if doc.xpath("//xmlns:Result/xmlns:Code").text.to_i == 0
end
def self.operation_state(id)
crc = get_hash(MERCHANT_LOGIN, id.to_s, MERCHANT_PASS_2)
svc_url = "#{SERVICES_URL}/OpState?MerchantLogin=#{MERCHANT_LOGIN}&InvoiceID=#{id}&Signature=#{crc}&StateCode=80"
doc = Nokogiri::XML(open(svc_url))
return nil unless doc.xpath("//xmlns:Result/xmlns:Code").text.to_i == 0
state_desc = {
1 => 'Информация об операции с таким InvoiceID не найдена',
5 => 'Только инициирована, деньги не получены',
10 => 'Деньги не были получены, операция отменена',
50 => 'Деньги получены, ожидание решение пользователя о платеже',
60 => 'Деньги после получения были возвращены пользователю',
80 => 'Исполнение операции приостановлено',
100 => 'Операция завершена успешно',
}
s = doc.xpath("//xmlns:State")[0]
code = s.xpath('./xmlns:Code').text.to_i
state = {
'code' => code,
'desc' => state_desc[code],
'request_date' => s.xpath('./xmlns:RequestDate').text,
'state_date' => s.xpath('./xmlns:StateDate').text
}
end
def self.get_hash(*s)
Digest::MD5.hexdigest(s.join(':'))
end
end
Router
And finally, you need to remember to register the routes, so that later you can tell them to the cash desk:
match 'payment/result' => "payments#result"
match 'payment/success' => "payments#success"
match 'payment/fail' => "payments#fail"
match 'payment' => "payments#pay"
Sources
All presented codes can be picked up by a hist here