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.


2 Comments
Commenting is closed for this article.