Всем привет! В этой статье я и мой коллега Рустем расскажем о том, как мы реализуем оплаты в наших проектах на Ruby On Rails на примере платформы RuStore, а также поделимся разработанной библиотекой для взаимодействия с её API.

Схема оплат

Чаще всего в наших приложениях мы реализуем следующий алгоритм покупок:

  1. Пользователь выбирает продукт для покупки в приложении и нажимает на кнопку "Купить". Приложение отправляет запрос на сервер платформы, передавая идентификатор продукта и другую необходимую информацию.

  2. Сервер платформы проверяет информацию о продукте и возвращает ответ с данными о покупке, чаще всего рецепт (чек), включающий цену и идентификатор транзакции. 

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

  4. Сервер приложения отправляет запрос с данными о покупке с помощью API, который предоставляет платформа для верификации покупки.

  5. Сервер платформы возвращает результат проверки - это может быть ответ с положительным или отрицательным результатом верификации или развёрнутая информация непосредственно о платеже.

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

Возникает вопрос: почему на сервере приложения происходит только подтверждение факта оплаты? В первую очередь это связано с тем, что хранение банковских данных пользователя крайне небезопасно. Популярные платформы самостоятельно обеспечивают защиту данных пользователей, а также управление платежами и подписками. Такой тип проведения оплат характерен для самых известных платформ – GooglePlay и AppStore. Таким образом, можно без проблем осуществлять как покупку единичных продуктов, так и оформление и продление подписок.

Сразу можно обозначить плюсы и минусы для данной схемы оплат. 

Плюсы:

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

  • Доступность для пользователей без банковской карты (часто возможна покупка через баланс счёта мобильного телефона). 

  • Скорость проведения платежа. 

Минусы: 

  • Высокие комиссии за проведение платежей, которые могут быть выше, чем при использовании других альтернативных систем.

Подключаем RuStore

Небезызвестная отечественная платформа RuStore также предоставляет вышеописанный способ проведения оплат. На странице с API показаны несколько методов для получения данных о платежах и подписках с помощью различных параметров.

Для взаимодействия с API RuStore прежде всего необходимо получить приватный ключ из пары ключей, сгенерированных через RuStore Консоль. Сразу отметим, что он генерируется с помощью алгоритма шифрования RSA (такой же используется для генерации ssh-ключей). Его можно хранить в файле либо просто в виде строки, но с одним условием – обязательно наличие маркеров начала и конца, иначе OpenSSL библиотека просто-напросто генерирует неправильную подпись.

Следующий этап – получение токена авторизации для отправки запросов на RuStore API. На странице довольно подробно описано, как получить данный токен, за исключением одного нюанса – параметр signature, который является подписанным с помощью приватного ключа SHA-512 хэшем, должен быть зашифрован в base64-строку (на момент написания статьи это не упоминалось в официальной документации). Поняли мы это спустя множество обращений в службу поддержки, пока нам не предоставили следующий sh скрипт с алгоритмом шифрования:

# $1 companyId
# $2 private key
# формируем строку для подписи
var1=$(date "+%Y-%m-%dT%T.%N%:z")
var2=$1
var3="$var2$var1"
echo Get hash from: $var3
# формируем хеш
var4=$(echo -n $var3 | openssl dgst -sha512)
var5=${var4#*= }
echo HASH: $var5
# формируем подпись передав подписываемую строку, при подписи будет сначала вычислен хеш (SHA-512), после
var6=$(echo -n $var3 | openssl dgst -sha512 -sign $2 -binary | base64 --wrap=0)
echo SIGN: $var6
echo result body json request:
echo "{\"companyId\":\"$var2\",\"timestamp\":\"$var1\",\"signature\":\"$var6\"}"

Ну а теперь можно написать метод получения токена авторизации на Ruby:

def authorize!
  timestamp = (DateTime.now - 1.second).strftime('%Y-%m-%dT%H:%M:%S%:z')
  data = {
    companyId: COMPANY_ID,
    timestamp: timestamp,
    signature: sign(timestamp)
  }
  response = connection.post('/public/auth/') do |req|
    req.body = data.to_json
  end
  response_body = handle_response(response)
  @token = response_body[:body][:jwe]
  @expired_at = DateTime.now + response_body[:body][:ttl].second
  response_body
end


private


def sign(timestamp)
  payload = "#{COMPANY_ID}#{timestamp}"
  pkey = OpenSSL::PKey::RSA.new(File.open(KEY_PATH, 'r', &:read))
  sign = pkey.sign(OpenSSL::Digest.new('SHA512'), payload)
  Base64.strict_encode64(sign)
end


def connection
   Faraday.new(
    url: BASE_URL,
    headers: { 'Content-Type' => 'application/json', 'charset' => 'utf-8' }
  )
end


def handle_response(response)
  response_body = JSON.parse(response.body).deep_symbolize_keys!
  raise Api::RustoreError.new(response_body[:message]) if response.status >= 400


  response_body
end

Константа KEY_PATH в коде выше обозначает путь к файлу с приватным RSA-ключом. COMPANY_ID можно получить в RuStore консоли, а BASE_URL – это адрес для отправки запросов. В качестве HTTP-клиента используется популярный гем Faraday. Можно заметить, что переменной timestamp присваивается время с отставанием на секунду. Всё дело в том, что при отправке запроса было обнаружено, что он периодически возвращает ошибку. Как оказалось, это было связано с тем, что локальное время опережало время на сервере RuStore приблизительно на секунду, поэтому было использовано такое незатейливое решение. Если у вас есть какие-либо мысли о том, почему так происходит, поделитесь, пожалуйста, ими в комментариях.

После получения токена нам открывается возможность отправлять все остальные запросы, описанные в документации, передавая полученный токен в заголовке Public-Token. К примеру, метод получения платежа по его subscription_token будет выглядеть следующим образом:

def payment_data(subscription_token )
  authorize! if token_expired_or_nil?


  response = connection.get("/public/purchases/#{subscription_token}") do |req|
    req.headers['Public-Token'] = @token
  end


  handle_response(response)
end

Данный метод можно использовать для верификации платежа на сервере приложения. JSON-объект, полученный в результате запроса, содержит несколько полей (все они описаны тут), по которым можно установить валидность платежа. Как правило, это поля, содержащие информацию, идентифицирующую совершившего покупку пользователя, идентификатор товара, сумму платежа, количество единиц продукта и прочее (к примеру, поле purchaser содержит данные покупателя, такие как электронная почта и номер телефона). Грубо говоря, осуществление покупки в Ruby-приложении сводится к следующему методу, в результате которого создаётся внутренняя транзакция, предоставляющая пользователю доступ к какому-либо продукту.

def call(store_item_id, buyable_type, subscription_token)
  buyable_obj = buyable_type.capitalize.constantize.find_by!(store_item_id: store_item_id)
  payment_data = RustoreApi::Client.new.payment_data(subscription_token)
  if payment_valid?(payment_data, buyable_obj, store_item_id)
    InternalTransaction.create!(
      user: user,
      buyable: buyable_obj,
      amount: buyable_obj.price
      # ...
    )
  end
end


private


def payment_valid?(payment_data, buyable_obj, store_item_id)
  payment_data[:invoice_id][:purchaser][:email] == user.email &&
    payment_data[:invoice][:order][:order_bundle].first[:item_code] == store_item_id &&
    payment_data[:order][:amount] == buyable_obj.price
end

Здесь store_item_id выступает в качестве идентификатора продукта в приложении. Подразумевается, что на платформе RuStore и на сервере приложения обязательно должен быть зарегистрирован продукт с таким идентификатором.

Вот и всё, мы рассказали вам о базовом механизме оплат на самых популярных платформах, а также о некоторых нюансах работы со свежим RuStore API. В результате мы разработали небольшой гем, предоставляющий интерфейс для работы с основными методами RuStore.

Поделитесь своим опытом в комментариях.

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


  1. dmazilov
    23.05.2023 10:39

    По схеме возможна ситуация:

    П.1 и 2 корректно отработали - пользователь выбрал товар, перешел на платформу, оплатил.

    П.3 не отработал (по какой-то причине) - бэкенд вообще не узнает о попытке оплаты и ее результате со всеми вытекающими.

    На схеме просто не отражено, или вы такие ситуации не учитываете?


    1. VeronikaMolchanova Автор
      23.05.2023 10:39

      Без выполнения пункта 3 пользователь просто не получит товар. Если он каким-то образом не сработал, пользователь может попробовать купить товар ещё раз, не обращаясь к серверу платформы, так как у него уже должен быть чек покупки в локальной бд.


      1. dmazilov
        23.05.2023 10:39

        пользователь просто не получит товар

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


  1. EvilBlueBeaver
    23.05.2023 10:39

    И в Google Play и в AppStore прекрасно работают схемы с подписями чеков приватным ключом стора и проверкой их без непосредственного обращения к самому стору со стороны бэкенда. Это в некоторых случаях, правда, добавляет других проблем.