bikeindex/bike_index

View on GitHub
app/models/invoice.rb

Summary

Maintainability
A
2 hrs
Test Coverage
C
71%
# frozen_string_literal: true

# daily_maintenance_tasks updates all invoices that have expiring subscriptions every day
class Invoice < ApplicationRecord
  include Amountable # included for formatting stuff
  belongs_to :organization
  belongs_to :first_invoice, class_name: "Invoice" # Use subscription_first_invoice_id + subscription_first_invoice, NOT THIS

  has_many :invoice_organization_features, dependent: :destroy
  has_many :organization_features, through: :invoice_organization_features
  has_many :payments

  validates :organization, :currency, presence: true

  before_save :set_calculated_attributes
  after_commit :update_organization

  scope :first_invoice, -> { where(first_invoice_id: nil) }
  scope :renewal_invoice, -> { where.not(first_invoice_id: nil) }
  scope :active, -> { where(is_active: true) }
  scope :inactive, -> { where(is_active: false) }
  scope :paid, -> { where.not(amount_due_cents: 0) }
  scope :free, -> { where(amount_due_cents: 0) }
  scope :current, -> { active.where("subscription_end_at > ? AND subscription_start_at < ?", Time.current, Time.current) }
  scope :expired, -> { where.not(subscription_start_at: nil).where("subscription_end_at < ?", Time.current) }
  scope :future, -> { where("subscription_start_at > ?", Time.current) }
  scope :endless, -> { where(is_endless: true) }
  scope :not_endless, -> { where.not(is_endless: true) }
  scope :should_expire, -> { not_endless.where(is_active: true).where("subscription_end_at < ?", Time.current) }
  scope :should_activate, -> { where(is_active: false).where("subscription_start_at < ? AND subscription_end_at > ?", Time.current, Time.current) }

  attr_accessor :timezone

  def self.friendly_find(str)
    str = str[/\d+/] if str.is_a?(String)
    where(id: str).first
  end

  def self.feature_slugs
    includes(:organization_features).pluck(:feature_slugs).flatten.uniq
  end

  def law_enforcement_functionality_invoice?
    organization_features.pluck(:name).any? { |n| n.match?(/law enforcement/i) }
  end

  # Static, at least for now
  def subscription_duration
    1.year
  end

  def renewal_invoice?
    first_invoice_id.present?
  end

  # Alias - don't directly access the db attribute, because it might change
  def active?
    is_active
  end

  def endless?
    is_endless
  end

  def not_endless?
    !endless?
  end

  def expired?
    not_endless? && subscription_end_at && subscription_end_at < Time.current
  end

  def future?
    subscription_start_at && subscription_start_at > Time.current
  end

  def current?
    active? && !expired? && !future?
  end

  def was_active?
    !future? && (expired? && force_active || subscription_start_at.present? && paid_in_full?)
  end

  # Use db attribute here, because that's what matters
  def should_expire?
    is_active && expired?
  end

  def discount_cents
    feature_cost_cents - (amount_due_cents || 0)
  end

  def paid_in_full?
    amount_paid_cents.present? && amount_due_cents.present? && amount_paid_cents >= amount_due_cents
  end

  def costs_money?
    amount_due_cents > 0
  end

  def no_cost?
    !costs_money?
  end

  def paid_money_in_full?
    paid_in_full? && costs_money?
  end

  def subscription_first_invoice_id
    first_invoice_id || id
  end

  def subscription_first_invoice
    first_invoice || self
  end

  def subscription_invoices
    self.class.where(first_invoice_id: subscription_first_invoice_id).where.not(id: id)
  end

  def display_name
    "Invoice ##{id}"
  end

  def organization_feature_ids
    invoice_organization_features.pluck(:organization_feature_id)
  end

  # There can be multiple features of the same id. View the spec for additional info
  def organization_feature_ids=(val)
    # This isn't super efficient, but whateves
    val = val.to_s.split(",") unless val.is_a?(Array)
    new_features = val.map { |v| OrganizationFeature.where(id: v).first }.compact
    new_feature_ids = new_features.map(&:id)
    existing_feature_ids = invoice_organization_features.pluck(:organization_feature_id)
    (existing_feature_ids - new_feature_ids).uniq.each do |absent_id| # ids absent from new features
      invoice_organization_features.where(organization_feature_id: absent_id).delete_all
    end
    new_feature_ids.uniq.each do |feature_id|
      new_matching_ids = new_feature_ids.select { |i| i == feature_id }
      existing_matching_ids = existing_feature_ids.select { |i| i == feature_id }
      if new_matching_ids.count > existing_matching_ids.count
        (new_matching_ids.count - existing_matching_ids.count).times do
          invoice_organization_features.create(organization_feature_id: feature_id)
        end
      elsif new_matching_ids.count < existing_matching_ids.count
        (existing_matching_ids.count - new_matching_ids.count).times do
          invoice_organization_features.where(organization_feature_id: feature_id).first.delete
        end
      end
    end
  end

  def child_enabled_feature_slugs_string
    (child_enabled_feature_slugs || []).join(", ")
  end

  def child_enabled_feature_slugs_string=(val)
    return if val.blank?
    unless val.is_a?(Array)
      val = val.strip.split(",").map(&:strip)
    end
    valid_slugs = (val & feature_slugs)
    self.child_enabled_feature_slugs = valid_slugs
  end

  # So that we can read and write
  def start_at
    subscription_start_at
  end

  def end_at
    subscription_end_at
  end

  def start_at=(val)
    self.subscription_start_at = TimeParser.parse(val, timezone)
  end

  def end_at=(val)
    self.subscription_end_at = TimeParser.parse(val, timezone)
  end

  def amount_due
    amnt = (amount_due_cents.to_i / 100.00)
    amnt % 1 != 0 ? amnt : amnt.round
  end

  def amount_due=(val)
    self.amount_due_cents = val.to_f * 100
  end

  def amount_due_formatted
    MoneyFormater.money_format(amount_due_cents, currency)
  end

  def amount_paid_formatted
    MoneyFormater.money_format(amount_paid_cents, currency)
  end

  def discount_formatted
    MoneyFormater.money_format(-(discount_cents || 0), currency)
  end

  def previous_invoice
    return nil unless renewal_invoice?
    subscription_invoices.where("id < ?", id).reorder(:id).last || subscription_first_invoice
  end

  def following_invoice
    subscription_invoices.where("id > ?", id).reorder(:id).first
  end

  def feature_cost_cents
    organization_features.sum(:amount_cents)
  end

  def feature_slugs
    organization_features.pluck(:feature_slugs).flatten.uniq
  end

  def create_following_invoice
    return nil unless active? || was_active? || future?
    return following_invoice if following_invoice.present?
    new_invoice = organization.invoices.create(start_at: subscription_end_at,
      first_invoice_id: subscription_first_invoice_id)
    new_invoice.organization_feature_ids = organization_features.recurring.pluck(:id)
    new_invoice.reload
    new_invoice.update(child_enabled_feature_slugs: child_enabled_feature_slugs)
    new_invoice
  end

  def set_calculated_attributes
    self.amount_paid_cents = payments.sum(:amount_cents)
    if subscription_start_at.present?
      self.subscription_end_at ||= subscription_start_at + subscription_duration
    end
    self.is_active = !expired? && !future? && (force_active || paid_in_full?)
    self.child_enabled_feature_slugs ||= []
  end

  def update_organization
    UpdateOrganizationAssociationsWorker.perform_async(organization_id)
  end
end