class Reji::Subscription

Public Instance Methods

active() click to toggle source

Determine if the subscription is active.

# File lib/reji/subscription.rb, line 78
def active
  (ends_at.nil? || on_grace_period) &&
    stripe_status != 'incomplete' &&
    stripe_status != 'incomplete_expired' &&
    stripe_status != 'unpaid' &&
    (!Reji.deactivate_past_due || stripe_status != 'past_due')
end
add_plan(plan, quantity = 1, options = {}) click to toggle source

Add a new Stripe plan to the subscription.

# File lib/reji/subscription.rb, line 269
def add_plan(plan, quantity = 1, options = {})
  guard_against_incomplete

  if items.any? { |item| item.stripe_plan == plan }
    raise Reji::SubscriptionUpdateFailureError.duplicate_plan(self, plan)
  end

  subscription = as_stripe_subscription

  item = subscription.items.create({
    plan: plan,
    quantity: quantity,
    tax_rates: get_plan_tax_rates_for_payload(plan),
    payment_behavior: payment_behavior,
    proration_behavior: proration_behavior,
  }.merge(options))

  items.create({
    stripe_id: item.id,
    stripe_plan: plan,
    quantity: quantity,
  })

  if single_plan?
    update({
      stripe_plan: nil,
      quantity: nil,
    })
  end

  self
end
add_plan_and_invoice(plan, quantity = 1, options = {}) click to toggle source

Add a new Stripe plan to the subscription, and invoice immediately.

# File lib/reji/subscription.rb, line 303
def add_plan_and_invoice(plan, quantity = 1, options = {})
  always_invoice

  add_plan(plan, quantity, options)
end
anchor_billing_cycle_on(date = 'now') click to toggle source

Change the billing cycle anchor on a plan change.

# File lib/reji/subscription.rb, line 197
def anchor_billing_cycle_on(date = 'now')
  @billing_cycle_anchor = date

  self
end
as_stripe_subscription(expand = {}) click to toggle source

Get the subscription as a Stripe subscription object.

# File lib/reji/subscription.rb, line 498
def as_stripe_subscription(expand = {})
  Stripe::Subscription.retrieve(
    { id: stripe_id, expand: expand }, owner.stripe_options
  )
end
cancel() click to toggle source

Cancel the subscription at the end of the billing period.

# File lib/reji/subscription.rb, line 334
def cancel
  subscription = as_stripe_subscription

  subscription.cancel_at_period_end = true

  subscription = subscription.save

  self.stripe_status = subscription.status

  # If the user was on trial, we will set the grace period to end when the trial
  # would have ended. Otherwise, we'll retrieve the end of the billing period
  # period and make that the end of the grace period for this current user.
  self.ends_at = on_trial ? trial_ends_at : Time.zone.at(subscription.current_period_end)

  save

  self
end
cancel_now() click to toggle source

Cancel the subscription immediately.

# File lib/reji/subscription.rb, line 354
def cancel_now
  as_stripe_subscription.cancel({
    prorate: proration_behavior == 'create_prorations',
  })

  mark_as_cancelled

  self
end
cancel_now_and_invoice() click to toggle source

Cancel the subscription and invoice immediately.

# File lib/reji/subscription.rb, line 365
def cancel_now_and_invoice
  as_stripe_subscription.cancel({
    invoice_now: true,
    prorate: proration_behavior == 'create_prorations',
  })

  mark_as_cancelled

  self
end
cancelled() click to toggle source

Determine if the subscription is no longer active.

# File lib/reji/subscription.rb, line 99
def cancelled
  !ends_at.nil?
end
decrement_quantity(count = 1, plan = nil) click to toggle source

Decrement the quantity of the subscription.

# File lib/reji/subscription.rb, line 155
def decrement_quantity(count = 1, plan = nil)
  guard_against_incomplete

  if plan
    find_item_or_fail(plan)
      .set_proration_behavior(proration_behavior)
      .decrement_quantity(count)

    return self
  end

  guard_against_multiple_plans

  update_quantity([1, quantity - count].max, plan)
end
ended() click to toggle source

Determine if the subscription has ended and the grace period has expired.

# File lib/reji/subscription.rb, line 104
def ended
  !!(cancelled && !on_grace_period)
end
extend_trial(date) click to toggle source

Extend an existing subscription's trial period.

# File lib/reji/subscription.rb, line 211
def extend_trial(date)
  raise ArgumentError, "Extending a subscription's trial requires a date in the future." unless date.future?

  subscription = as_stripe_subscription
  subscription.trial_end = date.to_i
  subscription.save

  update(trial_ends_at: date)

  self
end
find_item_or_fail(plan) click to toggle source

Get the subscription item for the given plan.

# File lib/reji/subscription.rb, line 58
def find_item_or_fail(plan)
  items.where(stripe_plan: plan).first
end
get_plan_tax_rates_for_payload(plan) click to toggle source

Get the plan tax rates for the Stripe payload.

# File lib/reji/subscription.rb, line 458
def get_plan_tax_rates_for_payload(plan)
  tax_rates = user.plan_tax_rates

  tax_rates[plan] || nil unless tax_rates.empty?
end
guard_against_incomplete() click to toggle source

Make sure a subscription is not incomplete when performing changes.

# File lib/reji/subscription.rb, line 479
def guard_against_incomplete
  raise Reji::SubscriptionUpdateFailureError.incomplete_subscription(self) if incomplete
end
guard_against_multiple_plans() click to toggle source

Make sure a plan argument is provided when the subscription is a multi plan subscription.

# File lib/reji/subscription.rb, line 484
def guard_against_multiple_plans
  return unless multiple_plans?

  raise ArgumentError, 'This method requires a plan argument since the subscription has multiple plans.'
end
incomplete() click to toggle source

Determine if the subscription is incomplete.

# File lib/reji/subscription.rb, line 68
def incomplete
  stripe_status == 'incomplete'
end
incomplete_payment?() click to toggle source

Determine if the subscription has an incomplete payment.

# File lib/reji/subscription.rb, line 465
def incomplete_payment?
  past_due || incomplete
end
increment_and_invoice(count = 1, plan = nil) click to toggle source

Increment the quantity of the subscription, and invoice immediately.

# File lib/reji/subscription.rb, line 136
def increment_and_invoice(count = 1, plan = nil)
  guard_against_incomplete

  always_invoice

  if plan
    find_item_or_fail(plan)
      .set_proration_behavior(proration_behavior)
      .increment_quantity(count)

    return self
  end

  guard_against_multiple_plans

  increment_quantity(count, plan)
end
increment_quantity(count = 1, plan = nil) click to toggle source

Increment the quantity of the subscription.

# File lib/reji/subscription.rb, line 119
def increment_quantity(count = 1, plan = nil)
  guard_against_incomplete

  if plan
    find_item_or_fail(plan)
      .set_proration_behavior(proration_behavior)
      .increment_quantity(count)

    return self
  end

  guard_against_multiple_plans

  update_quantity(quantity + count, plan)
end
invoice(options = {}) click to toggle source

Invoice the subscription outside of the regular billing cycle.

# File lib/reji/subscription.rb, line 413
def invoice(options = {})
  user.invoice(options.merge({
    subscription: stripe_id,
  }))
rescue IncompletePaymentError => e
  # Set the new Stripe subscription status immediately when payment fails...
  update(stripe_status: e.payment.invoice.subscription.status)

  raise e
end
latest_invoice() click to toggle source

Get the latest invoice for the subscription.

# File lib/reji/subscription.rb, line 425
def latest_invoice
  stripe_subscription = as_stripe_subscription(['latest_invoice'])

  Invoice.new(user, stripe_subscription.latest_invoice)
end
latest_payment() click to toggle source

Get the latest payment for a Subscription.

# File lib/reji/subscription.rb, line 470
def latest_payment
  payment_intent = as_stripe_subscription(['latest_invoice.payment_intent'])
    .latest_invoice
    .payment_intent

  payment_intent ? Payment.new(payment_intent) : nil
end
mark_as_cancelled() click to toggle source

Mark the subscription as cancelled.

# File lib/reji/subscription.rb, line 377
def mark_as_cancelled
  update({
    stripe_status: 'canceled',
    ends_at: Time.current,
  })
end
multiple_plans?() click to toggle source

Determine if the subscription has multiple plans.

# File lib/reji/subscription.rb, line 41
def multiple_plans?
  stripe_plan.nil?
end
on_grace_period() click to toggle source

Determine if the subscription is within its grace period after cancellation.

# File lib/reji/subscription.rb, line 114
def on_grace_period
  !!(ends_at && ends_at.future?)
end
on_trial() click to toggle source

Determine if the subscription is within its trial period.

# File lib/reji/subscription.rb, line 109
def on_trial
  !!(trial_ends_at && trial_ends_at.future?)
end
past_due() click to toggle source

Determine if the subscription is past due.

# File lib/reji/subscription.rb, line 73
def past_due
  stripe_status == 'past_due'
end
pending() click to toggle source

Determine if the subscription has pending updates.

# File lib/reji/subscription.rb, line 408
def pending
  !as_stripe_subscription.pending_update.nil?
end
plan?(plan) click to toggle source

Determine if the subscription has a specific plan.

# File lib/reji/subscription.rb, line 51
def plan?(plan)
  return items.any? { |item| item.stripe_plan == plan } if multiple_plans?

  stripe_plan == plan
end
recurring() click to toggle source

Determine if the subscription is recurring and not on trial.

# File lib/reji/subscription.rb, line 94
def recurring
  !on_trial && !cancelled
end
remove_plan(plan) click to toggle source

Remove a Stripe plan from the subscription.

# File lib/reji/subscription.rb, line 310
def remove_plan(plan)
  raise Reji::SubscriptionUpdateFailureError.cannot_delete_last_plan(self) if single_plan?

  item = find_item_or_fail(plan)

  item.as_stripe_subscription_item.delete({
    proration_behavior: proration_behavior,
  })

  items.where(stripe_plan: plan).destroy_all

  if items.count < 2
    item = items.first

    update({
      stripe_plan: item.stripe_plan,
      quantity: quantity,
    })
  end

  self
end
resume() click to toggle source

Resume the cancelled subscription.

# File lib/reji/subscription.rb, line 385
def resume
  raise ArgumentError, 'Unable to resume subscription that is not within grace period.' unless on_grace_period

  subscription = as_stripe_subscription

  subscription.cancel_at_period_end = false

  subscription.trial_end = on_trial ? Time.zone.at(trial_ends_at).to_i : 'now'

  subscription = subscription.save

  # Finally, we will remove the ending timestamp from the user's record in the
  # local database to indicate that the subscription is active again and is
  # no longer "cancelled". Then we will save this record in the database.
  update({
    stripe_status: subscription.status,
    ends_at: nil,
  })

  self
end
single_plan?() click to toggle source

Determine if the subscription has a single plan.

# File lib/reji/subscription.rb, line 46
def single_plan?
  !multiple_plans?
end
skip_trial() click to toggle source

Force the trial to end immediately.

# File lib/reji/subscription.rb, line 204
def skip_trial
  self.trial_ends_at = nil

  self
end
swap(plans, options = {}) click to toggle source

Swap the subscription to new Stripe plans.

# File lib/reji/subscription.rb, line 224
def swap(plans, options = {})
  plans = [plans] unless plans.instance_of? Array

  raise ArgumentError, 'Please provide at least one plan when swapping.' if plans.empty?

  guard_against_incomplete

  items = merge_items_that_should_be_deleted_during_swap(parse_swap_plans(plans))

  stripe_subscription = Stripe::Subscription.update(
    stripe_id,
    get_swap_options(items, options),
    owner.stripe_options
  )

  update({
    stripe_status: stripe_subscription.status,
    stripe_plan: stripe_subscription.plan ? stripe_subscription.plan.id : nil,
    quantity: stripe_subscription.quantity,
    ends_at: nil,
  })

  stripe_subscription.items.each do |item|
    self.items.find_or_create_by(stripe_id: item.id) do |subscription_item|
      subscription_item.stripe_plan = item.plan.id
      subscription_item.quantity = item.quantity
    end
  end

  # Delete items that aren't attached to the subscription anymore...
  self.items.where('stripe_plan NOT IN (?)', items.values.pluck(:plan).compact).destroy_all

  Payment.new(stripe_subscription.latest_invoice.payment_intent).validate if incomplete_payment?

  self
end
swap_and_invoice(plans, options = {}) click to toggle source

Swap the subscription to new Stripe plans, and invoice immediately.

# File lib/reji/subscription.rb, line 262
def swap_and_invoice(plans, options = {})
  always_invoice

  swap(plans, options)
end
sync_stripe_status() click to toggle source

Sync the Stripe status of the subscription.

# File lib/reji/subscription.rb, line 87
def sync_stripe_status
  subscription = as_stripe_subscription

  update({ stripe_status: subscription.status })
end
sync_tax_percentage() click to toggle source

Sync the tax percentage of the user to the subscription.

# File lib/reji/subscription.rb, line 432
def sync_tax_percentage
  subscription = as_stripe_subscription

  subscription.tax_percentage = user.tax_percentage

  subscription.save
end
sync_tax_rates() click to toggle source

Sync the tax rates of the user to the subscription.

# File lib/reji/subscription.rb, line 441
def sync_tax_rates
  subscription = as_stripe_subscription

  subscription.default_tax_rates = user.tax_rates

  subscription.save

  items.each do |item|
    stripe_subscription_item = item.as_stripe_subscription_item

    stripe_subscription_item.tax_rates = get_plan_tax_rates_for_payload(item.stripe_plan)

    stripe_subscription_item.save
  end
end
update_quantity(quantity, plan = nil) click to toggle source

Update the quantity of the subscription.

# File lib/reji/subscription.rb, line 172
def update_quantity(quantity, plan = nil)
  guard_against_incomplete

  if plan
    find_item_or_fail(plan)
      .set_proration_behavior(proration_behavior)
      .update_quantity(quantity)

    return self
  end

  guard_against_multiple_plans

  stripe_subscription = as_stripe_subscription
  stripe_subscription.quantity = quantity
  stripe_subscription.payment_behavior = payment_behavior
  stripe_subscription.proration_behavior = proration_behavior
  stripe_subscription.save

  update(quantity: quantity)

  self
end
update_stripe_subscription(options = {}) click to toggle source

Update the underlying Stripe subscription information for the model.

# File lib/reji/subscription.rb, line 491
def update_stripe_subscription(options = {})
  Stripe::Subscription.update(
    stripe_id, options, owner.stripe_options
  )
end
user() click to toggle source

Get the user that owns the subscription.

# File lib/reji/subscription.rb, line 36
def user
  owner
end
valid() click to toggle source

Determine if the subscription is active, on trial, or within its grace period.

# File lib/reji/subscription.rb, line 63
def valid
  active || on_trial || on_grace_period
end

Protected Instance Methods

get_swap_options(items, options) click to toggle source

Get the options array for a swap operation.

# File lib/reji/subscription.rb, line 530
          def get_swap_options(items, options)
  payload = {
    items: items.values,
    payment_behavior: payment_behavior,
    proration_behavior: proration_behavior,
    expand: ['latest_invoice.payment_intent'],
  }

  payload[:cancel_at_period_end] = false if payload[:payment_behavior] != 'pending_if_incomplete'

  payload = payload.merge(options)

  payload[:billing_cycle_anchor] = @billing_cycle_anchor unless @billing_cycle_anchor.nil?

  payload[:trial_end] = on_trial ? trial_ends_at : 'now'

  payload
end
merge_items_that_should_be_deleted_during_swap(items) click to toggle source

Merge the items that should be deleted during swap into the given items collection.

# File lib/reji/subscription.rb, line 515
          def merge_items_that_should_be_deleted_during_swap(items)
  as_stripe_subscription.items.data.each do |stripe_subscription_item|
    plan = stripe_subscription_item.plan.id

    item = items.key?(plan) ? items[plan] : {}

    item[:deleted] = true if item.empty?

    items[plan] = item.merge({ id: stripe_subscription_item.id })
  end

  items
end
parse_swap_plans(plans) click to toggle source

Parse the given plans for a swap operation.

# File lib/reji/subscription.rb, line 505
          def parse_swap_plans(plans)
  plans.map do |plan|
    [plan, {
      plan: plan,
      tax_rates: get_plan_tax_rates_for_payload(plan),
    },]
  end.to_h
end