Интеграция Деньги Online в ActiveMerchant

в 13:06, , рубрики: open source, ruby, ruby on rails

В приложении, которое я разрабатываю на Ruby on Rails, мне нужно было подключить платежную систему. Заказчик заключил договор с Деньги Online, и первым делом я, конечно, проверил список поддерживаемых систем в ActiveMerchant от Shopify — там этого сервиса не оказалось, также еще поискал готовые решения, которые смогли бы упростить интеграцию, но ничего полезного под RoR не нашлось. В итоге было решено форкнуть ActiveMerchant и разработать под него интеграцию для этого сервиса, а в последствии использовать наработки в проекте.

В этой статье хочу рассказать о процессе подключения платежной системы, в надежде что кому-нибудь это пригодится, потому что мне не хватало подобной информации. А также возможно кому-нибудь пригодится информация, конкретно по этой платежной системе.

До этого у меня уже было знакомство с ActiveMerchant: в одном из проектов интегрировал Robokassa —, тогда во много помогла эта статья, и ни с какими проблемами я не столкнулся, но в код реализации модуля пришлось заглянуть лишь несколько раз, что бы проверить или понять работу каких-то конкретных методов — на этом все ограничилось. А при интеграции своего модуля мне пришлось изучить достаточно большую часть ActiveMerchant.

Структура ActiveMerchant

Для помощи в интеграции подобных сервисов, в проекте существует 3 базовых класса:

  • Helper — на основе него происходит построение формы при помощи метода payment_service_for, этот метод сам генерирует форму, со всеми необходимыми параметрами, разработчику остается лишь правильно создать Helper, для конкретной платежной системы
  • Notification — в данном классе содержатся методы для проверки валидности информации пришедшей от сервера платежной системы, например при подтверждении завершения операции
  • Return — используется для обработки колбеков, но зачастую удобнее работать со встроенными в рельсы хелперами, например для обработки пришедших параметров, чем с этим классом. И в реализациях этого класса в различных системах обычно нет каких-то специфичных функций.

Каждая реализация платежной системы содержит несколько классов унаследованных от базовых, в некоторых модулях имеются дополнительные классы, реализующие дополнительные возможности протокола. Так же для каждой платежной системы в ActiveMerchant есть модуль, в котором происходит загрузка классов из файлов, и в которых содержатся методы для создания конкретного класса, включенного в этот модуль.

Интеграция Деньги Online в ActiveMerchant

Решение этой задачи началось с изучения протокола Деньги Online, и там все оказалось не так прозрачно как в случае с Robokassa: фоновая проверка правильности информации, подтверждение и колбеки, по сути все просто, но в большей степени меня смущали параметры передаваемые при запросах и куча различных оговорок.

image
Общая схема запросов

Вкратце: в общем случае по позитивному сценарию все происходит так: сайт отправляет системе форму со всеми необходимыми параметрами, при этом пользователь переходит на сайт платежной системы, далее платежная система в фоне запрашивает подтверждение правильности данных у нашего сайта. Если данные верны, то пользователь будет перенаправлен туда, где увидит необходимые инструкции для совержения платежа. После нажатия кнопки «Оплатить», платежная система перед принятием платежа, снова проверит правильность данных, потом платеж будет принят и система отправит на сайт запрос о подтверждении оплаты, и в случае положительного ответа, пользователь будет перенаправлен на колбек.

Основные особенности протокола, которые меня смутили:

  • При выставлении счета отправляются одни параметры, при проверках эти параметры возвращаются уже под другими именами
  • Изначально в протоколе предполагается, что проверка осуществляется только по идентификатору пользователя, сделавшего заказ, а зачастую гораздо удобнее проверять данные по внутреннему идентификатору платежа
  • Не смотря на то, что можно передать системе внутренний идентификатор платежа, при проверке и в некоторых случаях при подтверждении он не будет возвращаться, и является необязательным параметром
  • В колбеки вообще не передаются какие-либо параметры
  • При выставлении счета есть много дополнительных возможностей обеспечения удобства для пользователей, но все эти возможности в протоколе реализованы по разному

Из-за этих особенностей мне пришлось несколько раз написать в их поддержку (хотя сначала я связался с ними через модуль голосовой связи на сайте, но девушка выслушав мою проблему, перенаправила меня на автоответчик), и в течение этого же дня получал адекватные ответы.

Из плюсов заметил, что если сделать запрос точно по описанному протоколу, то в ответ получишь ожидаемый результат, хотя в некоторых случаях проверка данных не осуществляется, но видимо из-за особенностей шлюзов.

Также мне понравилась возможность в фоновом режиме выписать счет в некоторых случаях. И я решил сделать максимально полную поддержку протокола выставления счета, но у меня не было возможности протестить абсолютно все, так что возможно могут наблюдаться какие-то проблемы.

Еще удалось выявить одну недокументированную возможность: в ответ на фоновый запрос создания счета для оплаты через QIWI, приходит вместе с описанными параметрами, параметр iframeSRC с непонятным содержанием, недолго думая, декодировал данные из этого параметр при помощи base64 и получил ссылку, которая ведет на форму оплаты через QIWI, и видимо предполагается, что ссылку следует использовать для создания iframe.

Если посмотреть на различные реализации интеграций платежных систем в ActiveMerchant, то можно увидеть как один и тот же код может перетекать во множество модулей, в каких-то файлах кодом может стать объединение кода из нескольких модулей и так далее, но нигде нет каких-то сложных сценариев. Мне пришлось написать много своего, в частности для реализации фоновых запросов, поэтому боюсь что пул реквест не одобрят, хотя по сути все возможности и идеи, что несут в себе базовые классы в своей реализации я сохранил, просто немного добавил новых.

Что получилось в итоге можно посмотреть здесь. Единственное что у меня выходит из рамок общей концепции для хелпера, так это валидация данных, но в общем случае она совершенно не нужна, ей стоит пользоваться только при фоновых запросах, что бы избежать возможных ошибок еще перед запросом (не смотря на это в моем случае при реализации системы оплаты на сайте, мне пришлось грязно-хаково обойти это), и так же автоматически выставляются некоторые параметры запросов в особых случаях, что бы разработчику не пришлось долго разбираться с протоколом. Еще у моего хелпера есть возможность отправить фоновый запрос, если это делается для валидных mode_type, то ответ автоматически спарсится и с этим ответом можно будет удобно работать.

Не могу сказать что код получился хорошим, потому что с руби я знаком от силы месяца три, поэтому буду рад любой помощи или советам, также сам готов помочь тем, кто захочет интегрировать какую-либо платежную систему в ActiveMerchant.

Пример создания системы оплаты на сайте

Первым делом необходимо включить ActiveMerchant в проект, для этого добавляем его в Gemfile

gem 'activemerchant', :git => "https://github.com/ovcharik/active_merchant.git", :branch => "dengionline"

Пока можно добавлять так, но я отправил пул реквест в официальный репозиторий, и возможно он скоро будет одобрен.

Далее создаем необходимые роутеры

scope 'top_up' do
  post 'create' => 'top_up#create', :as => 'top_up_create'
  
  post 'notify' => 'top_up#notify', :as => 'top_up_notify'
  post 'check' => 'top_up#check', :as => 'top_up_check'

  match 'success' => 'top_up#success', :as => 'top_up_success'
  match 'fail' => 'top_up#fail', :as => 'top_up_fail'
end

  • create используется для создания объекта платежа и выписки счета, в принципе он не обязателен или его логику можно разместить в другом месте, но мне было удобнее сделать именно так.
  • success и fail — это колбеки, на которые пользователь будет перенаправлен после завершения транзакции, кстати именно в эти колбеки от системы не возвращаются какие-либо параметры.
  • notify и check нужны для подтверждения оплаты и проверки корректности данных системой оплаты на сайте соответсвенно, по этим адресам система оплаты будет совершать push-запросы, с определенными параметрами.

Последние 4 адреса, указываются платежной системе при регистрации в ней: в процессе необходимо будет заполнить анкету, в которой есть пункты по этим адресам. Сам сайт не предоставляет возможности изменения адресов, а делается это только через дополнительные запросы в поддержку, поэтому мне пришлось многое тестировать прямо в продакшене (не смотря на то, что проект запущен он пока еще в стадии разработки и для пользователей это никак не сказалось).

В проекте который я делаю, происходит такой сценарий при оплате услуг: пользователь нажав кнопку «Оплатить» посылает ajax-запрос на /top_up/create, там проверяется правильность данных, потом отправляется фоновый запрос в платежную систему, успешный ответ выводится пользователю, ошибки обрабатываются отдельно. В моем случае, в качестве успешного ответа, приходит форма со скрытыми полями и сразу после нее скрипт который выполнят отправку этой формы, то есть если вывести этот ответ где-то на странице, то пользователь будет неявно перенаправлен на страницу шлюза, где он может завершить оплату.

Во время фонового запроса происходит следующее: я делаю фоновый запрос, система проверяет все данные и если они верны то она обращается к методу /top_up/check, где должна получить положительный ответ, после проверки система возвращает необходимые данные.

На сайте шлюза пользователь видит форму, где ему необходимо указать реквизиты, и после нажатия кнопки «Оплатить», шлюз проверяет данные в системе Деньги Online, которая в свою очередь проверяет данные в проекте (опять по адресу /top_up/check), получает ответ и передает его платежному шлюзу. Он подтверждает платеж и уведомляет об этом платежную систему, а она уже отправляет на сайт уведомление (/top_up/notify), и в это же время пользователя возвращают на колбек.

Возможно проще понять этот процесс просто увидев логи

Started POST "/ru/top_up/create" for user_ip at 2013-10-05 14:45:00 +0400
Processing by TopUpController#create as */*
  Parameters: {"amount"=>"1.0", "authenticity_token"=>"token", "currency"=>"RUB", "order"=>"78", "lang"=>"ru"}
  
    Started POST "/top_up/check" for deingionline_ip at 2013-10-05 13:45:00 +0400
    Processing by TopUpController#check as */*
      Parameters: {"amount"=>"0", "userid"=>"example@mail.com", "userid_extra"=>"58", "paymentid"=>"0", "key"=>"key1"}
      Rendered text template (0.0ms)
    Completed 200 OK in 16ms (Views: 1.1ms | ActiveRecord: 2.5ms)
  
Completed 200 OK in 557ms (Views: 0.5ms | ActiveRecord: 11.1ms)

Started POST "/top_up/check" for deingionline_ip at 2013-10-05 14:46:20 +0400
Processing by TopUpController#check as */*
  Parameters: {"amount"=>"0", "userid"=>"example@mail.com", "userid_extra"=>"58", "paymentid"=>"0", "key"=>"key1"}
  Rendered text template (0.0ms)
Completed 200 OK in 19ms (Views: 1.0ms | ActiveRecord: 1.9ms)

Started POST "/top_up/notify" for deingionline_ip at 2013-10-05 14:46:20 +0400
Processing by TopUpController#notify as */*
  Parameters: {"amount"=>"1.00", "userid"=>"example@mail.com", "userid_extra"=>"58", "paymentid"=>"123456789", "paymode"=>"mode_type", "orderid"=>"58", "key"=>"key2"}
  Rendered text template (0.0ms)
Completed 200 OK in 127ms (Views: 0.9ms | ActiveRecord: 15.2ms)

Started GET "/top_up/success" for user_ip at 2013-10-05 14:46:30 +0400
Processing by TopUpController#success as HTML
  Rendered ...
Completed 200 OK in 21ms (Views: 16.6ms | ActiveRecord: 0.7ms)

В логах можно увидеть какие параметры передаются во всех случаях, и что при обращении к /top_up/check платежная система не передает orderid (внутренний идентификатор платежа в проекте, paymentid — идентификатор в платежной системе), поэтому для идентификации платежа было решено использовать параметр userid_extra, но здесь тоже нужно быть осторожным, так как в некоторых случаях он не возвращается, можно указывать внутренний номер платежа в userid, но в личном кабинете в платежной системы, есть возможность посмотреть все транзакции, а так же осуществить выборку по этому параметру, и если использовать уникальный идентификатор в качестве это параметра, то полезность возможности выборки будет нулевой.

Контроллер

class TopUpController < ApplicationController
  
  include ActiveMerchant::Billing::Integrations
  
  # аутенфикация пользователя для create
  skip_before_filter :verify_authenticity_token, :except => [:create]
  
  # создание нотификатора вкюченного в ActiveMerchant
  before_filter :create_notification, :only => [:check, :notify]
  # поиск объекта платежа
  before_filter :find_payment, :only => [:check, :notify]
  
  # создание платежа
  def create
    authorize! :create, Payment
    
    r = {
      :success => false,
      :errors => {:base => []}
    }
    errors = false
    
    # создаем объект модели платежа, эта модель используется только внутри
    # и к activemerchant никакого отношения не имеет
    @payment = Payment.new({
      :user => current_user,
      :amount => params[:amount]
    })
    
    # валидация данных
    unless errors or @payment.save
      r[:errors].merge! @payment.errors.messages
      errors = true
    end
    
    unless errors
      # формируем хелпер из ActiveMerchant
      helper=Dengionline.helper @payment.id, CONFIG["dengionline"]["project"], {
        :amount           => @payment.amount,
        :nickname         => @payment.user.email,
        # идентификатор внутреннего объекта платежа
        :nick_extra       => @payment.id,
        
        # в проекте используется только одно значение этого параметра
        :transaction_type => CONFIG["dengionline"]["mode_type"],
        # нужен что бы пользователь в последствии был редирекчен на колбеки
        :source           => CONFIG["dengionline"]["source"],
        # секретный код, задается при регистрации в платежной системе
        :secret           => CONFIG["dengionline"]["secret"],
        
        # указывам метод и режим, что бы хелпер правильно провалидировал данные,
        :method           => :credit_card,
        :mode             => :background
      }
      
      begin
        # здесь применен грязный хак, так как этот mode_type не поддерживает
        # фоновые запросы, но в случае ошибки, пользователь вместо каких-то
        # разнеснений почему так и что делать, увидит лишь чистый xml,
        # что не очень дружелюбно
        
        # смотрим, если единственная ошибка при валидации это mode_type
        # тогда можно считать что данные верны
        if (helper.valid?
          or helper.errors.size > 1
          or helper.errors[0] != "mode_type")
          
          r[:errors][:base] << I18n.t("views.top_up.create.technical_error")
          break
        end
        
        # совершаем фоновый запрос, восклицательный знак вконце стоит
        # потому что предположительно данные не валидны
        response = helper.background_request!
        
        # если не возникло ошибок у парсера, значит вернулся xml
        # это означает ошибку
        if response.errors.empty? and response.fail?
          r[:errors][:base] << response.comment
          break
        end
        
        # если ошибка не parse_error - это означает, что запрос не прошел
        # и вернулся код отличный от 200
        if (response.success?
          or response.errors.empty?
          or response.errors.size > 1
          or response.errors[0] != "parse_error")
          r[:errors][:base] << I18n.t("views.top_up.create.gateway_error")
          break
        end
        
        # если ошибка parse_error, значит либо все прошло успешно
        # либо поменялся протокол у платежной системы, но в любом случае
        # пользователю будет выведенно то, что было получено
        r[:success] = true
        r[:body] = response.body
      end until true
    end
    
    render :json => r
  end
  
  # проверка данных
  def check
    if (@notification.acknowledged?
      and @payment
      and @payment.wait?
      and @payment.valid?)
      render :text => @notification.generate_response("YES")
    else
      render :text => @notification.generate_response("NO")
    end
  end
  
  # подтверждение платежа
  def notify
    if (@notification.acknowledged?
      and @payment
      and @payment.amount == @notification.amount
      and @payment.amount > 0)
      
      # этот запрос может посылаться несколько раз
      # и нужно возвращать актуальный статус
      # в моем случае у платежа может не пройти валидация и что бы отдать
      # правильный ответ необходим этот флаг
      saved = true
      
      if @payment.wait?
        @payment.do_payment_id = @notification.payment_id
        @payment.paid!
        saved = @payment.save
        Notifier.payment_paid(@payment).deliver if saved
      end
      
      if @payment.paid? and saved
        render :text => @notification.generate_response("YES")
      else
        render :text => @notification.generate_response("NO")
      end
      
    else
      render :text => @notification.generate_response("NO")
    end
  end
  
  # в колбеках просто рендерятся страницы
  def success
  end
  
  def fail
  end
  
  private
    
    def create_notification
      @notification = Dengionline.notification(request.raw_post, {
        :secret => CONFIG["dengionline"]["secret"]
      })
    end
    
    def find_payment
      # ищу так, потому что удобнее обработать nil, чем исключение
      @payment = Payment.where(:id => @notification.nick_extra).first
    end
   
end

У меня все данные посылаются в фоне за-за особенностей ответов, но можно и использовать способ описанный здесь.

Еще приведу пример использования класса для получения удаленной информации о платеже, думаю ни у кого проблем с применением этого не возникнет.

Пример

$ rails c

irb(main):001:0> Dengionline = ActiveMerchant::Billing::Integrations::Dengionline
=> ActiveMerchant::Billing::Integrations::Dengionline

irb(main):002:0> status = Dengionline.status 69, 1234, :secret => "secret"
=> #<ActiveMerchant::Billing::Integrations::Dengionline::Status:0xb805bb4>

irb(main):003:0> status.to_hash 
=> {
  "id"=>123456789,
  "amount_rub"=>"1.00",
  "status"=>9,
  "status_description"=>"Success",
  "order"=>"69",
  "nick"=>"example@mail.com",
  "date_payment"=>"2013-10-05T13:00:00+04:00",
  "paymode"=>"mode_type",
  "currency_project"=>"RUB",
  "amount_project"=>"1.00",
  "currency_paymode"=>"RUB"
}

В данном примере в качестве основного параметра передается внутренний идентификатор платежа, если вы его не использовали при выписке счета, то можно передавать идентификатор платежа платежной системы, для этого последним параметром укажите :payment => payment_id.

Автор: movl

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js