Module: Freemium::Subscription

Includes:
Rates
Included in:
Subscription
Defined in:
lib/freemium/subscription.rb

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Rates

#daily_rate, #monthly_rate, #yearly_rate

Class Method Details

.included(base) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/freemium/subscription.rb', line 12

def self.included(base)
  base.class_eval do
    belongs_to :subscription_plan, :class_name => "SubscriptionPlan"
    belongs_to :subscribable, :polymorphic => true
    belongs_to :credit_card, :dependent => :destroy, :class_name => "CreditCard"
    has_many :coupon_redemptions, :conditions => "coupon_redemptions.expired_on IS NULL", :class_name => "CouponRedemption", :foreign_key => :subscription_id, :dependent => :destroy
    has_many :coupons, :through => :coupon_redemptions, :conditions => "coupon_redemptions.expired_on IS NULL"

    # Auditing
    has_many :transactions, :class_name => "AccountTransaction", :foreign_key => :subscription_id

    scope :paid, includes(:subscription_plan).where("subscription_plans.rate_cents > 0")
    scope :due, lambda {
      where(['paid_through <= ?', Date.today]) # could use the concept of a next retry date
    }
    scope :expired, lambda {
      where(['expire_on >= paid_through AND expire_on <= ?', Date.today])
    }

    before_validation :set_paid_through
    before_validation :set_started_on
    before_save :store_credit_card_offsite
    before_save :discard_credit_card_unless_paid
    before_destroy :cancel_in_remote_system

    after_create  :audit_create
    after_update  :audit_update
    after_destroy :audit_destroy

    validates_presence_of :subscribable
    validates_associated  :subscribable
    validates_presence_of :subscription_plan
    validates_presence_of :paid_through, :if => :paid?
    validates_presence_of :started_on
    validates_presence_of :credit_card, :if => :store_credit_card?
    validates_associated  :credit_card#, :if => :store_credit_card?

    validate :gateway_validates_credit_card
    validate :coupon_exist
  end
  base.extend ClassMethods
end

Instance Method Details

#coupon(date = Date.today) ⇒ Object



240
241
242
# File 'lib/freemium/subscription.rb', line 240

def coupon(date = Date.today)
  coupon_redemption(date).coupon rescue nil
end

#coupon=(coupon) ⇒ Object



233
234
235
236
237
238
# File 'lib/freemium/subscription.rb', line 233

def coupon=(coupon)
  if coupon
    s = ::CouponRedemption.new(:subscription => self, :coupon => coupon)
    coupon_redemptions << s
  end
end

#coupon_existObject



229
230
231
# File 'lib/freemium/subscription.rb', line 229

def coupon_exist
  self.errors.add :coupon, "could not be found for '#{@coupon_key}'" if !@coupon_key.blank? && ::Coupon.find_by_redemption_key(@coupon_key).nil?
end

#coupon_key=(coupon_key) ⇒ Object

Coupon Redemption



224
225
226
227
# File 'lib/freemium/subscription.rb', line 224

def coupon_key=(coupon_key)
  @coupon_key = coupon_key ? coupon_key.downcase : nil
  self.coupon = ::Coupon.find_by_redemption_key(@coupon_key) unless @coupon_key.blank?
end

#coupon_redemption(date = Date.today) ⇒ Object



244
245
246
247
248
249
# File 'lib/freemium/subscription.rb', line 244

def coupon_redemption(date = Date.today)
  return nil if coupon_redemptions.empty?
  active_coupons = coupon_redemptions.select{|c| c.active?(date)}
  return nil if active_coupons.empty?
  active_coupons.sort_by{|c| c.coupon.discount_percentage }.reverse.first
end

#credit(amount) ⇒ Object



337
338
339
340
341
342
343
344
345
346
347
# File 'lib/freemium/subscription.rb', line 337

def credit(amount)
  self.paid_through = if amount.cents % rate.cents == 0
    self.paid_through + (amount.cents / rate.cents).months
  else
    self.paid_through + (amount.cents / daily_rate.cents).days
  end

  # if they've paid again, then reset expiration
  self.expire_on = nil
  self.in_trial = false
end

#expire!Object

sends an expiration email, then downgrades to a free plan



294
295
296
297
298
299
300
301
# File 'lib/freemium/subscription.rb', line 294

def expire!
  Freemium.mailer.expiration_notice(self).deliver
  # downgrade to a free plan
  self.expire_on = Date.today
  self.subscription_plan = Freemium.expired_plan if Freemium.expired_plan
  self.destroy_credit_card
  self.save!
end

#expire_after_grace!(transaction = nil) ⇒ Object

sets the expiration for the subscription based on today and the configured grace period.



284
285
286
287
288
289
290
291
# File 'lib/freemium/subscription.rb', line 284

def expire_after_grace!(transaction = nil)
  return unless self.expire_on.nil? # You only set this once subsequent failed transactions shouldn't affect expiration
  self.expire_on = [Date.today, paid_through].max + Freemium.days_grace
  transaction.message = "now set to expire on #{self.expire_on}" if transaction
  Freemium.mailer.expiration_warning(self).deliver
  transaction.save! if transaction
  save!
end

#expired?Boolean

Returns:

  • (Boolean)


303
304
305
# File 'lib/freemium/subscription.rb', line 303

def expired?
  expire_on and expire_on <= Date.today
end

#gatewayObject



59
60
61
# File 'lib/freemium/subscription.rb', line 59

def gateway
  Freemium.gateway
end

#in_grace?Boolean

Returns:

  • (Boolean)


275
276
277
# File 'lib/freemium/subscription.rb', line 275

def in_grace?
  remaining_days < 0 and not expired?
end

#original_planObject



55
56
57
# File 'lib/freemium/subscription.rb', line 55

def original_plan
  @original_plan ||= ::SubscriptionPlan.find_by_id(subscription_plan_id_was) unless subscription_plan_id_was.nil?
end

#paid?Boolean

Returns:

  • (Boolean)


210
211
212
213
# File 'lib/freemium/subscription.rb', line 210

def paid?
  return false unless rate
  rate.cents > 0
end

#rate(options = {}) ⇒ Object

Rate



201
202
203
204
205
206
207
208
# File 'lib/freemium/subscription.rb', line 201

def rate(options = {})
  options = {:date => Date.today, :plan => self.subscription_plan}.merge(options)

  return nil unless options[:plan]
  value = options[:plan].rate
  value = self.coupon(options[:date]).discount(value) if self.coupon(options[:date])
  value
end

#receive_payment(transaction) ⇒ Object

extends the paid_through period according to how much money was received. when possible, avoids the days-per-month problem by checking if the money received is a multiple of the plan’s rate.

really, i expect the case where the received payment does not match the subscription plan’s rate to be very much an edge case.



324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/freemium/subscription.rb', line 324

def receive_payment(transaction)
  self.credit(transaction.amount)
  self.save!
  transaction.subscription.reload  # reloaded to that the paid_through date is correct
  transaction.message = "now paid through #{self.paid_through}"

  begin
    Freemium.mailer.invoice(transaction).deliver
  rescue => e
    transaction.message = "error sending invoice: #{e}"
  end
end

#receive_payment!(transaction) ⇒ Object

receives payment and saves the record



312
313
314
315
316
# File 'lib/freemium/subscription.rb', line 312

def receive_payment!(transaction)
  receive_payment(transaction)
  transaction.save!
  self.save!
end

#remaining_daysObject

if paid through today, returns zero



262
263
264
# File 'lib/freemium/subscription.rb', line 262

def remaining_days
  (self.paid_through - Date.today)
end

#remaining_days_of_graceObject

if under grace through today, returns zero



271
272
273
# File 'lib/freemium/subscription.rb', line 271

def remaining_days_of_grace
  (self.expire_on - Date.today - 1).to_i
end

#remaining_value(plan = self.subscription_plan) ⇒ Object

returns the value of the time between now and paid_through. will optionally interpret the time according to a certain subscription plan.



257
258
259
# File 'lib/freemium/subscription.rb', line 257

def remaining_value(plan = self.subscription_plan)
  self.daily_rate(:plan => plan) * remaining_days
end

#store_credit_card?Boolean

Allow for more complex logic to decide if a card should be stored

Returns:

  • (Boolean)


216
217
218
# File 'lib/freemium/subscription.rb', line 216

def store_credit_card?
  paid?
end