Back to Home

An example of integrating Robocassa with Rails

robokassa · rails on rails · ruby

An example of integrating Robocassa with Rails

    image
    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

    Read Next