本文概述
对于诸如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集成的原始索引页
支付网关凭证存储
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安全输入字段集成的索引页。
步骤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弹出窗口, 加载状态
步骤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集成后的索引页
步骤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会将此值返回到该值, 该值将用于更新数据库中最后创建的产品。
对于创建订阅, 任何步骤都不需要客户信息, 因为onApprove方法可能会在其基础实现中处理此链接。因此, 我们的表将保持不变。
第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测试。
评论前必须登录!
注册