Skip to content

Instantly share code, notes, and snippets.

@Lulu117
Last active September 25, 2020 14:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lulu117/e85ecf16d320c7f46bb9a8eb245c0ee5 to your computer and use it in GitHub Desktop.
Save Lulu117/e85ecf16d320c7f46bb9a8eb245c0ee5 to your computer and use it in GitHub Desktop.

Subscriptions

  • Learn how to set up basic subscriptions
  • Create composable service objects
  • Advanced subscriptions techniques

So far in this book we've talked about how to sell products once. A customer selects a product, puts their information in, and hits the "buy now" button, and that's end of our interaction with them.

The majority of SaaS businesses don't operate that way. Most of them will want their customers to pay on a regular schedule. Stripe has built-in support for these kinds of subscription payments that is easy to work with and convenient to set up.

In this chapter, we're going to go through a basic integration, following the same principles that we've laid out for the one-time product sales. Then, we'll explore some more advanced topics, including how to effectively use the subscription webhook events, in-depth coverage of Stripe's plans, and options for reporting.

Basic Integration

For this example, we're going to add a simple subscription system where people can sign up to receive periodic download links, like magazine articles.

Let's start by making a few models. We'll need models to keep track of our pricing plans and each user's subscriptions, since they may sign up for one or more magazine.

$ rails g model plan \
    stripe_id:string \
    name:string \
    description:text \
    amount:integer \
    interval:string \
    published:boolean
    $ rails g model subscription \
    user:references \
    plan:references \
    stripe_id:string
    $ rails g migration AddStripeCustomerIdToUser \
    stripe_customer_id:string

Open up the models and add audit trails:

   class Plan < ActiveRecord::Base
 has_paper_trail
 validates :stripe_id, uniqueness: true
end
class Subscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :plan

  has_paper_trail
end

Notice we also added a uniqueness constraint to Plan. Re-using Stripe plan IDs is technically allowed but it's not a very good idea.

Service Objects

In this integration we're going to be using service objects to encapsulate the business logic of creating users and subscriptions. In our usage, a service object lives in /app/services and contains one main class method named call which receives all of the dependencies that the object needs to do it's job.

First, create the directory, then tell Rails to look for it in config/application.rb. Add this below the other config lines:

config.autoload_paths += Dir["#{Rails.root}/app/services"]

Here's our CreateUser service, in /app/services/create_user.rb:

class CreateUser
  def self.call(email_address)

    user = User.find_by(email: email_address)

    return user if user.present?

    raw_token, enc_token = Devise.token_generator.generate(
      User, :reset_password_token)
    password = SecureRandom.hex(32)

    user = User.create!(
      email: email_address,
      password: password,
      password_confirmation: password,
      reset_password_token: enc_token,
      reset_password_sent_at: Time.now
    )

    return user, raw_token
  end
end

In our signup flow, we are going to have the user provide their email address at the same time they give us their credit card. Internally, Devise will set up a password reset token for us if we ask, but because of the way Devise token authentication works, there's no direct way to get the raw token so we have to generate a token pair the same way Devise does internally.

CreateUser.call takes an email address and first attempts to look up the user with that email address. If there isn't one, it proceeds to generate the Devise password reset token, create the user, and then return both the user and the token.

Now that we can create a user, let's create a subscription:

class CreateSubscription
  def self.call(plan, email_address, token)
    user, raw_token = CreateUser.call(email_address)

    subscription = Subscription.new(
      plan: plan,
      user: user
    )

    begin
      stripe_sub = nil
      if user.stripe_customer_id.blank?
        customer = Stripe::Customer.create(
          source: token,
          email: user.email,
          plan: plan.stripe_id,
        )
        user.stripe_customer_id = customer.id
        user.save!
        stripe_sub = customer.subscriptions.first
      else
        customer = Stripe::Customer.retrieve(user.stripe_customer_id)
        stripe_sub = customer.subscriptions.create(
          plan: plan.stripe_id
        )
      end

      subscription.stripe_id = stripe_sub.id

      subscription.save!
    rescue Stripe::StripeError => e
      subscription.errors[:base] << e.message
    end

    subscription
  end 
end

One of the best things about service objects is how easy it is to compose them. We can just use the CreateUser service we set up to create a user wherever we want, including in other service objects.

First we create the user and then a Subscription object. Next, we actually talk to Stripe. All we have to do is create a Stripe::Customer object with the plan, token, and email address of the user. We store the customer ID onto our Subscription object for later reference then send a receipt email which will contain a link for the user to set up their password.

How do we get those plans, though? Let's create one more service object for creating new plans in our database and propagating them to Stripe:

class CreatePlan
  def self.call(options={})
    plan = Plan.new(options)

    if !plan.valid?
      return plan
    end

    begin
      Stripe::Plan.create(
        id: options[:stripe_id],
        amount: options[:amount],
        currency: 'usd',
        interval: options[:interval],
        name: options[:name],
      )
    rescue Stripe::StripeError => e
      plan.errors[:base] << e.message
      return plan
    end

    plan.save

    return plan
  end
end

All this does is pass the options hash through to Plan#new and then attempts to create a Stripe-level plan with those same options. If everything goes well, it then saves our new plan and returns it. It's very easy to use this service object in the console so we're not going to build out a controller here. Here's an example of creating a plan from the console:

irb(main):001:0> CreatePlan.call(stripe_id: 'test_plan', name: 'Test Plan', amount: 500, interval: 'month', description: 'Test Plan', published: false)

Controller

The next thing we have to do is actually use the service objects. Thankfully, that's pretty simple:

class SubscriptionsController < ApplicationController
  skip_before_filter :authenticate_user!

  before_filter :load_plans

  def index
  end

  def new
    @subscription = Subscription.new
    @plan = Plan.find(params[:plan_id])
  end

  def create
    @plan = Plan.find(params[:plan_id])
    @subscription = CreateSubscription.call(
      @plan,
      params[:email_address],
      params[:stripeToken]
    )
    if @subscription.errors.blank?
      flash[:notice] = 'Thank you for your purchase!' +
        'Please click the link in the email we just sent ' +
        'you to get started.'
      redirect_to '/'
    else
      render :new
    end
  end

protected

  def load_plans
    @plans = Plan.where(published: true).order('amount')
  end

end

Before we do anything else, we have to load the published plans so they're available for the actions. Other than that, this is a normal, ordinary, every day Rails controller. We use the service object we created previously to actually create the subscription, and we check that it made it all the way through the process without any errors.

Now, we should set up stripe.js (if you did this in Custom Forms you can skip this). In app/views/layouts/application.html.erb, add this at to the end of <head>:

<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">
$(function(){
 Stripe.setPublishableKey('<%= Rails.configuration.stripe[:publishable_key] %>');
});
</script>

Now let's fill out the views:

/app/views/subscriptions/index.html.erb:

<% @plans.each do |plan| %>
  <%= link_to "#{plan.name} (#{formatted_price(plan.amount)})",
        new_subscription_path(plan_id: plan.id) %>
<% end %>

/app/views/subscriptions/new.html.erb:

<% unless @subscription.errors.blank? %>
  <%= @subscription.errors.full_messages.to_sentence %>
<% end %>

<h2>Subscribing to <%= @plan.name %></h2>

<%= form_for @subscription, html: { id: 'payment-form' } do |f| %>
  <input type="hidden" name="plan_id" value="<%= @plan.id %>" />
  <span class="payment-errors"></span>

  <div class="form-row">
    <label>
      <span>Email Address</span>
      <input type="email" size="20" name="email_address"/>
    </label>
  </div>

  <div class="form-row">
    <label>
      <span>Card Number</span>
      <input type="text" size="20" data-stripe="number"/>
    </label>
  </div>

  <div class="form-row">
    <label>
      <span>CVC</span>
      <input type="text" size="4" data-stripe="cvc"/>
    </label>
  </div>

  <div class="form-row">
    <label>
      <span>Expiration (MM/YYYY)</span>
      <input type="text" size="2" data-stripe="exp-month"/>
    </label>
    <span> / </span>
    <input type="text" size="4" data-stripe="exp-year"/>
  </div>

  <button type="submit">Pay Now</button>
<% end %>

/app/assets/javascripts/subscriptions.js:

jQuery(function($) {
  $('#payment-form').submit(function(event) {
    var $form = $(this);

    $form.find('button').prop('disabled', true);

    Stripe.card.createToken($form, stripeResponseHandler);

    return false;
  });
});

function stripeResponseHandler(status, response) {
  var $form = $('#payment-form');

  if (response.error) {
    // Show the errors on the form
    $form.find('.payment-errors').text(response.error.message);
    $form.find('button').prop('disabled', false);
  } else {
    // response contains id and card, which contains additional card details
    var token = response.id;
    // Insert the token into the form so it gets submitted to the server
    $form.append($('<input type="hidden" name="stripeToken" />').val(token));
    // and submit
    $form.get(0).submit();
  }
};

We also need to add routes:

resources :subscriptions, only: [:index, :new, :create]

Let's check it out. Go to http://localhost:3000/subscriptions/new?plan_id=1 and you should be able to fill out the form and create a paying user.

Multiple Subscriptions

Stripe allows a customer to have multiple subscriptions. Because of the way we've set up our Subscription class, this is trivial to accomplish in our application. All you have to do is call CreateSubscription.call(), passing in the user's email address, the plan, and a blank token, like this:

CreateSubscription.call(
  current_user.email_address,
  some_plan,
  ''
)

Upgrading and Downgrading Subscriptions

What about when a user wants to change their plan? For example, a user wants to go from the 10 frobs a month plan to one with 1000. Or maybe go the other way?

Let's wrap that up in another service object:

class ChangePlan
  def self.call(subscription, to_plan)
    from_plan = subscription.plan
    begin
      user = subscription.user
      customer = Stripe::Customer.retrieve(user.stripe_customer_id)
      stripe_sub = customer.subscriptions.retrieve(subscription.stripe_id)

      stripe_sub.plan = to_plan.stripe_id
      stripe_sub.save
      subscription.plan = to_plan
      subscription.save!
    rescue Stripe::StripeError => e
      subscription.errors[:base] << e.message
    end

    subscription
  end
end

What if the user wants to change or update their card? Again, pretty simple. Just set up a form like above but just with the card attributes, then create another service object to handle the action:

class ChangeSubscriptionCard
  def self.call(subscription, token)
    begin
      user = subscription.user
      customer = Stripe::Customer.retrieve(user.stripe_customer_id)
      stripe_sub = customer.subscriptions.retrieve(subscription.stripe_id)

      stripe_sub.source = token
      stripe_sub.save!
    rescue Stripe::StripeError => e
      subscription.errors[:base] << e.message
    end

    subscription
  end
end

The controller actions for both of these are self-explanatory. Just grab the subscription in question and the plan or token and pass them to the appropriate service object's call method.

Dunning

Sometimes customers don't pay their bill, often through no fault of their own. The process of communicating with your customers to get them to pay is called "dunning" and it's vital for any type of business. For a subscription SaaS using Stripe where the customer's card is billed automatically every period the dunning process kicks in when a charge fails for some reason and we send them an email. The next month we send them another, more strongly worded email, eventually leading to canceling their account.

Really, though, you don't want to let the process even get started. The number one reason why subscription charges start getting declined is that the customer's card expires. Since you're saving the customer's card expiration in your database (if you're not, you should start), it's a trivial matter to find all of the customers that have an expiration coming up and send them a short reminder email:

expiring_customers = Customer.where(
  'date_reminded is null and expiration_date <= ?',
  Date.today() + 30.days
)

expiring_customers.each do |customer|
  StripeMailer.card_expiring(customer).deliver
  customer.update_attributes(date_reminded: Date.today)
end

Andrew Culver is the author of Koudoku and currently is developing a product to automate this process. He has had phenomenal success reducing churn using this method:

In one product where this approach has been taken, the campaign stops 50% of expiring credit card accounts from having a failed payment. For the remaining 50% we have another campaign that kicks in once the payment fails. Then after a couple days an email is sent to the sales team. Before we automated this process it was a major source of pain for us to manage these accounts going delinquent. It's still a source of work for our sales team and a source of customer churn for us, but it's much smaller and more manageable overall.

Andrew's campaign sends emails at 30, 15, and three days before card expiration, as well as the day of. Make sure to describe what's going on and give them an easy way to login to your app and update their card. If the payment does eventually fail, make sure to contact them again. According to Patrick McKenzie you should also include a P.S. to the effect that you're a small business, not a bank, and that they're not in trouble or anything. You're sure it's a mistake so you won't be cutting them off for a few days.

Speaking of cutting them off, you really shouldn't automatically cancel anyone's account without a manual review process. Charges fail sometimes and it's nobody's fault, which is why Stripe automatically retries for you for a configurable number of days. After that's up and the charge finally fails, send yourself an email and follow up with the customer, either by email or over the phone.

There's one more aspect to dunning: following up on canceled accounts. If a high value customer decides to cancel, give them a call and ask if there's anything you can do to change their mind. It's worth a shot, and most of the time you can work something out.

Utility-style Usage Billing

Handling a basic subscription is straight forward and well covered in the example apps. Let's say, however, you're building an app where you want metered billing like a phone bill. You'd have a basic subscription for access and then monthly invoicing for anything else. Stripe has a feature they call Invoices that makes this easy. For example, you want to allow customers to send email to a list and base the charge it on how many emails get sent. You could do something like this:

class EmailSend < ActiveRecord::Base
  # ...

  belongs_to :user
  after_create :add_invoice_item

  def add_invoice_item
    Stripe::InvoiceItem.create(
      customer: user.stripe_customer_id,
      amount: 1,
      currency: "usd",
      description: "email to #{address}"
    )
  end
end

At the end of the customer's billing cycle Stripe will tally up all of the InvoiceItems that you've added to the customer's bill and charge them the total plus their subscription plan's amount.

Stripe will also send you a webhook detailing the customer's entire invoice right before they initiate the charge. Instead of creating an invoice item for every single email as it gets sent, you could just create one invoice item for the number of emails sent in the billing period:

StripeEvent.configure do |events|
  events.subscribe 'invoice.created' do |event|
    invoice = event.data.object

    num_emails = EmailSend.where(
      'created_at between ? and ?',
      [Time.at(invoice.period_start), Time.at(invoice.period_end)]
    ).count
    Stripe::InvoiceItem.create(
      invoice: invoice.id,
      amount: num_emails,
      currency: 'usd',
      description: "#{num_emails} emails sent @ $0.01"
    )
  end
end

Note that this can get kind of complicated if invoice items can be charged at different rates. You can either add one InvoiceItem per individual charge, or you can add one InvoiceItem per item type with the amount set to num_items * item amount.

Free Trials

Stripe supports adding free trials to your subscription plans. You can either set trial_period_days on the plan itself, or you can set trial_ends to a timestamp on the customer's subscription when you create it. trial_ends overrides trial_days, which means it's trivial to give a particular customer an extra long or extra short trial.

While a free trial is ongoing, you can manipulate the trial_ends attribute on a subscription. You can set it to a future time to extend the trial, or you can set it to the special value "now" to force it to end immediately.

If you want to prevent users from getting multiple trials, you'll need to do the deduplication for yourself. Stripe doesn't handle it. The good news is, Devise won't let multiple accounts share an email address, so we're good to go.

Reporting

Accepting payments with Stripe is only half of the battle. The other half is making sure you know if you're getting paid properly. I advise a "trust but verify" posture. Of course Stripe is going to be better at actually triggering payments, but we should record them as they happen so we know if Stripe or our bank messes up somehow.

The easiest way to do that is to record transactions as they happen by catching Stripe's webhooks. Let's add an InvoicePayment model:

 $ rails g model InvoicePayment \
    stripe_id:string \
    amount:integer \
    fee_amount:integer \
    user:references \
    subscription:references

We can populate these by adding another StripeEvent subscription:

  StripeEvent.configure do |events|
  events.subscribe('invoice.payment_succeeded') do |event|
    invoice = event.data.object
    user = User.find_by(stripe_id: invoice.customer)
    invoice_sub = invoice.lines.data.select { |i| i.type == 'subscription' }.first.id
    subscription = Subscription.find_by(stripe_id: invoice_sub)

    charge = invoice.charge

    balance_txn = Stripe::BalanceTransaction.retrieve(charge.balance_transaction)

    InvoicePayment.create(
      stripe_id: invoice.id,
      amount: invoice.total,
      fee_amount: balance_txn.fee,
      user_id: user.id,
      subscription_id: subscription.id
    )
  end
end

And now we can run queries against the invoice_payments table to see, for example, how much a given user has paid in the last year, or how much revenue a particular subscription plan has generated. There are a few tools that make this easier to work with:

  • Groupdate makes it trivial to group by various date dimensions
  • Chartkick generates wonderful charts from the data generated by Groupdate.

3rd Party Services

There is an entire ecosystem of Stripe reporting services these days. Here's a few examples:

  • Baremetrics gives amazing dashboards and drill-down reports
  • Hookfeed builds customer-level analytics and generates email reports
  • FirstOfficer provides insightful reports that tell you why your business is behaving how it is.

All three of these are driven directly from your Stripe event feed and hook into your account via Stripe Connect. To see how to build a service like that, read on to the Marketplaces chapter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment