Class: Billingly::BaseCustomer

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/billingly/base_customer.rb

Overview

A Customer is Billingly’s main actor.

  • Customers have a Subscription to your service which entitles them to use it.

  • Customers are invoiced regularly to pay for their Subscription

  • Payments are received on a Customers behalf and credited to their account.

  • Invoices are generated periodically calculating charges a Customer incurred in.

  • Receipts are sent to Customers when their invoices are paid.

Direct Known Subclasses

Customer

Constant Summary collapse

DEACTIVATION_REASONS =

The reason why this customer is deactivated. A customer can be deactivated for one of 3 reasons:

* trial_expired: Their trial period expired.
* debtor: They have unpaid invoices.
* left_voluntarily: They decided to leave the site.

This is important when reactivating their account. If they left in their own terms, we won’t try to reactivate their account when we receive a payment from them. The message shown to them when they reactivate will also be different depending on how they left.

%w(trial_expired debtor left_voluntarily)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#active_subscriptionSubscription? (readonly)

The Subscription for which the customer is currently being charged.

Returns:



64
65
66
67
# File 'app/models/billingly/base_customer.rb', line 64

def active_subscription
  last = subscriptions.last
  last unless last.nil? || last.terminated?
end

#deactivated?Boolean (readonly)

A customer can be deactivated when they cancel their subscription or when they miss a payment. Under the hood this function checks the #deactivated_since attribute.

Returns:

  • (Boolean)


41
42
43
# File 'app/models/billingly/base_customer.rb', line 41

def deactivated?
  not deactivated_since.nil?
end

#deactivated_sinceDateTime (readonly)

The Date and Time in which the Customer’s account was deactivated (see #deactivated?). This field denormalizes the date in which this customer’s last subscription was ended.

Returns:

  • (DateTime)


30
# File 'app/models/billingly/base_customer.rb', line 30

validates :deactivated_since, presence: true, if: :deactivation_reason

#deactivation_reasonSymbol

Returns:

  • (Symbol)


35
# File 'app/models/billingly/base_customer.rb', line 35

validates :deactivation_reason, inclusion: DEACTIVATION_REASONS, if: :deactivated?

#debtor?Boolean (readonly)

Returns whether this customer is a debtor or not.

Returns:

  • (Boolean)

    whether this customer is a debtor or not.



72
73
74
# File 'app/models/billingly/base_customer.rb', line 72

def debtor?
  not self.class.debtors.find_by_id(self.id).nil?
end

#doing_trial?Boolean (readonly)

Whether the user is on an unfinished trial period.

Returns:

  • (Boolean)


95
96
97
# File 'app/models/billingly/base_customer.rb', line 95

def doing_trial?
  active_subscription && active_subscription.trial?
end

#emailString

Used as contact address, validates format but does not check uniqueness.

Returns:

  • (String)


48
# File 'app/models/billingly/base_customer.rb', line 48

attr_accessible :email

#invoicesArray<Invoice>

All invoices ever created for this customer, for any Subscription

Returns:



79
# File 'app/models/billingly/base_customer.rb', line 79

has_many :invoices, foreign_key: 'customer_id'

#journal_entriesArray<JournalEntry>

Every JournalEntry ever created for this customer, a #ledger is created from these. See JournalEntry for a description on what they are.

Returns:



85
# File 'app/models/billingly/base_customer.rb', line 85

has_many :journal_entries, foreign_key: 'customer_id'

#paymentsArray<Payment>

All paymetns that were ever credited for this customer

Returns:



59
# File 'app/models/billingly/base_customer.rb', line 59

has_many :payments, foreign_key: 'customer_id'

#subscriptionsArray<Subscription>

All subscriptions this customer was ever subscribed to.

Returns:



54
# File 'app/models/billingly/base_customer.rb', line 54

has_many :subscriptions, foreign_key: 'customer_id'

#trial_days_leftInteger (readonly)

When the user is doing a trial, this would be how many days are left until it’s over.

Returns:

  • (Integer)


102
103
104
105
# File 'app/models/billingly/base_customer.rb', line 102

def trial_days_left
  return unless doing_trial?
  (active_subscription.is_trial_expiring_on.to_date - Time.now.utc.to_date).to_i
end

Class Method Details

.debtorsObject

Note:

A customer may be a debtor and still have an active account until Billingly’s rake task goes through the process of deactivating all debtors.

Furthermore, customers may unsubscribe before their invoices become overdue, hence they may be in a deactivated state and not be debtors yet.

A customer who has overdue invoices at the time of asking this question is considered a debtor.



200
201
202
203
204
# File 'app/models/billingly/base_customer.rb', line 200

def self.debtors
   joins(:invoices).readonly(false)
    .where("#{Billingly::Invoice.table_name}.due_on < ?", Time.now)
    .where(billingly_invoices: {deleted_on: nil, paid_on: nil})
end

Instance Method Details

#add_to_journal(amount, *accounts, extra) ⇒ Object

Note:

Most likely, you will never add entries to the customer journal yourself. These are created when invoicing or crediting payments

Shortcut for adding #journal_entries for this customer.



179
180
181
182
183
184
185
186
187
188
189
# File 'app/models/billingly/base_customer.rb', line 179

def add_to_journal(amount, *accounts, extra)
  accounts = [] if accounts.nil?
  unless extra.is_a?(Hash)
    accounts << extra
    extra = {}
  end
  
  accounts.each do ||
    journal_entries.create!(extra.merge(amount: amount, account: .to_s))
  end
end

#can_subscribe_to?(plan) ⇒ Boolean

Can this customer subscribe to a plan?. You may want to prevent customers from upgrading or downgrading to other plans depending on their usage of your service.

This method is only used in views and controllers to prevent customers from requesting to be upgraded or downgraded to a plan without your consent. The model layer can still subscribe the customer if you so desire.

The default implementation lets Customers upgrade to any if they are currently doing a trial period, and it does not let them re-subscribe to the same plan afterwards. It also always disallows debtors to subscribe to another plan.

Parameters:

Returns:

  • (Boolean)


302
303
304
305
306
# File 'app/models/billingly/base_customer.rb', line 302

def can_subscribe_to?(plan)
  return false if !doing_trial? && active_subscription && active_subscription.plan == plan
  return false if debtor?
  return true
end

#charge_pending_invoicesObject

Charges all invoices for which the customer has enough balance. Oldest invoices are charged first, newer invoices should not be charged until the oldest ones are paid.

See Invoice#charge for more information on how invoices are charged from the customer’s balance.



285
286
287
288
# File 'app/models/billingly/base_customer.rb', line 285

def charge_pending_invoices
  invoices.where(deleted_on: nil, paid_on: nil).order('period_start')
    .each{|invoice| break unless invoice.charge}
end

#credit_payment(amount) ⇒ Object

Note:

This is the single point of entry for Payments.

If you’re processing payments using http://activemerchant.org you should hook your ‘Incoming Payment Notifications’ to call this method to credit the received amount to the customer’s account.

Credits an amount of money to customer’s account and then triggers the corresponding actions if a payment was expected from this customer.

Apart from creating a Payment object this method will try to charge pending invoices and reactivate a customer who was deactivated for being a debtor.

Parameters:

  • amount (BigDecimal, float)

    the amount to be credited.



220
221
222
223
224
# File 'app/models/billingly/base_customer.rb', line 220

def credit_payment(amount)
  Billingly::Payment.credit_for(self, amount)
  charge_pending_invoices
  reactivate if deactivated? && deactivation_reason == 'debtor'
end

#deactivate(reason) ⇒ self?

Terminate a customer’s subscription to the service. Customers are deactivated due to lack of payment, because they decide to end their subscription to your service or because their trial period expired.

Use the shortcuts:

{#deactivate_left_voluntarily}, {#deactivate_trial_expired} or {#deactivate_debtor}

Deactivated customers can always be reactivated later.

Parameters:

Returns:

  • (self, nil)

    nil if the account was already deactivated, self otherwise.



236
237
238
239
240
241
242
243
# File 'app/models/billingly/base_customer.rb', line 236

def deactivate(reason)
  return if deactivated?
  active_subscription.terminate(reason)
  self.deactivated_since = Time.now
  self.deactivation_reason = reason
  save!
  return self
end

#deactivate_debtorObject

See Also:



262
263
264
# File 'app/models/billingly/base_customer.rb', line 262

def deactivate_debtor
  deactivate('debtor')
end

#deactivate_left_voluntarilyObject

See Also:



252
253
254
# File 'app/models/billingly/base_customer.rb', line 252

def deactivate_left_voluntarily
  deactivate('left_voluntarily')
end

#deactivate_trial_expiredObject

See Also:



257
258
259
# File 'app/models/billingly/base_customer.rb', line 257

def deactivate_trial_expired
  deactivate('trial_expired')
end

#do_not_email?Boolean

Some customers do not want to be bothered via email, which is understandable. You can override this method to decide which customers should never be emailed with invoices, receipts or when their trial is over.

It is false by default, this means all of your customers will be emailed.

Returns:

  • (Boolean)

    whether this customer opted out from receiving emails.



314
315
316
# File 'app/models/billingly/base_customer.rb', line 314

def do_not_email?
  false
end

#ledger{Symbol => BigDecimal}

TODO:

Due to silly rounding errors on sqlite this implementation needs to convert decimals to float and then to decimals again. :S

Creates a general ledger from journal entries. Every Invoice and Payment involves movements to the customer’s account. which are registered as a JournalEntry. The ledger can tell us whats the cash balance in our customer’s favor and how much money have they paid overall.

Returns:

  • ({Symbol => BigDecimal})

See Also:



165
166
167
168
169
170
171
172
173
174
# File 'app/models/billingly/base_customer.rb', line 165

def ledger
  Hash.new(0.0).tap do |all|
    journal_entries.group_by(&:account).collect do |, entries|
      values = entries.collect(&:amount).collect(&:to_f)
      all[.to_sym] = values.inject(0.0) do |sum,item|
        (BigDecimal.new(sum.to_s) + BigDecimal.new(item.to_s)).to_f
      end
    end
  end
end

#on_subscription_successObject

Callback called whenever this customer is successfully subscribed to a plan. This callback does not differentiate if the customer is subscribing for the first time, reactivating his account or just changing from one plan to another. self.active_subscription will be the current subscription when this method is called.



153
154
# File 'app/models/billingly/base_customer.rb', line 153

def on_subscription_success
end

#reactivate(new_plan = nil) ⇒ self?

Customers whose account has been deactivated can always re-join the service as long as they don’t owe any money

Returns:

  • (self, nil)

    nil if the customer could not be reactivated, self otherwise.



269
270
271
272
273
274
275
276
277
# File 'app/models/billingly/base_customer.rb', line 269

def reactivate(new_plan = nil)
  new_plan = new_plan || subscriptions.last
  return if new_plan.nil?
  return unless deactivated?
  return if debtor?
  update_attribute(:deactivated_since, nil)
  subscribe_to_plan(new_plan)
  return self
end

#redeem_special_plan_code(code) ⇒ Object

Customers can subscribe to a plan using a special subscription code which would allow them to access an otherwise hidden plan. The SpecialPlanCode can also contain an amount to be redeemed.

Parameters:



142
143
144
145
146
147
# File 'app/models/billingly/base_customer.rb', line 142

def redeem_special_plan_code(code)
  return if code.redeemed?
  credit_payment(code.bonus_amount) if code.bonus_amount
  subscribe_to_plan(code.plan)
  code.update_attributes(customer: self, redeemed_on: Time.now)
end

#subscribe_to_plan(plan, is_trial_expiring_on = nil) ⇒ Subscription

Customers subscribe to the service under certain conditions referred to as a Plan, and perform periodic payments to continue using it. We offer common plans stating how much and how often they should pay, also, if the payment is to be done at the beginning or end of the period (upfront or due-month) Every customer can potentially get a special deal, but we offer common deals as Plans from which a proper Subscription is created. A Subscription is also an acceptable argument, in that case the new one will maintain all the characteristics of that one, except the starting date.

Parameters:

Returns:



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'app/models/billingly/base_customer.rb', line 117

def subscribe_to_plan(plan, is_trial_expiring_on = nil) 
  subscriptions.last.terminate_changed_subscription if subscriptions.last

  subscription = subscriptions.build.tap do |new|
    [:payable_upfront, :description, :periodicity,
     :amount, :grace_period, :signup_price].each do |k|
      new[k] = plan[k]
    end
    new.plan = plan if plan.is_a?(Billingly::Plan)
    new.is_trial_expiring_on = is_trial_expiring_on
    new.subscribed_on = Time.now
    new.save!
    new.generate_next_invoice  
    on_subscription_success
  end
  self.deactivated_since = nil
  self.deactivation_reason = nil
  self.save!
  return subscription
end