peterkeen/payola

View on GitHub
app/models/payola/subscription.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'aasm'

module Payola
  class Subscription < ActiveRecord::Base
    include Payola::GuidBehavior

    has_paper_trail if respond_to? :has_paper_trail

    validates_presence_of :email
    validates_presence_of :plan_id
    validates_presence_of :plan_type

    validate :conditional_stripe_token
    validates_presence_of :currency

    belongs_to :plan, Rails::VERSION::MAJOR > 4 ? { polymorphic: true, optional: true } : { polymorphic: true }
    belongs_to :owner, Rails::VERSION::MAJOR > 4 ? { polymorphic: true, optional: true } : { polymorphic: true }
    belongs_to :affiliate, Rails::VERSION::MAJOR > 4 ? { optional: true } : {}

    has_many :sales, class_name: 'Payola::Sale', as: :owner

    include AASM

    attr_accessor :old_plan, :old_quantity

    aasm column: 'state', skip_validation_on_save: true do
      state :pending, initial: true
      state :processing
      state :active
      state :canceled
      state :errored

      event :process, after: :start_subscription do
        transitions from: :pending, to: :processing
      end

      event :activate, after: :instrument_activate do
        transitions from: :processing, to: :active
      end

      event :cancel, after: :instrument_canceled do
        transitions from: :active, to: :canceled
      end

      event :fail, after: :instrument_fail do
        transitions from: [:pending, :processing], to: :errored
      end

      event :refund, after: :instrument_refund do
        transitions from: :finished, to: :refunded
      end
    end

    def name
      self.plan.name
    end

    def price
      self.plan.amount
    end

    def redirect_path(sale)
      self.plan.redirect_path(self)
    end

    def verifier
      @verifier ||= ActiveSupport::MessageVerifier.new(Payola.secret_key_for_sale(self), digest: 'SHA256')
    end

    def verify_charge
      begin
        self.verify_charge!
      rescue RuntimeError => e
        self.error = e.message
        self.fail!
      end
    end

    def verify_charge!
      if Payola.charge_verifier.arity > 1
        Payola.charge_verifier.call(self, custom_fields)
      else
        Payola.charge_verifier.call(self)
      end
    end

    def custom_fields
      if self.signed_custom_fields.present?
        verifier.verify(self.signed_custom_fields)
      else
        nil
      end
    end

    def sync_with!(stripe_sub)
      self.current_period_start = Time.at(stripe_sub.current_period_start)
      self.current_period_end   = Time.at(stripe_sub.current_period_end)
      self.ended_at             = Time.at(stripe_sub.ended_at) if stripe_sub.ended_at
      self.trial_start          = Time.at(stripe_sub.trial_start) if stripe_sub.trial_start
      self.trial_end            = Time.at(stripe_sub.trial_end) if stripe_sub.trial_end
      self.canceled_at          = Time.at(stripe_sub.canceled_at) if stripe_sub.canceled_at
      self.quantity             = stripe_sub.quantity
      self.stripe_status        = stripe_sub.status
      self.amount               = stripe_sub.plan.amount
      self.currency             = stripe_sub.plan.respond_to?(:currency) ? stripe_sub.plan.currency : Payola.default_currency
      self.cancel_at_period_end = stripe_sub.cancel_at_period_end

      # Support for discounts is added to stripe-ruby-mock in v2.2.0, 84f08eb
      self.coupon               = stripe_sub.discount && stripe_sub.discount.coupon.id if stripe_sub.respond_to?(:discount)

      self.save!
      self
    end

    def to_param
      guid
    end

    def instrument_plan_changed(old_plan)
      self.old_plan = old_plan
      Payola.instrument(instrument_key('plan_changed'), self)
      Payola.instrument(instrument_key('plan_changed', false), self)
    end

    def instrument_quantity_changed(old_quantity)
      self.old_quantity = old_quantity
      Payola.instrument(instrument_key('quantity_changed'), self)
      Payola.instrument(instrument_key('quantity_changed', false), self)
    end

    def redirector
      plan
    end

    def conditional_stripe_token
      # Don't require a Stripe token if the subscription has an owner - we'll try to reuse the Stripe customer from an existing successful subscription
      return true if owner.present?
      # Don't require a Stripe token if we're creating a subscription for an existing Stripe customer
      return true if stripe_customer_id.present?
      return true if plan.nil?
      if (plan.amount > 0 )
        if plan.respond_to?(:trial_period_days) and (plan.trial_period_days.nil? or ( plan.trial_period_days and !(plan.trial_period_days > 0) ))
          errors.add(:base, 'No Stripe token is present for a paid plan') if stripe_token.nil?
        end
      end
    end

    private

    def start_subscription
      Payola::StartSubscription.call(self)
    end

    def instrument_activate
      Payola.instrument(instrument_key('active'), self)
      Payola.instrument(instrument_key('active', false), self)
    end

    def instrument_canceled
      Payola.instrument(instrument_key('canceled'), self)
      Payola.instrument(instrument_key('canceled', false), self)
    end

    def instrument_fail
      Payola.instrument(instrument_key('failed'), self)
      Payola.instrument(instrument_key('failed', false), self)
    end

    def instrument_refund
      Payola.instrument(instrument_key('refunded'), self)
      Payola.instrument(instrument_key('refunded', false), self)
    end

    def instrument_key(instrument_type, include_class=true)
      if include_class
        "payola.#{plan_type}.subscription.#{instrument_type}"
      else
        "payola.subscription.#{instrument_type}"
      end
    end

  end
end