2024-02-02 09:05:04 -06:00
|
|
|
class Account < ApplicationRecord
|
2024-08-16 12:13:48 -04:00
|
|
|
include Syncable, Monetizable, Issuable
|
2024-02-29 08:32:52 -05:00
|
|
|
|
2024-07-17 14:18:12 -04:00
|
|
|
validates :name, :balance, :currency, presence: true
|
2024-03-15 12:21:59 -07:00
|
|
|
|
2024-02-02 09:05:04 -06:00
|
|
|
belongs_to :family
|
2024-10-01 10:47:59 -04:00
|
|
|
belongs_to :import, optional: true
|
2024-11-15 13:49:37 -05:00
|
|
|
belongs_to :plaid_account, optional: true
|
2024-07-01 10:49:43 -04:00
|
|
|
|
2024-10-01 10:47:59 -04:00
|
|
|
has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
|
2024-07-01 10:49:43 -04:00
|
|
|
has_many :entries, dependent: :destroy, class_name: "Account::Entry"
|
|
|
|
has_many :transactions, through: :entries, source: :entryable, source_type: "Account::Transaction"
|
|
|
|
has_many :valuations, through: :entries, source: :entryable, source_type: "Account::Valuation"
|
2024-07-16 09:26:49 -04:00
|
|
|
has_many :trades, through: :entries, source: :entryable, source_type: "Account::Trade"
|
2024-11-27 16:01:50 -05:00
|
|
|
has_many :holdings, dependent: :destroy, class_name: "Account::Holding"
|
2024-03-29 12:53:08 -04:00
|
|
|
has_many :balances, dependent: :destroy
|
2024-08-16 12:13:48 -04:00
|
|
|
has_many :issues, as: :issuable, dependent: :destroy
|
2024-02-02 11:09:31 -06:00
|
|
|
|
2024-12-10 17:41:20 -05:00
|
|
|
monetize :balance, :cash_balance
|
2024-03-18 11:21:00 -04:00
|
|
|
|
2024-07-01 10:49:43 -04:00
|
|
|
enum :classification, { asset: "asset", liability: "liability" }, validate: { allow_nil: true }
|
2024-03-11 16:32:13 -04:00
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
scope :active, -> { where(is_active: true, scheduled_for_deletion: false) }
|
2024-03-11 16:32:13 -04:00
|
|
|
scope :assets, -> { where(classification: "asset") }
|
|
|
|
scope :liabilities, -> { where(classification: "liability") }
|
2024-04-16 12:44:31 -06:00
|
|
|
scope :alphabetically, -> { order(:name) }
|
2024-11-15 13:49:37 -05:00
|
|
|
scope :manual, -> { where(plaid_account_id: nil) }
|
2024-03-07 10:55:51 -05:00
|
|
|
|
2024-10-18 14:37:42 -04:00
|
|
|
has_one_attached :logo
|
|
|
|
|
2024-02-09 14:26:54 +00:00
|
|
|
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
2024-02-02 23:06:29 +00:00
|
|
|
|
2024-11-04 20:27:31 -05:00
|
|
|
accepts_nested_attributes_for :accountable, update_only: true
|
2024-08-23 08:47:08 -04:00
|
|
|
|
2024-07-08 09:04:59 -04:00
|
|
|
class << self
|
2024-08-02 17:09:25 -04:00
|
|
|
def by_group(period: Period.all, currency: Money.default_currency.iso_code)
|
2024-07-08 09:04:59 -04:00
|
|
|
grouped_accounts = { assets: ValueGroup.new("Assets", currency), liabilities: ValueGroup.new("Liabilities", currency) }
|
|
|
|
|
|
|
|
Accountable.by_classification.each do |classification, types|
|
|
|
|
types.each do |type|
|
2024-10-08 13:00:35 -04:00
|
|
|
accounts = self.where(accountable_type: type)
|
|
|
|
if accounts.any?
|
|
|
|
group = grouped_accounts[classification.to_sym].add_child_group(type, currency)
|
|
|
|
accounts.each do |account|
|
|
|
|
group.add_value_node(
|
|
|
|
account,
|
|
|
|
account.balance_money.exchange_to(currency, fallback_rate: 0),
|
|
|
|
account.series(period: period, currency: currency)
|
|
|
|
)
|
|
|
|
end
|
2024-07-08 09:04:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
grouped_accounts
|
|
|
|
end
|
|
|
|
|
2024-11-04 20:27:31 -05:00
|
|
|
def create_and_sync(attributes)
|
|
|
|
attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty
|
2024-12-10 17:41:20 -05:00
|
|
|
account = new(attributes.merge(cash_balance: attributes[:balance]))
|
2024-08-23 08:47:08 -04:00
|
|
|
|
2024-11-04 20:27:31 -05:00
|
|
|
transaction do
|
|
|
|
# Create 2 valuations for new accounts to establish a value history for users to see
|
|
|
|
account.entries.build(
|
|
|
|
name: "Current Balance",
|
2024-08-23 08:47:08 -04:00
|
|
|
date: Date.current,
|
|
|
|
amount: account.balance,
|
2024-07-08 09:04:59 -04:00
|
|
|
currency: account.currency,
|
|
|
|
entryable: Account::Valuation.new
|
2024-11-04 20:27:31 -05:00
|
|
|
)
|
|
|
|
account.entries.build(
|
|
|
|
name: "Initial Balance",
|
|
|
|
date: 1.day.ago.to_date,
|
|
|
|
amount: 0,
|
|
|
|
currency: account.currency,
|
|
|
|
entryable: Account::Valuation.new
|
|
|
|
)
|
2024-08-23 08:47:08 -04:00
|
|
|
|
|
|
|
account.save!
|
|
|
|
end
|
2024-11-04 20:27:31 -05:00
|
|
|
|
|
|
|
account.sync_later
|
|
|
|
account
|
2024-07-08 09:04:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-11-15 13:49:37 -05:00
|
|
|
def destroy_later
|
|
|
|
update!(scheduled_for_deletion: true)
|
|
|
|
DestroyJob.perform_later(self)
|
|
|
|
end
|
|
|
|
|
|
|
|
def sync_data(start_date: nil)
|
|
|
|
update!(last_synced_at: Time.current)
|
|
|
|
|
2024-12-10 17:41:20 -05:00
|
|
|
Syncer.new(self, start_date: start_date).run
|
2024-11-15 13:49:37 -05:00
|
|
|
end
|
|
|
|
|
2024-11-20 16:46:06 -05:00
|
|
|
def post_sync
|
2024-12-10 17:41:20 -05:00
|
|
|
broadcast_remove_to(family, target: "syncing-notice")
|
|
|
|
resolve_stale_issues
|
2024-11-20 16:46:06 -05:00
|
|
|
accountable.post_sync
|
|
|
|
end
|
|
|
|
|
2024-12-10 17:41:20 -05:00
|
|
|
def series(period: Period.last_30_days, currency: nil)
|
|
|
|
balance_series = balances.in_period(period).where(currency: currency || self.currency)
|
|
|
|
|
|
|
|
if balance_series.empty? && period.date_range.end == Date.current
|
|
|
|
TimeSeries.new([ { date: Date.current, value: balance_money.exchange_to(currency || self.currency) } ])
|
|
|
|
else
|
|
|
|
TimeSeries.from_collection(balance_series, :balance_money, favorable_direction: asset? ? "up" : "down")
|
|
|
|
end
|
|
|
|
rescue Money::ConversionError
|
|
|
|
TimeSeries.new([])
|
|
|
|
end
|
|
|
|
|
2024-10-08 17:16:37 -04:00
|
|
|
def original_balance
|
2024-10-09 17:20:38 -04:00
|
|
|
balance_amount = balances.chronological.first&.balance || balance
|
|
|
|
Money.new(balance_amount, currency)
|
2024-10-08 17:16:37 -04:00
|
|
|
end
|
|
|
|
|
2024-12-12 08:56:52 -05:00
|
|
|
def current_holdings
|
|
|
|
holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc)
|
2024-02-20 09:07:55 -05:00
|
|
|
end
|
|
|
|
|
2024-06-21 16:23:28 -04:00
|
|
|
def favorable_direction
|
|
|
|
classification == "asset" ? "up" : "down"
|
|
|
|
end
|
|
|
|
|
2024-10-08 17:16:37 -04:00
|
|
|
def update_with_sync!(attributes)
|
|
|
|
transaction do
|
|
|
|
update!(attributes)
|
|
|
|
update_balance!(attributes[:balance]) if attributes[:balance]
|
|
|
|
end
|
|
|
|
|
|
|
|
sync_later
|
|
|
|
end
|
|
|
|
|
2024-07-12 13:47:39 -04:00
|
|
|
def update_balance!(balance)
|
|
|
|
valuation = entries.account_valuations.find_by(date: Date.current)
|
|
|
|
|
|
|
|
if valuation
|
|
|
|
valuation.update! amount: balance
|
|
|
|
else
|
|
|
|
entries.create! \
|
|
|
|
date: Date.current,
|
|
|
|
amount: balance,
|
|
|
|
currency: currency,
|
|
|
|
entryable: Account::Valuation.new
|
|
|
|
end
|
|
|
|
end
|
2024-02-02 09:05:04 -06:00
|
|
|
end
|