Привет, на связи backend-отдел Joy Dev. В этой статье расскажем про оплаты через платежные ссылки и, в частности, как мы применили для этого отечественный сервис “Prodamus” в наших проектах на Ruby On Rails, и поделимся с вами созданным нами инструментом для облегчения интеграции с данным сервисом.

Платежные ссылки

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

В таком случае весь процесс оплаты обобщенно происходит всего лишь в два этапа:

  1. Получение ссылки на оплату происходит POST или GET запросом на сервер сервиса, в зависимости от их реализации механизма (см. документацию сервиса).

  2. Ожидание и получение результата может быть реализовано посредством запроса со стороны сервиса на ваш сервер, либо, при определенных условиях, делая запрос на сервис, получая статус покупки. Можно использовать оба этих способа одним механизмом, что является лучшим решением, чем по отдельности, но не все сервисы славятся таким широким функционалом.

Запрос к серверу на получение ссылки, ожидание оплаты и получение веб-хука с результатом для дальнейшей регистрацией покупки - подобным образом были сделаны оплаты через сервис “Prodamus” на наших проектах.

Звучит работа с платежными ссылками достаточно просто, проще аналогов, но всегда есть над чем посидеть пару ночей. Особенно когда случается интеграция с малоизвестным сервисом.

Как использовать для этого Продамус

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

“Платежная страница” в нашем случае это личный кабинет, в котором можно достать ключ для взаимодействия с API, включить/выключить демо-оплаты и установить URL на endpoint нашего сервера для приема веб-хука в случае оплаты.

Самое интересное начинается при формировании запроса на получение ссылки. Несмотря на то, что можно сгенерировать ссылку, используя веб-интерфейс, нам нужно это все это дело автоматизировать, для чего и предоставлен API.

Получение ссылки на оплату

Для формирования запроса на получение ссылки и верификацию веб-хука при оплате нам понадобятся:

  • Секретный ключ

  • Полная URL ссылка на главную платежную форму

Секретный ключ достаем из настроек формы:

Ссылка на главную платежную форму является еще и URL-адресом, на который будет выполняться POST или GET запрос. Пример такой ссылки.

Под POST или GET запросом имелось ввиду дословное “или”, как можно понять из документации. То есть, и POST и GET запросы с одинаковым содержимым будут делать одно и то же - отдавать сгенерированную платежную ссылку.

Обязательные параметры при формировании запроса:

Параметр

Тип

Описание

do

строка

может быть ‘link’ или ‘pay’

products

массив

товары (об этом чуть позже)

В нашем случае значение параметра ‘do’ будет ‘link’, потому что параметр ‘pay’, как описано в документации, “отправляет покупателя сразу на оплату. Используется для интернет-магазинов действие ‘Оплата’”.  Как это должно работать - мы не знаем и, все равно, не наш это случай.

Массив products содержит php-массивы (по сути, словари) с такими полями:

Параметр

Тип

Описание

name

строка

название товара

price

число

цена товара

quantity

целое число

количество товара

sku

строка

id товара в нашей системе

В дополнение к описанным полям есть еще необязательные, которые мы использовали:

Параметр

Тип

Описание

order_id

строка 

id заказа в нашей системе

customer_phone

строка 

если его не передать, то юзеру придется вводить его при оплате

link_expired

строка 

срок действия ссылки в формате "гггг-мм-дд чч:мм", иначе она будет действовать бессрочно.

installments_disabled

число

выключит все методы оплаты в рассрочку, если значение больше 0

callbackType

строка

если передано значение json, то в веб-хуке от Продамуса будет поле ‘submit’ в формате json.

payments_limit

число

количество возможных оплат по получаемой ссылке. Мы ставим 1

Есть еще параметр urlNotification, в который кладем ссылку на наш endpoint для веб-хука, но для этого нужно еще передавать параметр ‘sys’ с кодом вашей системы, определяемый внутри Продамуса, который надо еще и согласовывать с поддержкой. Этого можно избежать, прописав адрес в настройках формы, а не передавая в этом параметре, как мы и сделали.

Стоит упомянуть о таких параметрах, как payment_method, available_payment_methods, urlReturn и urlSuccess. В первом описываем один метод оплаты, во втором можно несколько, а параметры urlReturn и urlSuccess служат в перенаправлении пользователя в случае выхода без оплаты и непосредственно самой оплаты.

В документации описано как генерировать запрос на языке PHP, как раз это и создает немного трудностей, с которыми пришлось столкнуться при интеграции. С одной из них разберемся при запросе на получение ссылки.

Как выглядит идеальный запрос, описанный в документации? На PHP так:

открыть
<?php


header('Content-type:text/plain;charset=utf-8');
require_once __DIR__ . '/Hmac.php';


$linktoform = 'https://demo.payform.ru/';
$secret_key = '2y2aw4oknnke80bp1a8fniwuuq7tdkwmmuq7vwi4nzbr8z1182ftbn6p8mhw3bhz';
$data = [
  'order_id' => 'хххх',
  'customer_phone' => '+7хххххххххх',
  'customer_email' => 'ИМЯ@prodamus.ru',
  'products' => [
    [
      'sku' => 'ХХХХХ',
      'name' => 'товар 1',
      'price' => '123',
      'quantity' => 'Х'
    ]
  ]
];


$data['signature'] = Hmac::create($data, $secret_key);
$link = sprintf('%s?%s', $linktoform, http_build_query($data));

Попробуем сделать запрос на ruby.

Массивы в ruby отличаются от массивов php, вместо них уместно было бы использовать хэш, а в параметре products - массив хэшей, но к этому еще вернемся.

Параметр ‘signature’ можно вообще опустить, ссылка отлично генерируется и без него. 

‘Hmac::create’ - это метод мини-библиотеки Продамуса для генерации подписи с использованием алгоритма ‘sha256’, к переписыванию с php которой мы вернемся на этапе верификации веб-хука.

Метод ‘sprintf’ принимает первым аргументом строку-форматтер, которая в нашем случае просто добавляет ‘?’ между двумя строками, которые передаются следующими аргументами. Вторым аргументом передается строка с URL-адресом главной платежной формы, а третьим передается строка, конвертированная из PHP-массива в query-параметры методом ‘http_build_query’.

Проблема в том, что PHP-шный ‘http_build_query’, принимая массив, генерирует строку с параметрами, отличную от той, которая может быть сгенерирована методом ‘to_query’ над хэшем в ruby. Но решение было найдено.

Мы используем для формирования запросов на сторонние сервисы гем faraday, который тоже генерирует строку из хэша параметров и делает необходимый запрос. 

Для успешного запроса нужно собрать хэш такого вида:

data = {
  'do' => 'link',
  'callbackType' => 'json',
  'order_id' => transaction.id.to_s,
  'customer_phone' => customer_phone,
  'installments_disabled' => '1',
  'link_expired' => '2022-12-08 11:38',
  'products[0][name]' => object.title,
  'products[0][sku]' => object.id.to_s,
  'products[0][price]' => object.price.to_s,
  'products[0][quantity]' => '1'
}

Почему индексация массива ‘products’ так странно выглядит? Да потому что только так мы смогли привести генерацию хэша в строку параметров, идентичную строке, сгенерированной из массива PHP методом ‘http_build_query’ с таким наполнением данных. Было бы интересно увидеть лучший или хотя бы альтернативный способ ниже в комментариях.

Далее мы делаем уже сам запрос на сервер Продамуса:

Faraday.new(
  url: <main_payform_url>,
  params: data,
  headers: { 'Content-Type' => 'text/plain', 'charset' => 'utf-8' },
  request: { timeout: 5 }
).get

Такой запрос должен выдать нам ссылку, которую можно отправлять пользователю для оплаты товара.

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

Верификация ответа при оплате

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

Надеюсь, вы знаете, как создавать роуты и контроллеры в Rails, иначе пользователи вряд ли что-то купят безопасно. В своих проектах мы используем гемы dry-rb, про которые уже было написано в статье. В частности, мы активно используем dry-validation для валидации входящих параметров на контроллеры, создавая формы с правилами.

Вот так выглядит наша форма для приема запроса от Продамуса:

открыть
class CreateForm < Dry::Validation::Contract
  params do
    optional(:date).value(:string)
    optional(:order_id).value(:string)
    required(:order_num).value(:string)
    optional(:domain).value(:string)
    optional(:sum).value(:string)
    required(:customer_phone).value(:string)
    optional(:callbackType).value(:string)
    optional(:customer_extra).value(:string)
    optional(:payment_type).value(:string)
    optional(:commission).value(:string)
    optional(:commission_sum).value(:string)
    optional(:attempt).value(:string)
    optional(:sys).value(:string)
    optional(:link_expired).value(:string)
    required(:products).value(:array, min_size?: 1).each do
      hash do
        required(:name).value(:string)
        required(:price).value(:string)
        required(:quantity).value(:string)
        required(:sum).value(:string)
      end
    end
    required(:payment_status).value(:string)
    required(:payment_status_description).value(:string)
    required(:payment_init).value(:string)
    required(:submit).schema do
      optional(:date).value(:string)
      optional(:order_id).value(:string)
      required(:order_num).value(:string)
      optional(:domain).value(:string)
      optional(:sum).value(:string)
      optional(:currency).value(:string)
      required(:customer_phone).value(:string)
      optional(:customer_email).value(:string)
      optional(:customer_extra).value(:string)
      optional(:callbackType).value(:string)
      optional(:payment_type).value(:string)
      optional(:commission).value(:string)
      optional(:commission_sum).value(:string)
      optional(:attempt).value(:string)
      optional(:sys).value(:string)
      optional(:link_expired).value(:string)
      required(:products).value(:array, min_size?: 1).each do
        hash do
          required(:name).value(:string)
          required(:price).value(:string)
          required(:quantity).value(:string)
          required(:sum).value(:string)
        end
      end
      required(:payment_status).value(:string)
      required(:payment_status_description).value(:string)
      required(:payment_init).value(:string)
    end
  end
end

Такой payload и приходит. Много полей повторяется с теми, которые мы отправляем, формируя ссылку, но есть одна особенность - order_id теперь идентификатор заказа не в нашей системе, а в системе Продамуса. А order_num - это уже идентификатор заказа нашей системы, который мы отправляли как order_id. Интересно получилось.

Помимо всего прочего, в хедерах запроса приходит поле ‘Sign’, содержащее подписанные данные из поля тела ‘submit’.

Поле ‘submit’, значение которого в основном повторяет остальные поля, служит для того, чтобы верифицировать веб-хук по подписи из хедера. Для этого используется библиотека Продамуса, переписывание которой я упомянул ранее.

Продамус в документации предоставляет библиотеку на php и, с недавних пор, на js. Мне же пришлось переписать и на ruby самостоятельно.

Для верификации данных по подписи, собственно, требуется эти самые данные привести к необходимому виду, подписать и сравнить полученную от Продамуса подпись с созданной нами как две строки. Опять звучит просто? Давайте углубимся.

  1. Сперва надо привести данные, лежащие в ‘params[:submit]’ к необходимому виду и отсортировать. В параметрах данные нам Rails отдают, к счастью, хэшем. С ним и будем манипулировать.

Все, что нам нужно - рекурсивно отсортировать хэш по ключу.

На PHP это выглядит так:

static private function _sort(&$data) {
  ksort($data, SORT_REGULAR);
  foreach ($data as &$arr)
    is_array($arr) && self::_sort($arr);
}

На ruby таким образом:

def sort(data)
  data.sort.to_h.transform_values do |value|
    if value.is_a?(Array)
      value.map do |product|
        product.sort.to_h.transform_values(&:to_s)
      end
    else
      value.to_s
    end
  end
end

Этот метод сортировки мы отдельно вызывать нигде не будем, только внутри метода создания подписи.

  1. Создаем подпись, сначала сортируя предыдущим методом

def encode(data, key, algorithm = ‘sha256’)
  data = sort(data).to_json
  digest = OpenSSL::Digest.new(algorithm)


  OpenSSL::HMAC.hexdigest(digest, key, data)
rЭтот метод сортировки мы отдельно вызывать нигде не будем, только внутри метода создания подписи.

Создаем подпись, сначала сортируя предыдущим методом

escue NoMethodError
  raise ArgumentError, 'Expected a Hash with array of hashes.'
end

Отсортированный хэш конвертируем в JSON и подписываем.

  1. Для верификации просто сравниваем строку из поля ‘Sign’ в хедерах веб-хука и подписанные нами данные из поля ‘submit’ в теле как строки:

def verify(sign, data)
   encoded_data = encode(data)
   encoded_data && (encoded_data == sign)
end

Если метод verify(sign, data) возвращает true, то верификация пройдена, и покупку можно регистрировать в системе, если же false, то по веб-хук пришел либо не наш, по ошибке, либо кто-то решил обмануть вас.

При верификации необходимо ответить на веб-хук Продамусу 200-тым статусом, иначе он будет повторяться много раз.

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

Мы обсудили трудности, с которыми столкнулись при работе с Продамусом, и поделились решением этих проблем. Компиляцией этих решений стал наш гем, который уже был упомянут ранее, найти его можно по ссылке.

Надеюсь, для вас эта статья была полезной.

Комментарии (1)


  1. R5001
    18.10.2023 05:59

    Кстати, Prodamus быстро растет. Я уже третий раз встречаю его упоминание за последние 2 недели