From 3b6a5a573fc3eded0ea24a400c1fb938862642d2 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Wed, 9 Jul 2025 13:28:37 -0400 Subject: [PATCH] Clean up account creational methods --- app/models/account.rb | 9 ++ app/models/family.rb | 72 +-------- app/models/family/account_creatable.rb | 150 ++++++++++++++++++ test/models/account_test.rb | 3 +- test/models/family/account_creatable_test.rb | 151 +++++++++++++++++++ test/models/family_test.rb | 63 -------- 6 files changed, 313 insertions(+), 135 deletions(-) create mode 100644 app/models/family/account_creatable.rb create mode 100644 test/models/family/account_creatable_test.rb diff --git a/app/models/account.rb b/app/models/account.rb index fb074f5e..95ba5520 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -128,6 +128,15 @@ class Account < ApplicationRecord Account::BalanceUpdater.new(self, balance:, currency:, date:, notes:).update end + def update_currency!(new_currency) + raise "Currency cannot be changed" if linked? + + transaction do + update!(currency: new_currency) + entries.valuations.update_all(currency: new_currency) + end + end + def update_current_balance(balance:, cash_balance:) raise InvalidBalanceError, "Cash balance cannot exceed balance" if cash_balance > balance diff --git a/app/models/family.rb b/app/models/family.rb index 2a9fc553..5ad447a8 100644 --- a/app/models/family.rb +++ b/app/models/family.rb @@ -1,5 +1,5 @@ class Family < ApplicationRecord - include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable + include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable, AccountCreatable DATE_FORMATS = [ [ "MM-DD-YYYY", "%m-%d-%Y" ], @@ -116,74 +116,4 @@ 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 diff --git a/app/models/family/account_creatable.rb b/app/models/family/account_creatable.rb new file mode 100644 index 00000000..b3249854 --- /dev/null +++ b/app/models/family/account_creatable.rb @@ -0,0 +1,150 @@ +module Family::AccountCreatable + extend ActiveSupport::Concern + + def create_property_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_value, + cash_balance: 0, + accountable_type: Property, + opening_balance: purchase_price, + opening_date: purchase_date, + currency: currency + ) + end + + def create_vehicle_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_value, + cash_balance: 0, + accountable_type: Vehicle, + opening_balance: purchase_price, + opening_date: purchase_date, + currency: currency + ) + end + + def create_depository_account!(name:, current_balance:, opening_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_balance, + cash_balance: current_balance, + accountable_type: Depository, + opening_date: opening_date, + currency: currency + ) + end + + # Investment account values are built up by adding holdings / trades, not by initializing a "balance" + def create_investment_account!(name:, currency: nil) + create_manual_account!( + name: name, + balance: 0, + cash_balance: 0, + accountable_type: Investment, + opening_balance: 0, # Investment accounts start empty + opening_cash_balance: 0, + currency: currency + ) + end + + def create_other_asset_account!(name:, current_value:, purchase_price: nil, purchase_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_value, + cash_balance: 0, + accountable_type: OtherAsset, + opening_balance: purchase_price, + opening_date: purchase_date, + currency: currency + ) + end + + def create_other_liability_account!(name:, current_debt:, original_debt: nil, origination_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_debt, + cash_balance: 0, + accountable_type: OtherLiability, + opening_balance: original_debt, + opening_date: origination_date, + currency: currency + ) + end + + # For now, crypto accounts are very simple; we just track overall value + def create_crypto_account!(name:, current_value:, currency: nil) + create_manual_account!( + name: name, + balance: current_value, + cash_balance: current_value, + accountable_type: Crypto, + opening_balance: current_value, + opening_cash_balance: current_value, + currency: currency + ) + end + + def create_credit_card_account!(name:, current_debt:, opening_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_debt, + cash_balance: 0, + accountable_type: CreditCard, + opening_balance: 0, # Credit cards typically start with no debt + opening_date: opening_date, + currency: currency + ) + end + + def create_loan_account!(name:, current_principal:, original_principal: nil, origination_date: nil, currency: nil) + create_manual_account!( + name: name, + balance: current_principal, + cash_balance: 0, + accountable_type: Loan, + opening_balance: original_principal, + opening_date: origination_date, + currency: currency + ) + 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 + + private + + def create_manual_account!(name:, balance:, cash_balance:, accountable_type:, opening_balance: nil, opening_cash_balance: nil, opening_date: nil, currency: nil) + Family.transaction do + account = accounts.create!( + name: name, + balance: balance, + cash_balance: cash_balance, + currency: currency.presence || self.currency, + accountable: accountable_type.new + ) + + account.set_or_update_opening_balance!( + balance: opening_balance || balance, + cash_balance: opening_cash_balance || cash_balance, + date: opening_date + ) + + account + end + end +end diff --git a/test/models/account_test.rb b/test/models/account_test.rb index 5cb933d8..d7d2a06c 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -35,9 +35,10 @@ class AccountTest < ActiveSupport::TestCase # Currency updates earn their own method because updating an account currency incurs # side effects like recalculating balances, etc. test "can update the account currency" do - @account.update_currency("EUR") + @account.update_currency!("EUR") assert_equal "EUR", @account.currency + assert_equal "EUR", @account.entries.valuations.first.currency end # If a user has an opening balance (valuation) for their manual account and has 1+ transactions, the intent of diff --git a/test/models/family/account_creatable_test.rb b/test/models/family/account_creatable_test.rb new file mode 100644 index 00000000..a7b9b702 --- /dev/null +++ b/test/models/family/account_creatable_test.rb @@ -0,0 +1,151 @@ +require "test_helper" + +class Family::AccountCreatableTest < ActiveSupport::TestCase + def setup + @family = families(:dylan_family) + end + + test "creates manual property account" do + account = @family.create_property_account!( + name: "My House", + current_value: 450000, + purchase_price: 400000, + purchase_date: 1.year.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 400000) + assert_account_created_with(account: account, name: "My House", balance: 450000, cash_balance: 0) + end + + test "creates manual vehicle account" do + account = @family.create_vehicle_account!( + name: "My Car", + current_value: 25000, + purchase_price: 30000, + purchase_date: 2.years.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 30000) + assert_account_created_with(account: account, name: "My Car", balance: 25000, cash_balance: 0) + end + + test "creates manual depository account" do + account = @family.create_depository_account!( + name: "My Checking", + current_balance: 5000, + opening_date: 1.year.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 5000, cash_balance: 5000) + assert_account_created_with(account: account, name: "My Checking", balance: 5000, cash_balance: 5000) + end + + test "creates manual investment account" do + account = @family.create_investment_account!( + name: "My Brokerage" + ) + + assert_opening_valuation(account: account, balance: 0, cash_balance: 0) + assert_account_created_with(account: account, name: "My Brokerage", balance: 0, cash_balance: 0) + end + + test "creates manual other asset account" do + account = @family.create_other_asset_account!( + name: "Collectible", + current_value: 10000, + purchase_price: 5000, + purchase_date: 3.years.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 5000) + assert_account_created_with(account: account, name: "Collectible", balance: 10000, cash_balance: 0) + end + + test "creates manual other liability account" do + account = @family.create_other_liability_account!( + name: "Personal Loan", + current_debt: 5000, + original_debt: 10000, + origination_date: 2.years.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 10000) + assert_account_created_with(account: account, name: "Personal Loan", balance: 5000, cash_balance: 0) + end + + test "creates manual crypto account" do + account = @family.create_crypto_account!( + name: "Bitcoin Wallet", + current_value: 50000 + ) + + assert_opening_valuation(account: account, balance: 50000, cash_balance: 50000) + assert_account_created_with(account: account, name: "Bitcoin Wallet", balance: 50000, cash_balance: 50000) + end + + test "creates manual credit card account" do + account = @family.create_credit_card_account!( + name: "Visa Card", + current_debt: 2000, + opening_date: 6.months.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 0, cash_balance: 0) + assert_account_created_with(account: account, name: "Visa Card", balance: 2000, cash_balance: 0) + end + + test "creates manual loan account" do + account = @family.create_loan_account!( + name: "Home Mortgage", + current_principal: 200000, + original_principal: 250000, + origination_date: 5.years.ago.to_date + ) + + assert_opening_valuation(account: account, balance: 250000) + assert_account_created_with(account: account, name: "Home Mortgage", balance: 200000, cash_balance: 0) + end + + test "creates property account without purchase price" do + account = @family.create_property_account!( + name: "My House", + current_value: 500000 + ) + + assert_opening_valuation(account: account, balance: 500000) + assert_account_created_with(account: account, name: "My House", balance: 500000, cash_balance: 0) + end + + test "creates linked depository account" do + # TODO + end + + test "creates linked investment account" do + # TODO + end + + test "creates linked credit card account" do + # TODO + end + + test "creates linked loan account" do + # TODO + end + + private + def assert_account_created_with(account:, name:, balance:, cash_balance:) + assert_equal name, account.name + assert_equal balance, account.balance + assert_equal cash_balance, account.cash_balance + end + + def assert_opening_valuation(account:, balance:, cash_balance: 0) + valuations = account.valuations + assert_equal 1, valuations.count + + opening_valuation = valuations.first + assert_equal "opening_anchor", opening_valuation.kind + assert_equal balance, opening_valuation.balance + assert_equal cash_balance, opening_valuation.cash_balance + end +end diff --git a/test/models/family_test.rb b/test/models/family_test.rb index 07390db5..57c33e77 100644 --- a/test/models/family_test.rb +++ b/test/models/family_test.rb @@ -6,67 +6,4 @@ class FamilyTest < ActiveSupport::TestCase def setup @syncable = @family = families(:dylan_family) end - - test "creates manual property account" do - account = @family.create_property_account!( - name: "My House", - current_value: 450000, - purchase_price: 400000, - purchase_date: 1.year.ago.to_date - ) - - valuations = account.valuations - assert_equal 1, valuations.count - assert_equal "opening_anchor", valuations.first.kind - assert_equal 400000, valuations.first.balance - assert_equal 0, valuations.first.cash_balance - - assert_equal "My House", account.name - assert_equal 450000, account.balance - assert_equal 0, account.cash_balance - end - - test "creates manual vehicle account" do - # TODO - end - - test "creates manual depository account" do - # TODO - end - - test "creates manual investment account" do - # TODO - end - - test "creates manual other asset or liability account" do - # TODO - end - - test "creates manual crypto account" do - # TODO - end - - test "creates manual credit card account" do - # TODO - end - - test "creates manual loan account" do - # TODO - end - - test "creates linked depository account" do - # TODO - end - - test "creates linked investment account" do - # TODO - end - - test "creates linked credit card account" do - # TODO - end - - test "creates linked loan account" do - # TODO - end end