Pull to refresh

Пример интеграции Робокассы с Rails

Ruby on Rails *
image
Недавно друг спрашивал, нет ли у меня кода интеграции с робокассой для 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


Все представленные коды можно забрать гистом тут
Tags:
Hubs:
Total votes 37: ↑27 and ↓10 +17
Views 8.3K
Comments Comments 31