在Ruby on Rails中集成Stripe和PayPal付款方式

本文概述

对于诸如AliExpress, Ebay和Amazon之类的大型电子商务公司而言, 一项关键功能是一种安全的付款方式, 这对他们的业务至关重要。如果此功能失败, 后果将是灾难性的。这适用于从事电子商务应用程序的行业领导者和Ruby on Rails开发人员。

网络安全对于防止攻击至关重要, 而使交易过程更加安全的一种方法是要求第三方服务来处理它。在应用程序中包括支付网关是实现此目标的一种方法, 因为它们提供了用户授权, 数据加密和仪表板, 因此你可以即时跟踪交易状态。

Web上有多种支付网关服务, 但是在本文中, 我将重点介绍将Stripe和PayPal集成到Rails应用程序中。再说几个:Amazon Payments, Square, SecurePay, WorldPay, Authorize.Net, 2Checkout.com, Braintree, Amazon或BlueSnap。

付款网关集成如何工作

涉及支付网关的交易的一般表示

涉及支付网关的交易的一般表示

通常, 你的应用程序中将有一个表单/按钮, 用户可以在其中登录/插入信用卡数据。 PayPal和Stripe已经通过使用iframe表单或弹出窗口使第一步更加安全, 这些表单或弹出窗口可防止你的应用存储敏感的用户信用卡信息, 因为它们将返回代表此交易的令牌。有些用户可能已经知道第三方服务正在处理交易过程, 因此已经对处理付款更加有信心, 因此这也可能对你的应用程序有吸引力。

验证用户信息后, 支付网关将通过联系与银行进行通信的支付处理器来确认付款, 以确认付款。这确保了交易被适当地借记/贷记。

Stripe使用信用卡表格询问信用卡号, 简历和有效期。因此, 用户必须在安全的Stripe输入中填写信用卡信息。提供此信息后, 你的应用程序后端将通过令牌处理此付款。

与Stripe不同, PayPal将用户重定向到PayPal登录页面。用户通过PayPal授权并选择付款方式, 同样, 你的后端将处理令牌而不是用户敏感数据。

值得一提的是, 对于这两个支付网关, 你的后端应要求通过Stripe或PayPal API进行交易执行, 这将给出OK / NOK响应, 因此你的应用程序应将用户相应地重定向到成功页面或错误页面。

本文的目的是提供一个快速指南, 以将这两个支付网关集成在单个应用程序中。对于所有测试, 我们将使用Stripe和PayPal提供的沙盒和测试帐户来模拟付款。

设定

在集成支付网关之前, 我们将通过添加gem, 数据库表和索引页面进行设置以初始化应用程序。该项目是使用Rails版本5.2.3和Ruby 2.6.3创建的。

注意:你可以在我们最近的文章中查看Rails 6的新功能。

步骤1:初始化Rails应用程序。

通过使用带有你的应用程序名称的rails命令运行项目初始化来初始化项目:

rails new YOUR_APP_NAME

并在你的应用程序文件夹中cd。

步骤2:安装gem。

除了Stripe和PayPal宝石外, 还添加了其他一些宝石:

  • 设计:用于用户身份验证和授权
  • haml:用于呈现用户页面的模板工具
  • jquery-rails:用于前端脚本中的jquery
  • money-rails:用于显示格式化的货币值

添加到你的Gemfile:

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

添加后, 在你的CLI中运行:

bundle install

步骤3:初始化gem。

除了通过捆绑包安装这些宝石外, 其中一些宝石还需要初始化。

安装装置:

rails g devise:install

初始化钱轨:

rails g money_rails:initializer

通过将以下内容附加到app / assets / javascripts / application.js的底部来初始化jquery-rails:

//= require jquery
//= require jquery_ujs

步骤4:表格和迁移

此项目的”用户”, “产品”和”订单”中将使用三个表。

  • 用户:将通过设计生成
  • 产品栏:
    • 名称
    • price_cents
    • Stripe_plan_name:一个ID, 表示在Stripe中创建的订阅计划, 因此用户可以订阅它。仅与Stripe计划关联的产品才需要此字段。
    • paypal_plan_name:与stripe_plan_name相同, 但适用于PayPal
  • 订单栏:
    • product_id
    • 用户身份
    • 状态:这将通知订单是待处理, 失败还是已付款。
    • 令牌:这是从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

用于创建订单表。通过运行以下命令来生成迁移:

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:创建模型。

用户模型已经通过devise安装创建, 并且不需要对其进行任何更改。除此之外, 将为产品和订单创建两个模型。

产品。添加一个新文件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:填充数据库。

一个用户和两个产品将在控制台中创建。订单记录将根据付款测试创建。

  • 滑轨
  • 在浏览器中, 访问http:// localhost:3000
  • 你将被重定向到注册页面。
  • 通过填写用户的电子邮件地址和密码来注册用户。
  • 在你的终端中, 将提示以下日志, 显示你在数据库中创建了一个用户:
User Create (0.1ms)  INSERT INTO "users" ("email", "encrypted_password", "created_at", "updated_at") VALUES (?, ?, ?, ?) …
  • 通过运行rails c并添加以下内容来创建两个没有订阅的产品:
    • Product.create(名称:” Awesome T-Shirt”, price_cents:3000)
    • Product.create(名称:” Awesome Sneakers”, price_cents:5000)

步骤7:建立索引页面

该项目的主页包括用于购买或订阅的产品选择。此外, 它还有一个用于选择付款方式的部分(“Stripe”或”PayPal”)。对于每种付款网关类型, 还使用一个提交按钮, 因为对于PayPal, 我们将通过其JavaScript库添加其自己的按钮设计。

首先, 为索引创建路由, 然后在config / routes.rb中提交。

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

创建并添加操作索引, 然后在订单控制器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中创建一个文件。该文件包含我们将通过Submit方法发送到后端的所有输入, 以及支付网关和产品选择的交互。以下是一些输入名称属性:

  • Orders [product_id]存储产品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运行应用程序, 并访问http:// localhost:3000中的页面。你应该能够看到如下页面:

不带Stripe和PayPal集成的原始索引页

不带Stripe和PayPal集成的原始索引页

支付网关凭证存储

PayPal和Stripe密钥将存储在Git无法跟踪的文件中。每个付款网关在此文件中存储的密钥有两种, 现在, 我们将为其使用伪值。有关创建这些密钥的其他说明, 请参见其他章节。

步骤1:在.gitignore中添加它。

/config/application.yml

步骤2:使用config / application.yml中的凭据创建一个文件。它应包含所有用于访问这些API的PayPal和Stripe沙箱/测试键。

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中的变量, 请将这些行添加到Application类内的config / application.rb中, 以便它们在ENV中可用。

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

Stripe配置

我们将添加一个使用Stripe API的工具:stripe-rails。还需要创建一个Stripe帐户, 以便可以处理费用和订阅。如果需要, 可以在官方文档中查阅Stripe API的API方法。

步骤1:将stripe-rails gem添加到你的项目中。

stripe-rails gem将为该项目中使用的所有API请求提供一个接口。

将此添加到Gemfile中:

gem 'stripe-rails'

运行:

bundle install

第2步:生成你的API密钥。

为了拥有用于与Stripe通信的API密钥, 你将需要在Stripe中创建一个帐户。要测试应用程序, 可以使用测试模式, 因此在Stripe帐户创建过程中无需填写真实的业务信息。

  • 如果你没有, 请在Stripe中创建一个帐户(https://dashboard.stripe.com/)。
  • 仍在Stripe仪表板中时, 登录后, 打开”查看测试数据”。
  • 在https://dashboard.stripe.com/test/apikeys上, 将/config/application.yml中值STRIPE_PUBLISHABLE_KEY和STRIPE_SECRET_KEY的YOUR_CREDENTIAL_HERE替换为可发布密钥和秘密密钥中的内容。

步骤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提供)将Stripe javascript库添加到用户页面。

=  stripe_javascript_tag

Stripe使用通过其API创建的安全输入字段。由于它们是通过此API在iframe中创建的, 因此你无需担心可能会存在处理用户信用卡信息的漏洞。此外, 你的后端将无法处理/存储任何用户敏感数据, 并且只会收到代表此信息的令牌。

这些输入字段是通过调用stripe.elements()。create(‘card’)创建的。之后, 只需要通过将输入要安装到的HTML元素id / class作为参数传递, 就可以用mount()调用返回的对象。可以在Stripe中找到更多信息。

当用户使用Stripe付款方法点击Submit按钮时, 将在创建的Stripe卡元素上执行另一个返回承诺的API调用:

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

如果未分配属性错误, 则此函数的结果变量将具有一个令牌, 可以通过访问属性result.token.id来检索该令牌。该令牌将发送到后端。

为了进行这些更改, 请在index.html.haml中将注释的代码// //替换为你的Stripe和PayPal代码:

  (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)填写信用卡表格, 然后提交页面。检查是否在服务器输出中使用所有参数(product_id, payment_gateway和令牌)调用了commit操作。

Stripe收费

Stripe收费代表一次性交易。因此, 在进行Stripe收费交易后, 你将直接从客户那里收到钱。这是销售与计划无关的产品的理想选择。在下一节中, 我将展示如何使用PayPal进行相同的交易类型, 但是PayPal的这种交易类型名称为Payment。

在本节中, 我还将提供用于处理和提交订单的所有框架。提交Stripe表单时, 我们在Submit动作中创建一个订单。该订单最初将处于待处理状态, 因此, 如果在处理该订单时出现任何问题, 该订单仍将待处理。

如果Stripe API调用出现任何错误, 我们会将订单设置为失败状态, 并且如果成功完成收费, 则它将处于已付款状态。还根据Stripe API响应重定向用户, 如下图所示:

条纹交易。

Stripe交易。

此外, 执行Stripe充电时, 将返回ID。我们将存储此ID, 以便你以后可以根据需要在Stripe仪表板中查找它。如果必须退还订单, 也可以使用此ID。本文不会探讨这种事情。

步骤1:创建Stripe服务。

我们将使用Singleton类通过Stripe API表示Stripe操作。为了创建费用, 调用Stripe :: Charge.create方法, 并将返回的对象ID属性存储在订单记录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:实施Submit操作并调用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订阅

可以创建用于定期付款的订阅或计划。使用这种类型的产品, 将根据计划配置自动向用户每天, 每周, 每月或每年收费。在本部分中, 我们将使用产品stripe_plan_name的字段来存储计划ID(实际上, 我们可以选择ID, 我们将其称为premium-plan), 该字段将用于创建计划ID。关系客户<->订阅。

我们还将为用户表创建一个名为stripe_customer_id的新列, 其中将填充Stripe客户对象的id属性。调用功能Stripe :: Customer.create函数时会创建一个Stripe客户, 你还可以在(https://dashboard.stripe.com/test/customers)中检查创建并链接到你帐户的客户。通过传递源参数来创建客户, 在本例中, 该源参数是在前端提交时提交的表单中发送的令牌。

从最后提到的Stripe API调用获得的客户对象也用于创建预订, 该预订通过调用customer.subscriptions.create并将计划ID作为参数传递来完成。

此外, stripe-rails gem提供了从Stripe检索和更新客户的接口, 这可以通过分别调用Stripe :: Customer.retrieve和Stripe :: Customer.update来完成。

因此, 当用户记录已经具有stripe_customer_id时, 我们将使用Stripe_customer_id作为参数, 然后通过Stripe :: Customer.update调用Stripe :: Customer.retrieve, 而不是使用Stripe :: Customer.create创建新客户。 , 在这种情况下, 将令牌传递给参数。

首先, 我们将使用Stripe API创建计划, 以便可以使用stripe_plan_name字段创建新的订阅产品。之后, 我们将在orders_controller和Stripe服务中进行修改, 以便处理Stripe订阅的创建和执行。

步骤1:使用Stripe API创建计划。

使用命令栏c打开控制台。使用以下步骤为你的Stripe帐户创建订阅:

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

如果在此步骤中返回的结果为true, 则表示该计划已成功创建, 你可以在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。

在终端中运行以下命令:

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

最后, 在同一文件(app / services / orders / stripe.rb)中的execute函数中, 我们将首先调用find_or_create_customer, 然后通过传递先前检索/创建的客户来调用execute_subscription来执行订阅。因此, 用以下代码替换execute方法中将在此处处理的#SUBSCRIPTIONS注释:

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:测试你的应用程序。

访问你的网站, 选择订阅产品高级计划, 然后填写有效的测试卡。提交后, 它应将你重定向到成功的页面。此外, 请在你的Stripe仪表板中检查是否已成功创建订阅。

PayPal配置

与在Stripe中所做的一样, 我们还将添加一个使用PayPal API的工具:paypal-sdk-rest, 并且还需要创建一个PayPal帐户。可以在官方的PayPal API文档中查阅使用此gem的PayPal描述性工作流。

步骤1:将paypal-sdk-rest gem添加到你的项目中。

将此添加到Gemfile中:

gem 'paypal-sdk-rest'

运行:

bundle install

第2步:生成你的API密钥。

为了拥有用于与PayPal通信的API密钥, 你将需要创建一个PayPal帐户。所以:

  • 在https://developer.paypal.com/上创建一个帐户(或使用你的PayPal帐户)。
  • 仍然登录到你的帐户, 在https://developer.paypal.com/developer/accounts/创建两个沙箱帐户:
    • 个人(买方帐户)–将在你的测试中使用该帐户进行付款和订阅。
    • 商业(商家帐户)–这将链接到应用程序, 该应用程序将具有我们要查找的API密钥。除此之外, 所有交易都可以在该帐户中进行。
  • 使用以前的业务沙箱帐户在https://developer.paypal.com/developer/applications上创建一个应用程序。
  • 完成此步骤后, 你将收到PayPal的两个密钥:客户端ID和密钥。
  • 在config / application.yml中, 将PAYPAL_CLIENT_ID和PAYPAL_CLIENT_SECRET中的YOUR_CREDENTIAL_HERE替换为你刚收到的密钥。

步骤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指示应将PayPal按钮附加到的HTML元素标识符。

因此, 仍在同一个文件中, 将注释代码替换为:

  (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交易

与Stripe不同, PayPal(PayPal)交易的逻辑要复杂一些, 因为它涉及从前端到后端发起的更多请求。这就是为什么存在此部分的原因。我将或多或少地(没有任何代码)解释createOrder和onApprove方法中描述的功能将如何实现, 以及后端过程中的期望。

步骤1:当用户单击PayPal提交按钮时, 要求用户凭据的PayPal弹出窗口将打开, 但处于加载状态。函数回调createOrder被调用。

PayPal弹出窗口,加载状态

PayPal弹出窗口, 加载状态

步骤2:在此功能中, 我们将向后端执行请求, 这将创建一个付款/订阅。这是交易的开始, 尚不收取费用, 因此交易实际上处于待处理状态。我们的后端应返回一个令牌, 该令牌将使用PayPal模块(通过paypal-rest-sdk gem提供)生成。

步骤3:仍在createOrder回调中, 我们返回在后端生成的此令牌, 如果一切正常, 则PayPal弹出窗口将呈现以下内容, 要求用户提供凭据:

贝宝弹出窗口,用户凭证

PayPal弹出窗口, 用户凭证

步骤4:在用户登录并选择付款方式后, 弹出窗口会将其状态更改为以下内容:

贝宝弹出窗口,授权交易

PayPal弹出窗口, 授权交易

步骤5:现在调用onApprove函数回调。我们将其定义如下:onApprove:函数(数据)。数据对象将具有付款信息以便执行。在此回调中, 这次将传递数据对象以执行PayPal订单, 这是对后端功能的另一个请求。

步骤6:我们的后端执行此事务并返回200(如果成功)。

步骤7:当后端返回时, 我们提交表单。这是我们对后端的第三个请求。

请注意, 与Stripe不同, 在此过程中, 我们向后端提出了三个请求。我们将相应地使订单记录状态保持同步:

  • createOrder回调:创建交易, 并创建订单记录;因此, 默认情况下它处于挂起状态。
  • onApprove回调:交易已执行, 我们的订单将设置为paypal_exected。
  • 提交订单页面:事务已经执行, 因此没有任何变化。订单记录会将其状态更改为已付款。

下图描述了整个过程:

贝宝交易

PayPal交易

PayPal付款

PayPal支付遵循与Stripe Charges相同的逻辑, 因此它们表示一次性交易, 但是如上一节所述, 它们具有不同的流逻辑。这些是处理PayPal付款所需的更改:

步骤1:为PayPal创建新路线并执行付款。

在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

这将创建两条用于创建和执行付款的新路线, 这些路线将在paypal_create_payment和paypal_execute_payment订单控制器方法中进行处理。

步骤2:创建PayPal服务。

在以下位置添加单例类Orders :: Paypal:app / services / orders / paypal.rb。

该服务最初将承担三项职责:

  • create_payment方法通过调用PayPal :: SDK :: REST :: Payment.new创建付款。令牌已生成并返回到前端。
  • execute_payment方法通过首先通过PayPal :: SDK :: REST :: Payment.find(payment_id)查找先前创建的付款对象来执行付款, 该对象使用payment_id作为参数, 其值与上一步中存储的charge_id相同在订单对象中。之后, 我们在给定付款人作为参数的付款对象中调用execute。在用户提供凭据并在弹出窗口中选择付款方式后, 前端会给此付款人。
  • finish方法通过特定的charge_id查找订单, 以查询最近创建的处于paypal_exected状态的订单。如果找到记录, 则将其标记为已付款。
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

步骤3:在Submit操作中调用控制器中的PayPal服务。

通过在文件app / controllers / orders_controller.rb中添加以下内容, 在请求paypal_create_payment操作(将在下一步中添加)之前添加prepare_new_order的回调:

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

同样, 在同一文件中, 通过替换注释的代码#PAYPAL将在此处处理, 在Submit操作中调用PayPal服务。具有以下内容:

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

步骤4:创建用于处理请求的操作。

仍然在app / controllers / orders_controller.rb文件中, 创建两个新操作(应该是公共的)来处理对paypal_create_payment和paypal_execute_payment路由的请求:

  • paypal_create_payment方法:将调用我们的服务方法create_payment。如果成功返回, 它将返回由Orders :: Paypal.create_payment创建的订单令牌。
  • paypal_execute_payment方法:将调用我们的服务方法execute_payment(执行我们的付款)。如果付款成功完成, 则返回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
...

步骤5:为createOrder和onApprove实现前端回调函数。

使你的paypal.Button.render调用看起来像这样:

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');

如上一节所述, 我们为createOrder回调调用paypal_create_payment_url, 为onApprove回调调用paypal_execute_payment_url。请注意, 如果最后一个请求返回成功, 我们将提交订单, 这是对服务器的第三个请求。

在createOrder函数处理程序中, 我们返回一个令牌(从后端获取)。在onApprove回调中, 我们有两个属性传递给后端的PaymentID和payerID。这些将用于执行付款。

最后, 请注意, 我们有两个空白的else子句, 因为我在下一节中将要添加PayPal订阅的地方留出了空间。

如果你在集成了前端JavaScript部分之后访问了你的页面, 并选择PayPal作为付款方式, 则其外观应如下所示:

与PayPal集成后的索引页

与PayPal集成后的索引页

步骤6:测试你的应用程序。

  • 访问索引页面。
  • 选择一种付款/收费产品, 然后选择PayPal作为付款方式。
  • 单击提交PayPal按钮。
  • 在PayPal弹出窗口中:
    • 使用你创建的买方帐户的凭据。
    • 登录并确认你的订单。
    • 弹出窗口应该关闭。
  • 检查是否将你重定向到成功页面。
  • 最后, 通过在https://www.sandbox.paypal.com/signin上登录你的企业帐户并检查仪表板https://www.sandbox.paypal.com/listing, 检查是否在PayPal帐户中执行了订单/交易。

PayPal订阅

PayPal计划/协议/订阅遵循与Stripe订阅相同的逻辑, 并创建用于定期付款。使用这种类型的产品, 用户会根据其配置自动每天, 每周, 每月或每年向其收费。

我们将使用产品paypal_plan_name的字段, 以存储PayPal提供的计划ID。在这种情况下, 与Stripe不同, 我们没有选择ID, 而PayPal会将此值返回到该值, 该值将用于更新数据库中最后创建的产品。

对于创建订阅, 任何步骤都不需要客户信息, 因为o​​nApprove方法可能会在其基础实现中处理此链接。因此, 我们的表将保持不变。

第1步:使用PayPal API创建计划。

使用命令栏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:使用返回的plan.id更新数据库paypal_plan_name中的最后一个产品。

运行:

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 :: App / services / orders / paypal.rb的Pays中添加两个用于创建和执行订阅的功能:

  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中, 我们找到在上一个调用中创建的订单记录。之后, 我们初始化一个新协议, 设置这个先前对象的令牌并执行它。如果最后一步成功执行, 则订单状态将设置为paypal_exected。现在我们返回到前端的协议ID, 该ID也存储在order.chager_id中。

步骤5:在orders_controller中添加用于创建和执行订阅的操作。

更改app / controllers / orders_controller.rb。首先, 在类的顶部, 然后在调用paypal_create_subscription之前更新回调prepare_new_order也要执行:

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函数替换为以下内容, 这将填补我们之前遇到的两个空白:

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的数据已经具有一个payloadID, 它表示负责通过submitOrderPaypal(data.paymentID)提交表单的charge_id。对于订阅, 仅在执行后通过在paypal_execute_subscription_url上请求POST才能获得charge_id, 因此我们可以调用submitOrderPaypal(executeData.id)。

步骤7:测试你的应用程序。

  • 访问索引页面。
  • 选择订阅产品, 然后选择PayPal作为付款方式。
  • 单击提交PayPal按钮。
  • 在PayPal弹出窗口中:
    • 使用你创建的买方帐户的凭据。
    • 登录并确认你的订单。
    • 弹出窗口应该关闭。
  • 检查是否将你重定向到成功页面。
  • 最后, 通过在https://www.sandbox.paypal.com/signin上使用你的企业帐户登录并检查仪表板https://www.sandbox.paypal.com/listing/, 来检查订单是否在PayPal帐户中执行了交易。

总结

阅读本文之后, 你应该能够在Rails应用程序中集成PayPal和Stripe的付款/收费以及订阅交易。为了简洁起见, 我在本文中未添加很多要改进的地方。我根据困难的假设组织了一切:

  • 更轻松:
    • 使用传输层安全性(TLS), 以便你的请求使用HTTPS。
    • 为PayPal和Stripe实施生产环境配置。
    • 添加一个新页面, 以便用户可以访问以前的订单的历史记录。
  • 介质:
    • 退款或取消订阅。
    • 提供非注册用户付款的解决方案。
  • 更难:
    • 如果用户希望回来, 提供一种删除帐户并保留其令牌和customer_id的方法。但是几天后, 请删除此数据, 以使你的应用程序更兼容PCI。
    • 移至服务器端的PayPal版本2 API(https://developer.paypal.com/docs/api/payments/v2/)我们在本教程中使用的gem paypal-sdk-rest, 仅具有beta版本2, 因此可以谨慎使用(https://github.com/paypal/PayPal-Ruby-SDK/tree/2.0-beta)。
    • 包括幂等请求。
      • Stripe: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进行支付/ docs / web。

第二读建议是两个支付网关的安全页面。

  • 对于Stripe
  • 对于PayPal

最后, 感谢你阅读本文!你还可以检查用于该项目示例的我的GitHub项目。在那里, 我在开发时也添加了rspec测试。

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?