class Reji::Subscription
Public Instance Methods
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 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 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
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
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 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 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 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
Determine if the subscription is no longer active.
# File lib/reji/subscription.rb, line 99 def cancelled !ends_at.nil? end
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
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 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
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 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
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
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
Determine if the subscription is incomplete.
# File lib/reji/subscription.rb, line 68 def incomplete stripe_status == 'incomplete' end
Determine if the subscription has an incomplete payment.
# File lib/reji/subscription.rb, line 465 def incomplete_payment? past_due || incomplete end
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 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
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
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
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 the subscription as cancelled.
# File lib/reji/subscription.rb, line 377 def mark_as_cancelled update({ stripe_status: 'canceled', ends_at: Time.current, }) end
Determine if the subscription has multiple plans.
# File lib/reji/subscription.rb, line 41 def multiple_plans? stripe_plan.nil? end
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
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
Determine if the subscription is past due.
# File lib/reji/subscription.rb, line 73 def past_due stripe_status == 'past_due' end
Determine if the subscription has pending updates.
# File lib/reji/subscription.rb, line 408 def pending !as_stripe_subscription.pending_update.nil? end
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
Determine if the subscription is recurring and not on trial.
# File lib/reji/subscription.rb, line 94 def recurring !on_trial && !cancelled end
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 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
Determine if the subscription has a single plan.
# File lib/reji/subscription.rb, line 46 def single_plan? !multiple_plans? end
Force the trial to end immediately.
# File lib/reji/subscription.rb, line 204 def skip_trial self.trial_ends_at = nil self end
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 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 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 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 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 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 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
Get the user that owns the subscription.
# File lib/reji/subscription.rb, line 36 def user owner end
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 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 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 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