1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-04 21:15:19 +02:00

Account creation methods and tests

This commit is contained in:
Zach Gollwitzer 2025-07-09 11:38:34 -04:00
parent 2e09d1a8c0
commit a7cd046563
4 changed files with 425 additions and 2 deletions

View file

@ -1,4 +1,6 @@
class Account < ApplicationRecord
InvalidBalanceError = Class.new(StandardError)
include Syncable, Monetizable, Chartable, Linkable, Enrichable
include AASM
@ -15,7 +17,7 @@ class Account < ApplicationRecord
has_many :holdings, dependent: :destroy
has_many :balances, dependent: :destroy
monetize :balance, :cash_balance
monetize :balance, :cash_balance, :non_cash_balance
enum :classification, { asset: "asset", liability: "liability" }, validate: { allow_nil: true }
@ -126,6 +128,87 @@ class Account < ApplicationRecord
Account::BalanceUpdater.new(self, balance:, currency:, date:, notes:).update
end
def update_current_balance(balance:, cash_balance:)
raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance
if opening_anchor_valuation.present? && valuations.where(kind: "recon").empty?
adjust_opening_balance_with_delta(balance:, cash_balance:)
else
reconcile_balance!(balance:, cash_balance:, date: Date.current)
end
end
def reconcile_balance!(balance:, cash_balance:, date:)
raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance
raise InvalidBalanceError, "Linked accounts cannot be reconciled" if linked?
existing_valuation = valuations.joins(:entry).where(kind: "recon", entry: { date: Date.current }).first
if existing_valuation.present?
existing_valuation.update!(
balance: balance,
cash_balance: cash_balance
)
else
entries.create!(
date: date,
name: Valuation::Name.new("recon", self.accountable_type),
amount: balance,
currency: self.currency,
entryable: Valuation.new(
kind: "recon",
balance: balance,
cash_balance: cash_balance
)
)
end
end
def adjust_opening_balance_with_delta(balance:, cash_balance:)
delta = self.balance - balance
cash_delta = self.cash_balance - cash_balance
set_or_update_opening_balance!(
balance: balance - delta,
cash_balance: cash_balance - cash_delta
)
end
def set_or_update_opening_balance!(balance:, cash_balance:, date: nil)
# A reasonable start date for most accounts to fill up adequate history for graphs
fallback_opening_date = 2.years.ago.to_date
raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance
transaction do
if opening_anchor_valuation
opening_anchor_valuation.update!(
balance: balance,
cash_balance: cash_balance
)
opening_anchor_valuation.entry.update!(amount: balance)
opening_anchor_valuation.entry.update!(date: date) unless date.nil?
opening_anchor_valuation
else
entry = entries.create!(
date: date || fallback_opening_date,
name: Valuation::Name.new("opening_anchor", self.accountable_type),
amount: balance,
currency: self.currency,
entryable: Valuation.new(
kind: "opening_anchor",
balance: balance,
cash_balance: cash_balance,
)
)
entry.valuation
end
end
end
def start_date
first_entry_date = entries.minimum(:date) || Date.current
first_entry_date - 1.day
@ -153,4 +236,20 @@ class Account < ApplicationRecord
def long_subtype_label
accountable_class.long_subtype_label_for(subtype) || accountable_class.display_name
end
# For depository accounts, this is 0 (total balance is liquid cash)
# For all other accounts, this represents "asset value" or "debt value"
# (i.e. Investment accounts would refer to this as "holdings value")
def non_cash_balance
balance - cash_balance
end
private
def opening_anchor_valuation
valuations.opening_anchor.first
end
def current_anchor_valuation
valuations.current_anchor.first
end
end

View file

@ -116,4 +116,74 @@ class Family < ApplicationRecord
def self_hoster?
Rails.application.config.app_mode.self_hosted?
end
def create_property_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
Family.transaction do
property = accounts.create!(
name: name,
balance: current_value,
cash_balance: 0,
currency: currency.presence || self.currency,
accountable: Property.new
)
property.set_or_update_opening_balance!(
balance: purchase_price || 0,
cash_balance: 0,
date: purchase_date
)
property
end
end
def create_vehicle_account(current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
# TODO
end
def create_depository_account(current_balance:, opening_date: nil, currency: nil)
# TODO
end
# Investment account values are built up by adding holdings / trades, not by initializing a "balance"
def create_investment_account(currency: nil)
# TODO
end
def create_other_asset_account(current_value:, purchase_price: nil, purchase_date: nil, currency: nil)
# TODO
end
def create_other_liability_account(current_debt:, original_debt: nil, origination_date: nil, currency: nil)
# TODO
end
# For now, crypto accounts are very simple; we just track overall value
def create_crypto_account(current_value:, currency: nil)
# TODO
end
def create_credit_card_account(current_debt:, opening_date: nil, currency: nil)
# TODO
end
def create_loan_account(current_principal:, original_principal: nil, origination_date: nil, currency: nil)
# TODO
end
def link_depository_account
# TODO
end
def link_investment_account
# TODO
end
def link_credit_card_account
# TODO
end
def link_loan_account
# TODO
end
end