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 假定您想要创建一个立即收费订阅。这意味着免费计划和免费试用计划需要在注册时提供信用卡。所以解决这个问题可能是:

  1. 使用 stripe-rails gem 而不是 payola 库。 Stripe 对于免费计划或试用计划不需要卡片。

  1. 您遵循 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.