
Недавно друг спрашивал, нет ли у меня кода интеграции с робокассой для Rails, и так сложилось, что он у меня был. Я поделился и подумал, что, возможно, этот код может понадобится кому-либо еще, потому и решил создать этот топик здесь.
Controller
Начнем с контроллера, тут нам понадобятся 4 метода:
- pay — это страница, где будет располагаться кнопка «оплатить»
- result — это колбэк, который вызывает робокасса чтобы сообщить о результате операции, который должен либо принять его, вернув «OK» + «InvId», либо, если результаты трансакции нас не устроили, вернуть «FAIL».
- success — страница, на которую робокасса редиректит пользователя, в случае успешной оплаты
- fail — страница на которую робокасса редиректит пользователя, в случае если что-то не так.
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
Форма для метода pay:
<form action="<%= @pay_desc['mrh_url'] %>" method="post"> <input type=hidden name=MrchLogin value="<%= @pay_desc['mrh_login'] %>"> <input type=hidden name=OutSum value="<%= @pay_desc['out_summ'] %>"> <input type=hidden name=InvId value="<%= @pay_desc['inv_id'] %>"> <input type=hidden name=Desc value="<%= @pay_desc['inv_desc'] %>"> <input type=hidden name=SignatureValue value="<%= @pay_desc['crc'] %>"> <input type=hidden name=Shp_item value="<%= @pay_desc['shp_item'] %>"> <input type=hidden name=IncCurrLabel value="<%= @pay_desc['in_curr'] %>"> <input type=hidden name=Culture value="<%= @pay_desc['culture'] %>"> <input type=submit value='Оплатить'> </form>
Тут стоит обратить внимание на то, что я не использую методов rails для генерации форм, сделано это с тем умыслом, что толку от них здесь все равно не много, а проблемы могут быть, хотя я в общем-то пока не сталкивался. Rails, как правило, еще генерирует hidden поля для защиты от CSRF, но поскольку мы шлем их в робокассу, то ей они вряд ли понадобятся.
Кэп, не удержался и решил добавить сюда вью для метода result:
<%= @result %>
success и fail могут быть любыми.
Model
В данной модели я использую mondoid, но думаю, что не велика проблема переделать его под любой другой ORM.
Также в этой модели представлены методы для работы с XML-интерфейсами робокассы. На момент написания кода я не нашел адекватных SOAP клиентов под rails, поэтому забил и решил написать просто на 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
Ну и напоследок, нужно не забыть прописать роуты, чтобы потом сообщить их робокассе:
match 'payment/result' => "payments#result" match 'payment/success' => "payments#success" match 'payment/fail' => "payments#fail" match 'payment' => "payments#pay"
Sources
Все представленные коды можно забрать гистом тут
