Recurring Billing with TrustCommerce

I recently refactored some very old billing code where I used API calls from my TrustCommerce gem directly in my controllers - wow, that’s ugly!

Knowing that skinny controller fat model is the way to go I ramped up for a quick refactor…

Here are a few snippets that came out of the refactor that may be of use to others designing a subscription-based site:

Accounts

class Account < ActiveRecord::Base
  ...
  before_create :build_dependencies

  has_one :billing_detail, :dependent => :destroy
  delegate :pricing_plan, :to => :billing_detail
  ...

  private

    def build_dependencies
      # accounts start off on the free trial plan
      build_billing_detail(:pricing_plan_id => PricingPlan.find_by_cents(0).id)
    end
end

Pricing Plans schema

class CreatePricingPlans < ActiveRecord::Migration
  def self.up
    create_table :pricing_plans do |t|
      t.string    :name,  :null => false
      t.integer   :cents, :null => false
      t.datetime  :deleted_at
      t.timestamps
      # add booleans here that reflect allowances of plan
      # Example: If accounts on plan are able to use SSL
      t.integer :ssl, :null => false
      ...
    end
                     
    # create new plans
    PricingPlan.create!(:name => 'Free', :cents => 0, :ssl => true, ...)
    ...
  end

  def self.down
    drop_table :pricing_plans
  end
end

Pricing Plans model

class PricingPlan < ActiveRecord::Base
  acts_as_paranoid
  has_many :billing_detail
  has_many :accounts, :through => :billing_detail

  def paying_plan?
    cents > 0
  end

  def free?
    cents <= 0
  end
end

Billing Details schema

class AddBillingDetails < ActiveRecord::Migration
  def self.up
    create_table :billing_details do |t|
      t.integer   :account_id, :null => false
      t.integer   :pricing_plan_id, :null => false
      t.string    :billing_id
      t.string    :cardholder_name
      t.string    :card_type
      t.string    :credit_card
      t.datetime  :expiration_date
      t.timestamps
    end
  end

  def self.down
    drop_table :billing_details
  end
end

Billing Details model

class BillingDetail < ActiveRecord::Base
  before_validation :update_trustcommerce_account_if_needed
  before_validation :create_trustcommerce_account_if_needed
  before_save :hash_credit_card

  belongs_to :account
  belongs_to :pricing_plan, :with_deleted => true

  validates_presence_of :account_id, :pricing_plan_id
  validates_associated :account, :pricing_plan

  validates_presence_of :billing_id,      :if => :paying_plan?, :message => 'Billing id required for paid plan'
  validates_presence_of :cardholder_name, :if => :paying_plan?, :message => 'Cardholder name required for paid plan'
  validates_presence_of :credit_card,     :if => :paying_plan?, :message => 'Credit card required for paid plan'

  # always reload pricing plan so before_validation can check the new plan
  def paying_plan?
    pricing_plan(true).paying_plan?
  end

  private

    def update_trustcommerce_account_if_needed
      if paying_plan? && !billing_id.blank?
        params = if credit_card =~ /^\*+/
          # no change to credit card, just update amount
          trustcommerce_parameters.only(:billingid, :amount)
        else
          trustcommerce_parameters
        end
        logger.debug "TrustCommerce::Subscription.update(#{params.inspect})"
        response = TrustCommerce::Subscription.update(params)
      	logger.debug "TrustCommerce::Subscription.update response: #{response.inspect}"
      	if !response.respond_to?(:[]) || response[:status] != 'accepted'
          errors.add(:billing_id, 'Your billing or credit card information appears to be incomplete or incorrect.')
          false
        end
      end
    end

    def create_trustcommerce_account_if_needed
      if paying_plan? && billing_id.blank?
        params = trustcommerce_parameters.except(:billingid)
        logger.debug "TrustCommerce::Subscription.create(#{params.inspect})"
        response = TrustCommerce::Subscription.create(params)
      	logger.debug "TrustCommerce::Subscription.create response: #{response.inspect}"
      	if response.respond_to?(:[]) && response[:status] == 'approved'
      	  self.billing_id = response[:billingid]
      	else
          errors.add(:billing_id, 'Your billing or credit card information appears to be incomplete or incorrect.')
          false
        end
      end
    end

    def trustcommerce_parameters
      {
        :billingid  => billing_id,
        :name       => cardholder_name,
        :cc         => credit_card,
        :exp        => expiration_date.strftime('%m%y'),
        :amount     => pricing_plan(true).cents,
        :cycle      => '30d'
      }
    end

    def hash_credit_card
      self.credit_card = "************#{credit_card[-4..-1]}" if !credit_card.blank?
    end
end

Note: There is the use of at least one of the hash extensions I use in these snippets.

Comment or question via
FYI: This post was migrated over from another blogging engine. If you encounter any issues please let me know on . Thanks.