Подключаем RuStore оплаты для вашего Ruby-приложения

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет! В этой статье я и мой коллега Рустем расскажем о том, как мы реализуем оплаты в наших проектах на 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)
    InternalTransaction.create!(
      user: user,
      buyable: buyable_obj,
      amount: buyable_obj.price
      # ...
    )
  end
end


private


def payment_valid?(payment_data, buyable_obj)
  payment_data[:invoice_id][:purchaser][:email] == user.email &&
    payment_data[:invoice_params] == product_id &&
    payment_data[:order][:amount] == buyable_obj.price
end

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

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

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

Источник: https://habr.com/ru/companies/joydev/articles/736756/


Интересные статьи

Интересные статьи

Российский магазин приложений RuStore продолжает серию дайджестов о рынке мобильной разработки — мы собираем самые интересные и важные новости, которые вы могли пропустить.
Создание монетизируемого мобильного приложения для App Store или Google Play  – только первый шаг на пути к успеху. Не менее важно достичь постоянного роста пользователей и прибыли проекта. Для э...
Всегда ли обновление дизайна — это хорошо для сайта с точки зрения SEO? Нет, не всегда. Более того, часто редизайн влечет за собой ухудшение позиций. Так может, лучше не трогать то, что и так работает...
Новый хаб от Xiaomi с поддержкой технологий Zigbee 3, Bluetooth Mesh, HomeKit и его подключение к достаточно популярной системе умного дома — Home Assistant, интересует? ...
Прошлым летом мы с пацанами рассказали про свою либу, которую наш заказчик не принял и выкинул на помойку. Мы бомбили, потому что верили в свое решение, и рассказали о нем сообществу — уж обы...