Rails Stripe 订阅、Webhook 和 Payola
Rails Stripe Subscriptions, Webhooks, and Payola
我目前正在使用 rails-stripe-membership-saas 来设置我的 SaaS 站点。我目前在 Stripe 中设置了与 rails 相对应的计划,让用户注册某个计划,所有计划都有 14 天的试用期。
我需要帮助弄清楚如何不要求用户在注册时输入他们的信用卡,然后使用 Stripe webhook 向他们发送请求或以某种方式提醒用户他们需要输入信用卡继续他们的订阅,否则他们将无法重新登录。
每当我从表单中删除需要卡片的代码时,表单就会停止工作 - 即使它可以工作,我也不确定如何实施条纹 webhook,我相信我可能必须使用它事件条纹 gem for.
如果有人能指出正确的方向,我将不胜感激。如果需要任何其他 code/information,请告诉我,我一定会 post。
这是我用来注册用户的表格:
registrations/new.html.erb
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :role => 'form',
:class => 'payola-onestep-subscription-form',
'data-payola-base-path' => payola_path,
'data-payola-plan-type' => resource.plan.plan_class,
'data-payola-plan-id' => resource.plan.id}) do |f| %>
<div>
<span id="error_explanation" class="payola-payment-error"></span>
</div>
<div class="form-group">
<h3 class="text-center"><i>Try Free for 14 Days!</i></h3> <br>
<%= f.label 'Subscription plan' %>
<%= f.collection_select(:plan_id, Plan.all, :id, :name) %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, :autofocus => true, class: 'form-control', data: { payola: 'email' } %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="form-group">
<%= label_tag :card_number, "Credit Card Number" %>
<%= text_field_tag :card_number, nil, name: nil, class: 'form-control', data: { stripe: 'number' } %>
</div>
<div class="form-group">
<%= label_tag :card_code, "Card Security Code" %>
<%= text_field_tag :card_code, nil, name: nil, class: 'form-control', data: { stripe: 'cvc' } %>
</div>
<br />
<div class="form-group">
<%= label_tag :card_month, "Card Expiry" %>
<%= select_month nil, { use_two_digit_numbers: true}, { name: nil, data: { stripe: 'exp-month' } } %>
<%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+10}, { name: nil, data: { stripe: 'exp-year' } } %>
</div>
<div class="text-center">
<%= f.submit 'Sign up', :class => 'button right' %>
</div>
<% end %>
这是我目前制定的计划。
class CreatePlanService
def call
p1 = Plan.where(name: 'Yearly').first_or_initialize do |p|
p.amount = 36000
p.interval = 'year'
p.stripe_id = 'yearly'
end
p1.save!(:validate => false)
p2 = Plan.where(name: 'Monthly').first_or_initialize do |p|
p.amount = 3000
p.interval = 'month'
p.stripe_id = 'monthly'
end
p2.save!(:validate => false)
end
end
用户模型
class User < ActiveRecord::Base
enum role: [:user, :admin, :yearly, :monthly]
after_initialize :set_default_role, :if => :new_record?
after_initialize :set_default_plan, :if => :new_record?
# after_create :sign_up_for_mailing_list
belongs_to :plan
validates_associated :plan
has_many :dashboards
has_many :cardtools
def set_default_role
self.role ||= :user
end
def set_default_plan
self.plan ||= Plan.last
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
def sign_up_for_mailing_list
MailingListSignupJob.perform_later(self)
end
def subscribe
mailchimp = Gibbon::Request.new(api_key: Rails.application.secrets.mailchimp_api_key)
list_id = Rails.application.secrets.mailchimp_list_id
result = mailchimp.lists(list_id).members.create(
body: {
email_address: self.email,
status: 'subscribed'
})
Rails.logger.info("Subscribed #{self.email} to MailChimp") if result
end
end
注册控制器
class RegistrationsController < Devise::RegistrationsController
include Payola::StatusBehavior
before_action :cancel_subscription, only: [:destroy]
def new
build_resource({})
unless params[:plan].nil?
# If broken, follow console https://github.com/RailsApps/rails-stripe-membership-saas/issues/127
@plan = Plan.find_by!(stripe_id: params[:plan])
resource.plan = @plan
end
yield resource if block_given?
respond_with self.resource
end
def create
build_resource(sign_up_params)
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
resource.role = User.roles[plan.stripe_id] unless resource.admin?
resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_flashing_format?
sign_up(resource_name, resource)
subscribe
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
expire_data_after_sign_in!
subscribe
end
else
clean_up_passwords resource
render json:
{error: resource.errors.full_messages.to_sentence},
status: 400
end
end
def change_plan
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
unless plan == current_user.plan
role = User.roles[plan.stripe_id]
if current_user.update_attributes!(plan: plan, role: role)
subscription = Payola::Subscription.find_by!(owner_id: current_user.id)
Payola::ChangeSubscriptionPlan.call(subscription, plan)
redirect_to edit_user_registration_path, :notice => "Plan changed."
else
flash[:alert] = 'Unable to change plan.'
build_resource
render :edit
end
end
end
private
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation, :plan_id)
end
def subscribe
return if resource.admin?
params[:plan] = current_user.plan
subscription = Payola::CreateSubscription.call(params, current_user)
current_user.save
render_payola_status(subscription)
end
def cancel_subscription
subscription = Payola::Subscription.find_by!(owner_id: current_user.id, state: 'active')
Payola::CancelSubscription.call(subscription)
end
end
从该表单中删除卡片属性肯定会使它崩溃。
Payola 假定您想要创建一个立即收费订阅。这意味着免费计划和免费试用计划需要在注册时提供信用卡。所以解决这个问题可能是:
- 使用
stripe-rails
gem 而不是 payola 库。 Stripe 对于免费计划或试用计划不需要卡片。
或
- 您遵循 Payola 创建者对此的解决方法。 https://github.com/peterkeen/payola/commit/0b0282fb256eba7247999fbf2b33f2ded567c311。
虽然对你来说可能是 Head on Rock。
至于 webhooks,我认为这是可能的。当信用卡过期或月度交易被拒绝时,Stripe 会在失败后自动重试定期付款。经过多次尝试(在您的 Stripe 帐户设置中设置)后,Stripe 将取消订阅。您的应用程序需要知道拒绝具有过期帐户的订户访问。 Stripe 提供 webhooks 来向您传达事件(有关详细信息,请参阅 Stripe Webhooks Documentation)。
Stripe webhook 是从 Stripe 服务器到您网站的 HTTP 请求,包含 JSON 提供事件数据的数据,以及可用于从 Stripe 服务器检索数据的事件 ID。示例应用程序使用由 Danny Whalen 的 stripe_event
gem 提供的实现来响应 Stripe webhook,它随 Payola gem 一起提供。应用程序在 https://www.example.com/payola/events.
响应 webhook 请求
示例应用程序仅响应 “customer.subscription.deleted”
事件。您可以自定义应用程序以响应其他事件(例如发送感谢电子邮件以响应 “invoice.payment_succeeded”
事件)。
要使 webhook 正常工作,您必须通过 https://manage.stripe.com/#account/webhooks 访问您的 Stripe 控制面板并添加 URL 适用于您的应用程序,例如 https://www.example.com/payola/events.
我目前正在使用 rails-stripe-membership-saas 来设置我的 SaaS 站点。我目前在 Stripe 中设置了与 rails 相对应的计划,让用户注册某个计划,所有计划都有 14 天的试用期。
我需要帮助弄清楚如何不要求用户在注册时输入他们的信用卡,然后使用 Stripe webhook 向他们发送请求或以某种方式提醒用户他们需要输入信用卡继续他们的订阅,否则他们将无法重新登录。
每当我从表单中删除需要卡片的代码时,表单就会停止工作 - 即使它可以工作,我也不确定如何实施条纹 webhook,我相信我可能必须使用它事件条纹 gem for.
如果有人能指出正确的方向,我将不胜感激。如果需要任何其他 code/information,请告诉我,我一定会 post。
这是我用来注册用户的表格: registrations/new.html.erb
<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :role => 'form',
:class => 'payola-onestep-subscription-form',
'data-payola-base-path' => payola_path,
'data-payola-plan-type' => resource.plan.plan_class,
'data-payola-plan-id' => resource.plan.id}) do |f| %>
<div>
<span id="error_explanation" class="payola-payment-error"></span>
</div>
<div class="form-group">
<h3 class="text-center"><i>Try Free for 14 Days!</i></h3> <br>
<%= f.label 'Subscription plan' %>
<%= f.collection_select(:plan_id, Plan.all, :id, :name) %>
</div>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, :autofocus => true, class: 'form-control', data: { payola: 'email' } %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<div class="form-group">
<%= label_tag :card_number, "Credit Card Number" %>
<%= text_field_tag :card_number, nil, name: nil, class: 'form-control', data: { stripe: 'number' } %>
</div>
<div class="form-group">
<%= label_tag :card_code, "Card Security Code" %>
<%= text_field_tag :card_code, nil, name: nil, class: 'form-control', data: { stripe: 'cvc' } %>
</div>
<br />
<div class="form-group">
<%= label_tag :card_month, "Card Expiry" %>
<%= select_month nil, { use_two_digit_numbers: true}, { name: nil, data: { stripe: 'exp-month' } } %>
<%= select_year nil, {start_year: Date.today.year, end_year: Date.today.year+10}, { name: nil, data: { stripe: 'exp-year' } } %>
</div>
<div class="text-center">
<%= f.submit 'Sign up', :class => 'button right' %>
</div>
<% end %>
这是我目前制定的计划。
class CreatePlanService
def call
p1 = Plan.where(name: 'Yearly').first_or_initialize do |p|
p.amount = 36000
p.interval = 'year'
p.stripe_id = 'yearly'
end
p1.save!(:validate => false)
p2 = Plan.where(name: 'Monthly').first_or_initialize do |p|
p.amount = 3000
p.interval = 'month'
p.stripe_id = 'monthly'
end
p2.save!(:validate => false)
end
end
用户模型
class User < ActiveRecord::Base
enum role: [:user, :admin, :yearly, :monthly]
after_initialize :set_default_role, :if => :new_record?
after_initialize :set_default_plan, :if => :new_record?
# after_create :sign_up_for_mailing_list
belongs_to :plan
validates_associated :plan
has_many :dashboards
has_many :cardtools
def set_default_role
self.role ||= :user
end
def set_default_plan
self.plan ||= Plan.last
end
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
def sign_up_for_mailing_list
MailingListSignupJob.perform_later(self)
end
def subscribe
mailchimp = Gibbon::Request.new(api_key: Rails.application.secrets.mailchimp_api_key)
list_id = Rails.application.secrets.mailchimp_list_id
result = mailchimp.lists(list_id).members.create(
body: {
email_address: self.email,
status: 'subscribed'
})
Rails.logger.info("Subscribed #{self.email} to MailChimp") if result
end
end
注册控制器
class RegistrationsController < Devise::RegistrationsController
include Payola::StatusBehavior
before_action :cancel_subscription, only: [:destroy]
def new
build_resource({})
unless params[:plan].nil?
# If broken, follow console https://github.com/RailsApps/rails-stripe-membership-saas/issues/127
@plan = Plan.find_by!(stripe_id: params[:plan])
resource.plan = @plan
end
yield resource if block_given?
respond_with self.resource
end
def create
build_resource(sign_up_params)
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
resource.role = User.roles[plan.stripe_id] unless resource.admin?
resource.save
yield resource if block_given?
if resource.persisted?
if resource.active_for_authentication?
set_flash_message :notice, :signed_up if is_flashing_format?
sign_up(resource_name, resource)
subscribe
else
set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
expire_data_after_sign_in!
subscribe
end
else
clean_up_passwords resource
render json:
{error: resource.errors.full_messages.to_sentence},
status: 400
end
end
def change_plan
plan = Plan.find_by!(id: params[:user][:plan_id].to_i)
unless plan == current_user.plan
role = User.roles[plan.stripe_id]
if current_user.update_attributes!(plan: plan, role: role)
subscription = Payola::Subscription.find_by!(owner_id: current_user.id)
Payola::ChangeSubscriptionPlan.call(subscription, plan)
redirect_to edit_user_registration_path, :notice => "Plan changed."
else
flash[:alert] = 'Unable to change plan.'
build_resource
render :edit
end
end
end
private
def sign_up_params
params.require(:user).permit(:email, :password, :password_confirmation, :plan_id)
end
def subscribe
return if resource.admin?
params[:plan] = current_user.plan
subscription = Payola::CreateSubscription.call(params, current_user)
current_user.save
render_payola_status(subscription)
end
def cancel_subscription
subscription = Payola::Subscription.find_by!(owner_id: current_user.id, state: 'active')
Payola::CancelSubscription.call(subscription)
end
end
从该表单中删除卡片属性肯定会使它崩溃。
Payola 假定您想要创建一个立即收费订阅。这意味着免费计划和免费试用计划需要在注册时提供信用卡。所以解决这个问题可能是:
- 使用
stripe-rails
gem 而不是 payola 库。 Stripe 对于免费计划或试用计划不需要卡片。
或
- 您遵循 Payola 创建者对此的解决方法。 https://github.com/peterkeen/payola/commit/0b0282fb256eba7247999fbf2b33f2ded567c311。 虽然对你来说可能是 Head on Rock。
至于 webhooks,我认为这是可能的。当信用卡过期或月度交易被拒绝时,Stripe 会在失败后自动重试定期付款。经过多次尝试(在您的 Stripe 帐户设置中设置)后,Stripe 将取消订阅。您的应用程序需要知道拒绝具有过期帐户的订户访问。 Stripe 提供 webhooks 来向您传达事件(有关详细信息,请参阅 Stripe Webhooks Documentation)。
Stripe webhook 是从 Stripe 服务器到您网站的 HTTP 请求,包含 JSON 提供事件数据的数据,以及可用于从 Stripe 服务器检索数据的事件 ID。示例应用程序使用由 Danny Whalen 的 stripe_event
gem 提供的实现来响应 Stripe webhook,它随 Payola gem 一起提供。应用程序在 https://www.example.com/payola/events.
示例应用程序仅响应 “customer.subscription.deleted”
事件。您可以自定义应用程序以响应其他事件(例如发送感谢电子邮件以响应 “invoice.payment_succeeded”
事件)。
要使 webhook 正常工作,您必须通过 https://manage.stripe.com/#account/webhooks 访问您的 Stripe 控制面板并添加 URL 适用于您的应用程序,例如 https://www.example.com/payola/events.