Интеграция методов оплаты Stripe и PayPal в Ruby on Rails

Опубликовано: 2022-03-11

Ключевой особенностью крупных компаний электронной коммерции, таких как AliExpress, Ebay и Amazon, является безопасный способ обработки платежей, который необходим для их бизнеса. Если эта функция не работает, последствия будут разрушительными. Это относится к лидерам отрасли и разработчикам Ruby on Rails, работающим над приложениями для электронной коммерции.

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

В Интернете существует множество сервисов платежных шлюзов, но в этой статье я сосредоточусь на интеграции Stripe и PayPal в приложение Rails. Упомянем еще несколько: Amazon Payments, Square, SecurePay, WorldPay, Authorize.Net, 2Checkout.com, Braintree, Amazon или BlueSnap.

Как работает интеграция платежного шлюза

Общее представление транзакций с участием платежных шлюзов
Общее представление транзакций с участием платежных шлюзов

В общем, в вашем приложении будет форма/кнопка, где пользователь может авторизоваться/вставить данные кредитной карты. PayPal и Stripe уже сделали этот первый шаг более безопасным, используя формы или popups iframe , которые не позволяют вашему приложению хранить конфиденциальную информацию о кредитной карте пользователя, поскольку они возвращают токен, представляющий эту транзакцию. Некоторые пользователи также могут чувствовать себя более уверенно при обработке платежей, зная, что сторонний сервис обрабатывает процесс транзакции, поэтому это также может быть привлекательным для вашего приложения.

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

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

В отличие от Stripe, PayPal перенаправляет пользователя на страницу входа в PayPal. Пользователь авторизует и выбирает способ оплаты через PayPal, и снова ваш сервер будет обрабатывать токены вместо конфиденциальных данных пользователя.

Важно отметить, что для этих двух платежных шлюзов ваша серверная часть должна запрашивать продолжение выполнения транзакции через API-интерфейсы Stripe или PayPal, которые будут давать ответ OK/NOK, поэтому ваше приложение должно перенаправлять пользователя на успешную или ошибочную страницу соответственно.

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

Настраивать

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

Примечание. Вы можете ознакомиться с новыми функциями Rails 6 в нашей недавней статье.

Шаг 1: Инициализируйте приложение Rails.

Инициализируйте проект, запустив инициализацию проекта с помощью команды rails с именем вашего приложения:

 rails new YOUR_APP_NAME

И cd в папке вашего приложения.

Шаг 2: Установите драгоценные камни.

Помимо драгоценных камней Stripe и PayPal, было добавлено несколько других драгоценных камней:

  • devise : используется для аутентификации и авторизации пользователя.
  • haml : шаблонизатор для рендеринга пользовательских страниц
  • jquery-rails : для jquery во внешних сценариях.
  • money-rails : для отображения отформатированных денежных значений

Добавьте в свой Gemfile :

 gem "devise", ">= 4.7.1" gem "haml" gem "jquery-rails" gem "money-rails"

После добавления запустите в CLI:

 bundle install

Шаг 3: Инициализируйте драгоценные камни.

Некоторые из этих драгоценных камней потребуют инициализации, кроме их установки через bundle .

Установка устройства:

 rails g devise:install

Инициализация money-rails :

 rails g money_rails:initializer

Инициализируйте jquery-rails , добавив в конец app/assets/javascripts/application.js следующее:

 //= require jquery //= require jquery_ujs

Шаг 4: Таблицы и миграции

В этом проекте будут использоваться три таблицы Users , Products и Orders .

  • Users : будут сгенерированы через devise
  • Столбцы Products :
    • name
    • price_cents
    • Stripe_plan_name : идентификатор, представляющий план подписки, созданный в Stripe, чтобы пользователи могли подписаться на него. Это поле требуется только для продуктов, связанных с планом Stripe.
    • paypal_plan_name : то же самое, что и stripe_plan_name , но для PayPal.
  • Столбцы Orders :
    • product_id
    • user_id
    • status : это сообщит, находится ли заказ в ожидании, не выполнен или оплачен.
    • token : это токен, сгенерированный из API (либо Stripe, либо PayPal) для инициализации транзакции.
    • price_cents : аналогично продукту, но используется для того, чтобы это значение сохранялось в записи заказа.
    • payment_gateway : хранит, какой платежный шлюз используется для заказа PayPal или Stripe.
    • customer_id : будет использоваться для Stripe для хранения клиента Stripe для подписки, и это будет объяснено более подробно в следующем разделе.

Для создания этих таблиц необходимо выполнить несколько миграций:

Для создания таблицы пользователей . Бегать:

 rails g devise User

Для создания таблицы «Продукты» . Создайте миграцию, запустив:

 rails generate migration CreateProducts name:string stripe_plan_name:string paypal_plan_name:string

Откройте созданный вами файл миграции, который должен находиться в db/migrate/ , и внесите изменения, чтобы ваша миграция выглядела примерно так:

 class CreateProducts < ActiveRecord::Migration[5.2] def change create_table :products do |t| t.string :name t.string :stripe_plan_name t.string :paypal_plan_name end add_money :products, :price, currency: { present: true } end end

Для создания таблицы Orders . Создайте миграцию, запустив:

 rails generate migration CreateOrders product_id:integer user_id:integer status:integer token:string charge_id:string error_message:string customer_id:string payment_gateway:integer

Снова откройте созданный файл миграции, который должен находиться в db/migrate/ , и внесите изменения в этот файл, чтобы он выглядел примерно так:

 class CreateOrders < ActiveRecord::Migration[5.2] def change create_table :orders do |t| t.integer :product_id t.integer :user_id t.integer :status, default: 0 t.string :token t.string :charge_id t.string :error_message t.string :customer_id t.integer :payment_gateway t.timestamps end add_money :orders, :price, currency: { present: false } end end

Запустите миграцию базы данных, выполнив:

 rails db:migrate

Шаг 5: Создайте модели.

Пользовательская модель уже создана при установке устройства, и в ней не потребуется никаких изменений. Кроме того, для Product и Order будут созданы две модели.

Товар. Добавьте новый файл app/models/product.rb с:

 class Product < ActiveRecord::Base monetize :price_cents has_many :orders end

Заказ. Добавьте новый файл app/models/order.rb с:

 class Order < ApplicationRecord enum status: { pending: 0, failed: 1, paid: 2, paypal_executed: 3} enum payment_gateway: { stripe: 0, paypal: 1 } belongs_to :product belongs_to :user scope :recently_created, -> { where(created_at: 1.minutes.ago..DateTime.now) } def set_paid self.status = Order.statuses[:paid] end def set_failed self.status = Order.statuses[:failed] end def set_paypal_executed self.status = Order.statuses[:paypal_executed] end end

Шаг 6: Заполните базу данных.

В консоли будут созданы пользователь и два продукта. Записи заказов будут созданы в соответствии с тестами оплаты.

  • Беговые rails s
  • В браузере перейдите http://localhost:3000
  • Вы будете перенаправлены на страницу регистрации.
  • Зарегистрируйте пользователя, заполнив его адрес электронной почты и пароль.
  • В вашем терминале будут запрошены следующие журналы, показывающие, что пользователь был создан в вашей базе данных:
 User Create (0.1ms) INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at") VALUES (?, ?, ?, ?) …
  • Создайте два продукта без подписок, запустив rails c и добавив:
    • Product.create(name: "Awesome T-Shirt", price_cents: 3000)
    • Product.create(name: "Awesome Sneakers", price_cents: 5000)

Шаг 7: Создайте индексную страницу

На главной странице проекта есть выбор товаров для покупки или подписки. Кроме того, в нем также есть раздел для выбора способа оплаты (Stripe или PayPal). Кнопка отправки также используется для каждого типа платежного шлюза, так как для PayPal мы добавим собственный дизайн кнопки через библиотеку JavaScript.

Сначала создайте маршруты для index и submit их в config/routes.rb .

 Rails.application.routes.draw do devise_for :users get '/', to: 'orders#index' post '/orders/submit', to: 'orders#submit' end

Создайте и добавьте index действий и submit его в app/controllers/orders_controller.rb . Действие orders#index хранит две переменные, которые будут использоваться во внешнем интерфейсе: @products_purchase со списком продуктов без планов и @products_subscription со списком продуктов с планами PayPal и Stripe.

 class OrdersController < ApplicationController before_action :authenticate_user! def index products = Product.all @products_purchase = products.where(stripe_plan_name:nil, paypal_plan_name:nil) @products_subscription = products - @products_purchase end def submit end end

Создайте файл в app/views/orders/index.html.haml . Этот файл содержит все входные данные, которые мы собираемся отправить на наш сервер с помощью метода отправки, а также взаимодействие для платежных шлюзов и выбора продукта. Вот несколько атрибутов входного имени:

  • Orders[product_id] хранит идентификатор продукта.
  • Orders[payment_gateway] содержит платежный шлюз со значениями Stripe или PayPal для другого.
 %div %h1 List of products = form_tag({:controller => "orders", :action => "submit" }, {:id => 'order-details'}) do %input{id:'order-type', :type=>"hidden", :value=>"stripe", :name=>'orders[payment_gateway]'} .form_row %h4 Charges/Payments - @products_purchase.each do |product| %div{'data-charges-and-payments-section': true} = radio_button_tag 'orders[product_id]', product.id, @products_purchase.first == product %span{id: "radioButtonName#{product.id}"} #{product.name} %span{id: "radioButtonPrice#{product.id}", :'data-price' => "#{product.price_cents}"} #{humanized_money_with_symbol product.price} %br %h4 Subscriptions - @products_subscription.each do |product| %div = radio_button_tag 'orders[product_id]', product.id, false %span{id: "radioButtonName#{product.id}"} #{product.name} %span{id: "radioButtonPrice#{product.id}", :'data-price' => "#{product.price_cents}"} #{humanized_money_with_symbol product.price} %br %hr %h1 Payment Method .form_row %div = radio_button_tag 'payment-selection', 'stripe', true, onclick: "changeTab();" %span Stripe %br %div = radio_button_tag 'payment-selection', 'paypal', false, onclick: "changeTab();" %span Paypal %br %br %div{id:'tab-stripe', class:'paymentSelectionTab active'} %div{id:'card-element'} %div{id:'card-errors', role:"alert"} %br %br = submit_tag "Buy it!", id: "submit-stripe" %div{id:'tab-paypal', class:'paymentSelectionTab'} %div{id: "submit-paypal"} %br %br %hr :javascript function changeTab() { var newActiveTabID = $('input[name="payment-selection"]:checked').val(); $('.paymentSelectionTab').removeClass('active'); $('#tab-' + newActiveTabID).addClass('active'); } :css #card-element { width:500px; } .paymentSelectionTab { display: none; } .paymentSelectionTab.active { display: block !important; }

Если вы запустите свое приложение с помощью rails s и посетите свою страницу в http://localhost:3000 . Вы должны увидеть страницу следующим образом:

Необработанная индексная страница без интеграции Stripe и PayPal
Необработанная индексная страница без интеграции Stripe и PayPal

Хранилище учетных данных платежного шлюза

Ключи PayPal и Stripe будут храниться в файле, не отслеживаемом Git. В этом файле хранится два типа ключей для каждого платежного шлюза, и сейчас мы будем использовать для них фиктивное значение. Дополнительные указания по созданию этих ключей представлены в следующих разделах.

Шаг 1: Добавьте это в .gitignore .

 /config/application.yml

Шаг 2: Создайте файл с вашими учетными данными в config/application.yml . Он должен содержать все ваши тестовые/тестовые ключи PayPal и Stripe для доступа к этим API.

 test: &default PAYPAL_ENV: sandbox PAYPAL_CLIENT_ID: YOUR_CREDENTIAL_HERE PAYPAL_CLIENT_SECRET: YOUR_CREDENTIAL_HERE STRIPE_PUBLISHABLE_KEY: YOUR_CREDENTIAL_HERE STRIPE_SECRET_KEY: YOUR_CREDENTIAL_HERE development: <<: *default

Шаг 3: Чтобы сохранить переменные из файла config/application.yml при запуске приложения, добавьте эти строки в config/application.rb внутри класса Application , чтобы они были доступны в ENV .

 config_file = Rails.application.config_for(:application) config_file.each do |key,value| ENV[key] = value end unless config_file.nil?

Конфигурация полосы

Мы добавим гем для использования Stripe API: stripe-rails . Также необходимо создать учетную запись Stripe, чтобы можно было обрабатывать платежи и подписки. Если вам нужно, вы можете ознакомиться с методами API для Stripe API в официальной документации.

Шаг 1: Добавьте гем Stripe-Rails в свой проект.

Гем stripe-rails предоставит интерфейс для всех запросов API, используемых в этом проекте.

Добавьте это в Gemfile :

 gem 'stripe-rails'

Бегать:

 bundle install

Шаг 2: Создайте ключи API.

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

  • Создайте учетную запись в Stripe, если у вас ее нет (https://dashboard.stripe.com/).
  • Находясь на панели инструментов Stripe, после входа в систему включите просмотр тестовых данных .
  • На https://dashboard.stripe.com/test/apikeys замените YOUR_CREDENTIAL_HERE для значений STRIPE_PUBLISHABLE_KEY и STRIPE_SECRET_KEY в /config/application.yml содержимым из Publishable Key и Secret key .

Шаг 3: Инициализируйте модуль Stripe

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

Создайте файл в config/initializers/stripe.rb с:

 Rails.application.configure do config.stripe.secret_key = ENV["STRIPE_SECRET_KEY"] config.stripe.publishable_key = ENV["STRIPE_PUBLISHABLE_KEY"] end

Шаг 4: Интегрируйте Stripe во внешний интерфейс.

Мы добавим библиотеку Stripe JavaScript и логику для отправки токена, который представляет информацию о кредитной карте пользователя и будет обрабатываться в нашей серверной части.

В файле index.html.haml добавьте это в начало файла. Это будет использовать модуль Stripe (предоставленный gem) для добавления библиотеки javascript Stripe на страницу пользователя.

 = stripe_javascript_tag

Stripe использует безопасные поля ввода, которые создаются через их API. Поскольку они создаются в iframe , созданном с помощью этого API, вам не придется беспокоиться о возможных уязвимостях, связанных с обработкой информации о кредитной карте пользователя. Кроме того, ваш сервер не сможет обрабатывать/хранить какие-либо конфиденциальные данные пользователя, и он получит только токен, представляющий эту информацию.

Эти поля ввода создаются вызовом stripe.elements().create('card') . После этого просто необходимо вызвать возвращаемый объект с помощью mount() , передав в качестве аргумента идентификатор/класс элемента HTML, к которому должны быть смонтированы эти входные данные. Более подробную информацию можно найти на Stripe.

Когда пользователь нажимает кнопку отправки с помощью метода оплаты Stripe, другой вызов API, возвращающий обещание, выполняется для созданного элемента карты Stripe:

 stripe.createToken(card).then(function(result)

Переменная result этой функции, если ей не назначена ошибка свойства, будет иметь токен, который можно получить, обратившись к атрибуту result.token.id . Этот токен будет отправлен на серверную часть.

Чтобы внести эти изменения, замените закомментированный код // YOUR STRIPE AND PAYPAL CODE WILL BE HERE в index.html.haml на:

 (function setupStripe() { //Initialize stripe with publishable key var stripe = Stripe("#{ENV['STRIPE_PUBLISHABLE_KEY']}"); //Create Stripe credit card elements. var elements = stripe.elements(); var card = elements.create('card'); //Add a listener in order to check if card.addEventListener('change', function(event) { //the div card-errors contains error details if any var displayError = document.getElementById('card-errors'); document.getElementById('submit-stripe').disabled = false; if (event.error) { // Display error displayError.textContent = event.error.message; } else { // Clear error displayError.textContent = ''; } }); // Mount Stripe card element in the #card-element div. card.mount('#card-element'); var form = document.getElementById('order-details'); // This will be called when the #submit-stripe button is clicked by the user. form.addEventListener('submit', function(event) { $('#submit-stripe').prop('disabled', true); event.preventDefault(); stripe.createToken(card).then(function(result) { if (result.error) { // Inform that there was an error. var errorElement = document.getElementById('card-errors'); errorElement.textContent = result.error.message; } else { // Now we submit the form. We also add a hidden input storing // the token. So our back-end can consume it. var $form = $("#order-details"); // Add a hidden input orders[token] $form.append($('<input type="hidden" name="orders[token]"/>').val(result.token.id)); // Set order type $('#order-type').val('stripe'); $form.submit(); } }); return false; }); }()); //YOUR PAYPAL CODE WILL BE HERE

Если вы посещаете свою страницу, она должна выглядеть следующим образом с новыми полями безопасного ввода Stripe:

Индексная страница интегрирована с безопасными полями ввода Stripe.
Индексная страница интегрирована с безопасными полями ввода Stripe.

Шаг 5: Протестируйте свое приложение.

Заполните форму кредитной карты тестовой картой (https://stripe.com/docs/testing) и отправьте страницу. Проверьте, вызывается ли действие submit со всеми параметрами ( product_id , payment_gateway и token ) в выходных данных вашего сервера.

Полосатые заряды

Плата за чередование представляет собой разовые транзакции. Таким образом, после транзакции оплаты Stripe вы получите деньги напрямую от клиента. Это идеально подходит для продажи продуктов, которые не связаны с планами. В следующем разделе я покажу, как сделать такой же тип транзакции с PayPal, но имя PayPal для этого типа транзакции — Payment .

В этом разделе я также предоставлю весь скелет для обработки и отправки заказа. Мы создаем заказ в действии submit при отправке формы Stripe. Первоначально этот заказ будет иметь статус « ожидание », поэтому, если что-то пойдет не так во время обработки этого заказа, заказ все еще будет находиться в режиме ожидания .

Если при вызовах Stripe API возникает какая-либо ошибка, мы устанавливаем заказ в состояние failed , а в случае успешного завершения списания он будет в состоянии оплачен . Пользователь также перенаправляется в соответствии с ответом Stripe API, как показано на следующем графике:

Полоса транзакций.
Полоса транзакций.

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

Шаг 1: Создайте сервис Stripe.

Мы будем использовать одноэлементный класс для представления операций Stripe с помощью Stripe API. Для создания начисления вызывается метод Stripe::Charge.create , а возвращаемый атрибут идентификатора объекта будет сохранен в записи заказа charge_id . Эта функция create вызывается путем передачи токена, созданного во внешнем интерфейсе, цены заказа и описания.

Итак, создайте новую папку app/services/orders и добавьте сервис Stripe: app/services/orders/stripe.rb , содержащий одноэлементный класс Orders::Stripe , который имеет запись в методе execute .

 class Orders::Stripe INVALID_STRIPE_OPERATION = 'Invalid Stripe Operation' def self.execute(order:, user:) product = order.product # Check if the order is a plan if product.stripe_plan_name.blank? charge = self.execute_charge(price_cents: product.price_cents, description: product.name, card_token: order.token) else #SUBSCRIPTIONS WILL BE HANDLED HERE end unless charge&.id.blank? # If there is a charge with id, set order paid. order.charge_id = charge.id order.set_paid end rescue Stripe::StripeError => e # If a Stripe error is raised from the API, # set status failed and an error message order.error_message = INVALID_STRIPE_OPERATION order.set_failed end private def self.execute_charge(price_cents:, description:, card_token:) Stripe::Charge.create({ amount: price_cents.to_s, currency: "usd", description: description, source: card_token }) end end

Шаг 2: Реализуйте действие отправки и вызовите сервис Stripe.

В orders_controller.rb добавьте следующее в действие submit , которое в основном вызовет службу Orders::Stripe.execute . Обратите внимание, что также были добавлены две новые приватные функции: prepare_new_order и order_params .

 def submit @order = nil #Check which type of order it is if order_params[:payment_gateway] == "stripe" prepare_new_order Orders::Stripe.execute(order: @order, user: current_user) elsif order_params[:payment_gateway] == "paypal" #PAYPAL WILL BE HANDLED HERE end ensure if @order&.save if @order.paid? # Success is rendered when order is paid and saved return render html: SUCCESS_MESSAGE elsif @order.failed? && [email protected]_message.blank? # Render error only if order failed and there is an error_message return render html: @order.error_message end end render html: FAILURE_MESSAGE end private # Initialize a new order and and set its user, product and price. def prepare_new_order @order = Order.new(order_params) @order.user_id = current_user.id @product = Product.find(@order.product_id) @order.price_cents = @product.price_cents end def order_params params.require(:orders).permit(:product_id, :token, :payment_gateway, :charge_id) end

Шаг 3: Протестируйте свое приложение.

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

Полоса подписки

Подписки или планы могут быть созданы для регулярных платежей. С этим типом продукта с пользователя взимается плата ежедневно, еженедельно, ежемесячно или ежегодно в соответствии с конфигурацией плана. В этом разделе мы будем использовать поле для продукта stripe_plan_name для хранения идентификатора плана — на самом деле, мы можем выбрать идентификатор, и мы назовем его premium-plan который будет использоваться для создания отношение customer <-> subscription .

Мы также создадим новый столбец для таблицы пользователей с именем stripe_customer_id , который будет заполнен свойством id объекта клиента Stripe. Клиент Stripe создается при вызове функции Stripe::Customer.create , и вы также можете проверить клиентов, созданных и связанных с вашей учетной записью, в (https://dashboard.stripe.com/test/customers). Клиенты создаются путем передачи source параметра, который в нашем случае является токеном, сгенерированным во внешнем интерфейсе, который отправляется при отправке формы.

Объект клиента, полученный в результате последнего вызова Stripe API, также используется для создания подписки, которая выполняется путем вызова customer.subscriptions.create и передачи идентификатора плана в качестве параметра.

Кроме того, гем Stripe stripe-rails Rails предоставляет интерфейс для получения и обновления клиента из Stripe, что выполняется вызовами Stripe::Customer.retrieve и Stripe::Customer.update соответственно.

Таким образом, когда в записи пользователя уже есть stripe_customer_id , вместо создания нового клиента с помощью Stripe::Customer.create мы вызовем Stripe::Customer.retrieve , передав в качестве параметра stripe_customer_id , за которым следует Stripe::Customer.update . , и в этом случае передача токена в качестве параметра.

Во-первых, мы создадим план с использованием Stripe API, чтобы мы могли создать новый продукт подписки, используя поле stripe_plan_name . После этого мы внесем изменения в orders_controller и Stripe, чтобы обрабатывать создание и выполнение подписок Stripe.

Шаг 1. Создайте план с помощью Stripe API.

Откройте консоль с помощью команды rails c . Создайте подписку для своей учетной записи Stripe с помощью:

 Stripe::Plan.create({ amount: 10000, interval: 'month', product: { name: 'Premium plan', }, currency: 'usd', id: 'premium-plan', })

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

Шаг 2: Создайте продукт в базе данных с установленным полем stripe_plan_name .

Теперь создайте продукт с stripe_plan_name , установленным как premium-plan в базе данных:

 Product.create(price_cents: 10000, name: 'Premium Plan', stripe_plan_name: 'premium-plan')

Шаг 3: Создайте миграцию для добавления столбца stripe_customer_id в таблицу users .

Запустите в терминале следующее:

 rails generate migration AddStripeCustomerIdToUser stripe_customer_id:string rails db:migrate

Шаг 4: Реализуйте логику подписки в сервисном классе Stripe.

Добавьте еще две функции в приватные методы app/services/orders/stripe.rb : execute_subscription отвечает за создание подписок в объекте клиента. Функция find_or_create_customer отвечает за возврат уже созданного клиента или путем возврата вновь созданного клиента.

 def self.execute_subscription(plan:, token:, customer:) customer.subscriptions.create({ plan: plan }) end def self.find_or_create_customer(card_token:, customer_id:, email:) if customer_id stripe_customer = Stripe::Customer.retrieve({ id: customer_id }) if stripe_customer stripe_customer = Stripe::Customer.update(stripe_customer.id, { source: card_token}) end else stripe_customer = Stripe::Customer.create({ email: email, source: card_token }) end stripe_customer end

Наконец, в функции execute в том же файле ( app/services/orders/stripe.rb ) мы сначала вызовем find_or_create_customer а затем выполним подписку, вызвав execute_subscription , передав предыдущего полученного/созданного клиента. Итак, замените комментарий #SUBSCRIPTIONS WILL BE HANDLED HERE в методе execute следующим кодом:

 customer = self.find_or_create_customer(card_token: order.token, customer_id: user.stripe_customer_id, email: user.email) if customer user.update(stripe_customer_id: customer.id) order.customer_id = customer.id charge = self.execute_subscription(plan: product.stripe_plan_name, customer: customer)

Шаг 5: Протестируйте свое приложение.

Посетите свой веб-сайт, выберите продукт подписки Premium Plan и заполните действующую тестовую карту. После отправки он должен перенаправить вас на успешную страницу. Кроме того, проверьте на панели инструментов Stripe, успешно ли создана подписка.

Конфигурация PayPal

Как и в Stripe, мы также добавим гем для использования PayPal API: paypal-sdk-rest , также потребуется создание учетной записи PayPal. С описанием рабочего процесса PayPal с использованием этого драгоценного камня можно ознакомиться в официальной документации PayPal API.

Шаг 1: Добавьте гем paypal-sdk-rest в свой проект.

Добавьте это в Gemfile :

 gem 'paypal-sdk-rest'

Бегать:

 bundle install

Шаг 2: Создайте ключи API.

Чтобы иметь ключи API для связи с PayPal, вам необходимо создать учетную запись PayPal. Так:

  • Создайте учетную запись (или используйте свою учетную запись PayPal) на странице https://developer.paypal.com/.
  • Все еще войдя в свою учетную запись, создайте две учетные записи песочницы на странице https://developer.paypal.com/developer/accounts/:
    • Личный (аккаунт покупателя) — он будет использоваться в ваших тестах для осуществления платежей и подписок.
    • Бизнес (аккаунт продавца) — это будет связано с приложением, в котором будут ключи API, которые мы ищем. Кроме того, все транзакции можно отслеживать в этом аккаунте.
  • Создайте приложение на странице https://developer.paypal.com/developer/applications, используя предыдущую учетную запись бизнес-песочницы.
  • После этого шага вы получите два ключа для PayPal: Client ID и Secret .
  • В config/application.yml замените YOUR_CREDENTIAL_HERE из PAYPAL_CLIENT_ID и PAYPAL_CLIENT_SECRET ключами, которые вы только что получили.

Шаг 3: Инициализируйте модуль PayPal.

Как и в случае с Stripe, помимо замены ключей в application.yml , нам еще нужно инициализировать модуль PayPal, чтобы он мог использовать ключи, уже установленные в нашей переменной ENV . Для этого создайте файл в config/initializers/paypal.rb с:

 PayPal::SDK.configure( mode: ENV['PAYPAL_ENV'], client_id: ENV['PAYPAL_CLIENT_ID'], client_secret: ENV['PAYPAL_CLIENT_SECRET'], ) PayPal::SDK.logger.level = Logger::INFO

Шаг 4: Интегрируйте PayPal во внешний интерфейс.

В index.html.haml добавьте это в начало файла:

 %script(src="https://www.paypal.com/sdk/js?client-id=#{ENV['PAYPAL_CLIENT_ID']}")

В отличие от Stripe, PayPal использует только кнопку, при нажатии которой открывается безопасное всплывающее окно, в котором пользователь может войти в систему и перейти к оплате/подписке. Эту кнопку можно отобразить, вызвав метод paypal.Button(PARAM1).render(PARAM2) .

  • PARAM1 — это объект с конфигурацией среды и двумя функциями обратного вызова в качестве свойств: createOrder и onApprove .
  • PARAM2 указывает идентификатор элемента HTML, к которому должна быть прикреплена кнопка PayPal.

Итак, все еще в том же файле, замените прокомментированный код YOUR PAYPAL CODE WILL BE HERE на:

 (function setupPaypal() { function isPayment() { return $('[data-charges-and-payments-section] input[name="orders[product_id]"]:checked').length } function submitOrderPaypal(chargeID) { var $form = $("#order-details"); // Add a hidden input orders[charge_id] $form.append($('<input type="hidden" name="orders[charge_id]"/>').val(chargeID)); // Set order type $('#order-type').val('paypal'); $form.submit(); } paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { }, onApprove: function(data) { } }).render('#submit-paypal'); }());

Шаг 5: Протестируйте свое приложение.

Посетите свою страницу и проверьте, отображается ли кнопка PayPal, когда вы выбираете PayPal в качестве способа оплаты.

Транзакции PayPal

Логика транзакций PayPal, в отличие от Stripe, немного сложнее, поскольку включает в себя больше запросов, исходящих от внешнего интерфейса к серверному. Для этого существует этот раздел. Я объясню более или менее (без какого-либо кода), как будут реализованы функции, описанные в createOrder и onApprove , а также что ожидается от внутренних процессов.

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

Всплывающее окно PayPal, состояние загрузки
Всплывающее окно PayPal, состояние загрузки

Шаг 2: В этой функции мы будем выполнять запрос к нашему серверу, который создаст платеж/подписку. Это самое начало транзакции, и комиссия еще не взимается, поэтому транзакция фактически находится в состоянии ожидания . Наш сервер должен вернуть нам токен, который будет сгенерирован с помощью модуля PayPal (предоставляется через гем paypal-rest-sdk ).

Шаг 3: Все еще в createOrder , мы возвращаем этот токен, сгенерированный в нашем бэкэнде, и если все в порядке, всплывающее окно PayPal отобразит следующее, запрашивая учетные данные пользователя:

Всплывающее окно PayPal, учетные данные пользователя
Всплывающее окно PayPal, учетные данные пользователя

Шаг 4: После того, как пользователь войдет в систему и выберет способ оплаты, всплывающее окно изменит свое состояние на следующее:

Всплывающее окно PayPal, авторизованная транзакция
Всплывающее окно PayPal, авторизованная транзакция

Шаг 5: теперь вызывается обратный вызов функции onApprove . Мы определили его следующим образом: onApprove: function(data) . Объект data будет иметь платежную информацию для его выполнения. В этом обратном вызове будет выполнен еще один запрос к нашей серверной функции, на этот раз с передачей объекта данных для выполнения заказа PayPal.

Шаг 6: Наш сервер выполняет эту транзакцию и возвращает 200 (в случае успеха).

Шаг 7: Когда наша серверная часть возвращается, мы отправляем форму. Это третий запрос, который мы делаем к нашему серверу.

Обратите внимание, что, в отличие от Stripe, в этом процессе к нашему серверу делается три запроса. И мы будем синхронизировать статус записи нашего заказа соответствующим образом:

  • Обратный вызов createOrder : создается транзакция, а также создается запись заказа; поэтому по умолчанию он находится в состоянии ожидания .
  • Обратный вызов onApprove : транзакция выполнена, и наш заказ будет установлен как paypal_executed .
  • Страница заказа отправлена: Транзакция уже была выполнена, поэтому ничего не меняется. Запись заказа изменит свое состояние на оплаченное .

Весь этот процесс описан на следующем графике:

PayPal-транзакции
PayPal-транзакции

PayPal Платежи

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

Шаг 1: Создайте новые маршруты для PayPal и выполните платежи.

Add the following routes in config/routes.rb :

 post 'orders/paypal/create_payment' => 'orders#paypal_create_payment', as: :paypal_create_payment post 'orders/paypal/execute_payment' => 'orders#paypal_execute_payment', as: :paypal_execute_payment

This will create two new routes for creating and executing payments which will be handled in the paypal_create_payment and paypal_execute_payment orders controller methods.

Step 2: Create the PayPal service.

Add the singleton class Orders::Paypal at: app/services/orders/paypal.rb .

This service will initially have three responsibilities:

  • The create_payment method creates a payment by calling PayPal::SDK::REST::Payment.new . A token is generated and returned to the front-end.
  • The execute_payment method executes the payment by first finding the previous created payment object through PayPal::SDK::REST::Payment.find(payment_id) which uses the payment_id as an argument which has the same value as the charge_id stored in the previous step in the order object. After that, we call execute in the payment object with a given payer as the parameter. This payer is given by the front end after the user has provided credentials and selected a payment method in the popup.
  • The finish method finds an order by a specific charge_id querying for recently created orders in the paypal_executed state. If a record is found, it is marked as paid.
 class Orders::Paypal def self.finish(charge_id) order = Order.paypal_executed.recently_created.find_by(charge_id: charge_id) return nil if order.nil? order.set_paid order end def self.create_payment(order:, product:) payment_price = (product.price_cents/100.0).to_s currency = "USD" payment = PayPal::SDK::REST::Payment.new({ intent: "sale", payer: { payment_method: "paypal" }, redirect_urls: { return_url: "/", cancel_url: "/" }, transactions: [{ item_list: { items: [{ name: product.name, sku: product.name, price: payment_price, currency: currency, quantity: 1 } ] }, amount: { total: payment_price, currency: currency }, description: "Payment for: #{product.name}" }] }) if payment.create order.token = payment.token order.charge_id = payment.id return payment.token if order.save end end def self.execute_payment(payment_id:, payer_id:) order = Order.recently_created.find_by(charge_id: payment_id) return false unless order payment = PayPal::SDK::REST::Payment.find(payment_id) if payment.execute( payer_id: payer_id ) order.set_paypal_executed return order.save end end

Step 3: Call the PayPal service in the controller in the submit action.

Add a callback for prepare_new_order before the action paypal_create_payment (which will be added in the next step) is requested by adding the following in the file app/controllers/orders_controller.rb :

 class OrdersController < ApplicationController before_action :authenticate_user! before_action :prepare_new_order, only: [:paypal_create_payment] ...

Again, in the same file, call PayPal service in the submit action by replacing the commented code #PAYPAL WILL BE HANDLED HERE. со следующим:

 ... elsif order_params[:payment_gateway] == "paypal" @order = Orders::Paypal.finish(order_params[:token]) end ...

Step 4: Create the actions for handling requests.

Still, in the app/controllers/orders_controller.rb file, create two new actions (which should be public) for handling requests to paypal_create_payment and paypal_execute_payment routes:

  • The paypal_create_payment method: Will call our service method create_payment . If that returns successfully, it will return the order token created by Orders::Paypal.create_payment .
  • The paypal_execute_payment method: Will call our service method execute_payment (which executes our payments). If the payment is performed successfully, it returns 200.
 ... def paypal_create_payment result = Orders::Paypal.create_payment(order: @order, product: @product) if result render json: { token: result }, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end def paypal_execute_payment if Orders::Paypal.execute_payment(payment_id: params[:paymentID], payer_id: params[:payerID]) render json: {}, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end ...

Step 5: Implement the front-end callback functions for createOrder and onApprove .

Make your paypal.Button.render call look like this:

 paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { $('#order-type').val("paypal"); if (isPayment()) { return $.post("#{paypal_create_payment_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } else { } }, onApprove: function(data) { if (isPayment()) { return $.post("#{paypal_execute_payment_url}", { paymentID: data.paymentID, payerID: data.payerID }).then(function() { submitOrderPaypal(data.paymentID) }); } else { } } }).render('#submit-paypal');

As mentioned in the previous section, we call paypal_create_payment_url for the createOrder callback and paypal_execute_payment_url for the onApprove callback. Notice that if the last request returns success, we submit the order, which is the third request made to the server.

In the createOrder function handler, we return a token (obtained from the back end). In the onApprove callback, we have two properties passed down to our back-end paymentID and payerID . These will be used in order to execute the payment.

Finally, notice that we have two empty else clauses as I'm leaving room for the next section where we will be adding PayPal subscriptions.

If you visit your page after integrating the front-end JavaScript section and select PayPal as the payment method, it should look like the following:

Index page after integration with PayPal
Index page after integration with PayPal

Step 6: Test your application.

  • Посетите главную страницу.
  • Выберите продукт оплаты/списания и PayPal в качестве способа оплаты.
  • Нажмите на кнопку отправки PayPal.
  • Во всплывающем окне PayPal:
    • Используйте учетные данные созданной вами учетной записи покупателя.
    • Войдите и подтвердите свой заказ.
    • Всплывающее окно должно закрыться.
  • Проверьте, не перенаправлены ли вы на страницу успеха.
  • Наконец, проверьте, был ли заказ выполнен в учетной записи PayPal, войдя в свою бизнес-аккаунт по адресу https://www.sandbox.paypal.com/signin и проверив панель инструментов https://www.sandbox.paypal.com/listing. /транзакции.

Подписки PayPal

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

Мы будем использовать поле product paypal_plan_name для хранения идентификатора плана, предоставленного PayPal. В этом случае, в отличие от Stripe, мы не выбираем идентификатор, а PayPal возвращает это значение, которое будет использоваться для обновления последнего продукта, созданного в нашей базе данных.

Для создания подписки ни на одном этапе не требуется никакой информации о customer , так как метод onApprove , вероятно, обрабатывает эту связь в своей базовой реализации. Так что наши столы останутся прежними.

Шаг 1. Создайте план с помощью PayPal API.

Откройте консоль с помощью команды rails c . Создайте подписку для своей учетной записи PayPal с помощью:

 plan = PayPal::SDK::REST::Plan.new({ name: 'Premium Plan', description: 'Premium Plan', type: 'fixed', payment_definitions: [{ name: 'Premium Plan', type: 'REGULAR', frequency_interval: '1', frequency: 'MONTH', cycles: '12', amount: { currency: 'USD', value: '100.00' } }], merchant_preferences: { cancel_url: 'http://localhost:3000/', return_url: 'http://localhost:3000/', max_fail_attempts: '0', auto_bill_amount: 'YES', initial_fail_amount_action: 'CONTINUE' } }) plan.create plan_update = { op: 'replace', path: '/', value: { state: 'ACTIVE' } } plan.update(plan_update)

Шаг 2: Обновите последний продукт в базе данных paypal_plan_name с возвращенным plan.id .

Бегать:

 Product.last.update(paypal_plan_name: plan.id)

Шаг 3: Добавьте маршруты для подписки PayPal.

Добавьте два новых маршрута в config/routes.rb :

 post 'orders/paypal/create_subscription' => 'orders#paypal_create_subscription', as: :paypal_create_subscription post 'orders/paypal/execute_subscription' => 'orders#paypal_execute_subscription', as: :paypal_execute_subscription

Шаг 4. Обработайте создание и выполнение в службе PayPal.

Добавьте еще две функции для создания и выполнения подписок в Orders::Paypal app/services/orders/paypal.rb :

 def self.create_subscription(order:, product:) agreement = PayPal::SDK::REST::Agreement.new({ name: product.name, description: "Subscription for: #{product.name}", start_date: (Time.now.utc + 1.minute).iso8601, payer: { payment_method: "paypal" }, plan: { id: product.paypal_plan_name } }) if agreement.create order.token = agreement.token return agreement.token if order.save end end def self.execute_subscription(token:) order = Order.recently_created.find_by(token: token) return false unless order agreement = PayPal::SDK::REST::Agreement.new agreement.token = token if agreement.execute order.charge_id = agreement.id order.set_paypal_executed return order.charge_id if order.save end end

В create_subscription мы инициализируем соглашение, вызывая метод PayPal::SDK::REST::Agreement.new и передавая product.paypal_plan_name в качестве одного из его атрибутов. После этого мы создаем его, и теперь для этого последнего объекта будет установлен токен. Мы также возвращаем токен на внешний интерфейс.

В execute_subscription мы находим запись order , созданную в предыдущем вызове. После этого инициализируем новое соглашение, устанавливаем токен этого предыдущего объекта и выполняем его. Если этот последний шаг выполнен успешно, статус заказа устанавливается на paypal_executed . И теперь мы возвращаем во внешний интерфейс идентификатор соглашения, который также хранится в order.chager_id .

Шаг 5: Добавьте действия для создания и выполнения подписок в orders_controller .

Измените app/controllers/orders_controller.rb . Сначала в верхней части класса, а затем обновите обратный вызов prepare_new_order , чтобы он также выполнялся до paypal_create_subscription :

 class OrdersController < ApplicationController before_action :authenticate_user! before_action :prepare_new_order, only: [:paypal_create_payment, :paypal_create_subscription]

Кроме того, в том же файле добавьте две общедоступные функции, чтобы они вызывали службу Orders::Paypal с таким же потоком, как у нас уже есть в платежах PayPal:

 ... def paypal_create_subscription result = Orders::Paypal.create_subscription(order: @order, product: @product) if result render json: { token: result }, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end def paypal_execute_subscription result = Orders::Paypal.execute_subscription(token: params[:subscriptionToken]) if result render json: { id: result}, status: :ok else render json: {error: FAILURE_MESSAGE}, status: :unprocessable_entity end end ...

Шаг 6: Добавление обработчиков подписки для обратных вызовов createOrder и onApprove во внешнем интерфейсе.

Наконец, в index.html.haml замените функцию paypal.Buttons на следующую, которая заполнит два пустых места else которые у нас были раньше:

 paypal.Buttons({ env: "#{ENV['PAYPAL_ENV']}", createOrder: function() { $('#order-type').val("paypal"); if (isPayment()) { return $.post("#{paypal_create_payment_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } else { return $.post("#{paypal_create_subscription_url}", $('#order-details').serialize()).then(function(data) { return data.token; }); } }, onApprove: function(data) { if (isPayment()) { return $.post("#{paypal_execute_payment_url}", { paymentID: data.paymentID, payerID: data.payerID }).then(function() { submitOrderPaypal(data.paymentID) }); } else { return $.post("#{paypal_execute_subscription_url}", { subscriptionToken: data.orderID }).then(function(executeData) { submitOrderPaypal(executeData.id) }); } } }).render('#submit-paypal');

Создание и выполнение подписок имеет ту же логику, что и для платежей. Одно отличие состоит в том, что при выполнении платежей данные из функции обратного вызова onApprove уже имеют paymentID , представляющий charge_id для отправки формы через submitOrderPaypal(data.paymentID) . Для подписок мы получаем charge_id только после его выполнения, запрашивая POST на paypal_execute_subscription_url , поэтому мы можем вызвать submitOrderPaypal(executeData.id) .

Шаг 7: Протестируйте свое приложение.

  • Посетите главную страницу.
  • Выберите продукт по подписке и PayPal в качестве способа оплаты.
  • Нажмите на кнопку отправки PayPal.
  • Во всплывающем окне PayPal:
    • Используйте учетные данные созданной вами учетной записи покупателя.
    • Войдите и подтвердите свой заказ.
    • Всплывающее окно должно закрыться.
  • Проверьте, не перенаправлены ли вы на страницу успеха.
  • Наконец, проверьте, был ли заказ выполнен в учетной записи PayPal, войдя в свой бизнес-аккаунт на странице https://www.sandbox.paypal.com/signin и проверив панель инструментов https://www.sandbox.paypal.com/listing/. транзакции.

Заключение

После прочтения этой статьи вы сможете интегрировать платежи/сборы, а также транзакции подписки для PayPal и Stripe в свое приложение Rails. Есть много моментов, которые можно было бы улучшить, которые я не добавил в эту статью для краткости. Я организовал все, исходя из предположения о сложности:

  • Полегче:
    • Используйте безопасность транспортного уровня (TLS), чтобы ваши запросы использовали HTTPS.
    • Реализуйте конфигурации производственной среды для PayPal и Stripe.
    • Добавьте новую страницу, чтобы пользователи могли получить доступ к истории предыдущих заказов.
  • Середина:
    • Возврат или отмена подписки.
    • Обеспечьте решение для незарегистрированных пользовательских платежей.
  • Сильнее:
    • Предоставьте способ удалить учетные записи и сохранить их токен и customer_id , если пользователь захочет вернуться. Но через определенное количество дней удалите эти данные, чтобы ваше приложение было более совместимым с PCI.
    • Перейдите на API PayPal версии 2 на стороне сервера (https://developer.paypal.com/docs/api/payments/v2/). Gem, который мы использовали в этом руководстве, paypal-sdk-rest имеет только бета-версию для версии. 2, поэтому его можно использовать с осторожностью (https://github.com/paypal/PayPal-Ruby-SDK/tree/2.0-beta).
    • Включить идемпотентные запросы.
      • Полоса: https://stripe.com/docs/api/idempotent_requests
      • PayPal: https://developer.paypal.com/docs/api-basics/#api-idempotency

Я также рекомендую прочитать об элементе Stripe Checkout, который является еще одним способом интеграции Stripe во внешний интерфейс. В отличие от Stripe Elements, которые мы использовали в этом руководстве, Stripe Checkout открывает всплывающее окно после нажатия кнопки (аналогично PayPal), где пользователь вводит информацию о кредитной карте ИЛИ выбирает оплату через Google Pay/Apple Pay https://stripe.com /документы/веб.

Вторая рекомендация для чтения — это страницы безопасности для обоих платежных шлюзов.

  • Для полосы
  • Для PayPal

Наконец, спасибо за чтение этой статьи! Вы также можете проверить мой проект GitHub, используемый для этого примера проекта. Там я также добавил тесты rspec во время разработки.