Roll Your Own Recurring Billing With Rails

When it comes to recurring billing there is a bewilderment of services available: Recurly.com, Chargify.com, Spreedly.com, CheddarGetter.com, and so on. Each has pros and cons, but our billing needs were a bit involved, and since we couldn’t find a service that seemed to be a perfect match we decided to create our own system, using Braintree as our credit card payment processor.

Full disclosure, it did end up being quite complicated, and for those with simpler needs it is certainly worth investigating any of the above - Braintree even has its own simple recurring billing system built in. However, we’ve now got a system we’re confident in, which is super easy to customize as our needs mature, over which we have full control, and which will never go out of business on us or suffer downtime or start charging us more or…you get the idea.

This isn’t so much a guide as a collection of stuff that we figured out along the way. It may be useful for you if you are considering taking the plunge yourself.

Overview

We needed a system that would allow the following:

  • Credit card as well as invoice-based billing
  • Usage-based payment packages, with a minimum monthly fee
  • Trial period, customizable per customer
  • Millicents-based (usage is based on attributions, which cost e.g. €0.000125 each)
  • Customer or account manager can upgrade/downgrade package any time
  • VAT based on customer location and VAT status

Our customers’ payment details are stored in an Account model, the billing-relevant parts of which look like this:

braintree_customer_id: string,  # braintree's 'vault' id for stored credit cards
billing_start:         date,    # prior to this date a customer is in their trial period
billing_day:           integer, # the day this customer will be charged each month
invoice:               boolean  # customers can pay via invoice or credit card
vat_number:            string   # don't charge VAT to EU customers with a valid VAT number
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Account < ActiveRecord::Base
  has_many :receipts
  has_many :payment_plans

  before_create :set_billing_start_and_day
  after_create  :set_up_payment_plans

  # [...]

  # Billing days range from 1-28
  def set_billing_start_and_day
    date = Date.today + DEMO_PERIOD
    self.billing_start ||= date
    self.billing_day   ||= [date.day, 28].min
  end

  def set_up_payment_plans
    self.payment_plans = PaymentPlanTemplate.all.map(&:to_plan)
  end
end

As you can see, when an account is created we set up a few things. PaymentPlanTemplate refers to our three ‘default’ payment plans - Basic, Business and Enterprise. Each PaymentPlan has a different attribution cost and minimum fee. Since each account has its own collection of PaymentPlans, our account managers can easily edit a customer’s available payment plans as needed.

You may wonder why billing_day is limited to 28 - well, that’s our cheeky way of bypassing the complexity involved in end-of-month calculations. If billing_day can be anything up to and including 31 then things get pretty gnarly in shorter months (god forbid you should ever experience a leap year). We had a stab at it (here’s a representative sample), recoiled in horror, and decided it wouldn’t kill anybody who signs up on the 29th to pay on the 28th of each month.

Saving credit cards

Braintree takes all of the complication out of storing customers’ payment details. Using their braintree_ruby gem’s awesome transparent redirect feature we can easily set up a form in our user’s profile to save a card without any sensitive data ever touching our system:

The controller:

1
2
3
4
5
6
7
8
9
def new
  @tr_data =
  Braintree::TransparentRedirect.create_customer_data(
    :redirect_url => user_braintree_confirm_url,
    :customer => {
    :company => @user.name
      }
    )
end

The view:

1
2
3
  <%= form_tag Braintree::TransparentRedirect.url %>
  <%= hidden_field_tag :tr_data, @tr_data %>
  #[...bog standard inputs for card number, expiration date etc...]

…yep, that’s it! The form gets submitted directly to Braintree, never touching your server, and braintree responds with success or failure allowing you to store the customer’s braintree_customer_id for use at billing time. The Braintree docs are a great place to go looking for more info if you can’t quite believe it’s that simple.

The recurring part

As you would expect, at the heart of our billing system is a recurring cron job running a rake task that checks who should be billed today and does the necessary leg-work. Simple scopes on the Account model provide us with our victims targets:

1
2
3
4
5
6
7
8
9
namespace :payment do
  desc "send and generate bills and invoices for today"
  task generate_for_today: [:environment] do
    today = Date.today
    Account.billable_on(today).with_billing_day(today.day).each do |account|
      Receipt.generate(account, today - 1.day).process_async!
    end
  end
end

process_async! comes from the excellent Sidekiq gem which handles all of our background jobs.

Most of the complexity in our Receipt.generate method comes from figuring out what period we are billing the customer for. Everything else (how many attributions the account had, which payment plan is active etc.) can easily be figured out from there and is much more dependent on how you calculate cost, so I won’t go into detail.

Remember billing_day? That’s all you need to work out which billing period an account is/was/will be in on any given date:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Account < ActiveRecord::Base

  # [...]

  def billing_period(date)
    billing_period_start_date_for(date)..billing_period_end_date_for(date)
  end

  def billing_period_start_date_for(date)
    year, month = if date.day < billing_day
                    [(date - 1.month).year, (date - 1.month).month]
                  else
                    [date.year, date.month]
                  end

    Date.new(year, month, billing_day)
  end

  def billing_period_end_date_for(date)
    year, month = if date.day < billing_day
                    [date.year, date.month]
                  else
                    [(date + 1.month).year, (date + 1.month).month]
                  end

    Date.new(year, month, billing_day) - 1.day
  end
end

Hopefully this is fairly self explanatory - if the date’s day-of-the-month is smaller than (ie. before) the account’s billing day, then the period must have started the month before and will end this month; if bigger (after) then it will start this month and end the month after.

Happily, ActiveSupport takes care of a lot of the complexity here, as we can use crazy magic talk like date - 1.month to cover a variety of ills.

Once we know what period we’re talking about, it’s trivial to work out how many attributions the account had and multiply them by the active payment plan’s attribution cost.

We end up with a receipt that looks something like this:

1
2
3
4
5
  Receipt id: 65,
  start_date: "2013-07-14",
  end_date: "2013-08-13",
  amount: 248468250, # millicents, remember!
  ....

The billing part

So we now have a receipt all calculated and ready to go. What happens next depends on what sort of customer we are dealing with:

Credit card customers

This is dead simple, thanks to Braintree. We submit a transaction like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module BillingService
  class CreditCardProcessor < BaseProcessor
    def self.process!(receipt)
      calculator = CalculatorService::Calculator.new(receipt.account)

      if Braintree::Transaction.sale(
        custom_fields: { adjust_io_bill_id: receipt.id },
        customer_id:   receipt.account.braintree_customer_id,
        options:       { submit_for_settlement: true },
        amount:        calculator.total(receipt.amount).to_euros
      ).success?
        accept(receipt)
      else
        reject(receipt)
      end
    end
  end
end

The CalculatorService stuff is there to calculate VAT etc, which is based on the customer’s location and whether or not they have submitted a valid VAT number. We use composed_of in the Receipt model to enable us to call methods like to_euros on a value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Receipt < ActiveRecord::Base
    composed_of :amount, class_name: 'Money::Millicents', mapping: [:amount, :value], converter: :new
    # [...]
end

class Money::Millicents < SimpleDelegator
  include Comparable

  def initialize(value = 0)
    super(value.to_i)
  end

  def to_euros
    (__getobj__ / 1000.0).ceil / 100.0
  end

  # [...] there is quite a bit more complexity in here that possibly warrants a
  # blog post of its own
end

Invoice customers

This is even simpler, as we simply generate a PDF invoice from the receipt’s show action, using our wonderful shrimp gem, and email it to the customer, who cheerfully pays us. Job done!








User experience

Obviously when money is involved you want to be as transparent as possible. One big advantage of everything being in our own system is that it’s simple to show our customers all the information they need about their billing status, such as their previous bills and current status:

or which cards they have saved with us:

Not having to rely on external APIs for anything except credit cards also makes the entire dashboard a lot snappier, which is a big plus!

Conclusion

The hardest part of building a recurring billing system is designing it in the first place. We were lucky enough to be able to take it at our own pace and the design grew out of the implementation process quite naturally. No one part of it is particularly complicated, but taken all together there is indeed a great deal of complexity involved, which is why so many people advise against ‘reinventing the wheel’ and making your own. I haven’t even touched on how we deal with trial periods and payment-based authorization - this will come in a later post - but I hope this gives some idea of what is required and what you should think about if you decide to roll your own. Above all - ENJOY RESPONSIBLY.





NOTE: ‘attribution’ refers to a click or an install that we have tracked for a customer.

Comments