diff --git a/app/controllers/loans_controller.rb b/app/controllers/loans_controller.rb index b9968faf..961c5acf 100644 --- a/app/controllers/loans_controller.rb +++ b/app/controllers/loans_controller.rb @@ -2,6 +2,6 @@ class LoansController < ApplicationController include AccountableResource permitted_accountable_attributes( - :id, :rate_type, :interest_rate, :term_months + :id, :rate_type, :interest_rate, :term_months, :initial_balance ) end diff --git a/app/models/account.rb b/app/models/account.rb index cd1bd8aa..b93a13e1 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -34,6 +34,7 @@ class Account < ApplicationRecord def create_and_sync(attributes) attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty account = new(attributes.merge(cash_balance: attributes[:balance])) + initial_balance = attributes.dig(:accountable_attributes, :initial_balance)&.to_d || 0 transaction do # Create 2 valuations for new accounts to establish a value history for users to see @@ -47,7 +48,7 @@ class Account < ApplicationRecord account.entries.build( name: "Initial Balance", date: 1.day.ago.to_date, - amount: 0, + amount: initial_balance, currency: account.currency, entryable: Account::Valuation.new ) @@ -92,11 +93,6 @@ class Account < ApplicationRecord end end - def original_balance - balance_amount = balances.chronological.first&.balance || balance - Money.new(balance_amount, currency) - end - def current_holdings holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc) end @@ -104,9 +100,13 @@ class Account < ApplicationRecord def update_with_sync!(attributes) should_update_balance = attributes[:balance] && attributes[:balance].to_d != balance + initial_balance = attributes.dig(:accountable_attributes, :initial_balance) + should_update_initial_balance = initial_balance && initial_balance.to_d != accountable.initial_balance + transaction do update!(attributes) update_balance!(attributes[:balance]) if should_update_balance + update_inital_balance!(attributes[:accountable_attributes][:initial_balance]) if should_update_initial_balance end sync_later @@ -127,11 +127,34 @@ class Account < ApplicationRecord end end + def update_inital_balance!(initial_balance) + valuation = first_valuation + + if valuation + valuation.update! amount: initial_balance + else + entries.create! \ + date: Date.current, + name: "Initial Balance", + amount: initial_balance, + currency: currency, + entryable: Account::Valuation.new + end + end + def start_date first_entry_date = entries.minimum(:date) || Date.current first_entry_date - 1.day end + def first_valuation + entries.account_valuations.order(:date).first + end + + def first_valuation_amount + first_valuation&.amount_money || balance_money + end + private def sync_balances strategy = linked? ? :reverse : :forward diff --git a/app/models/loan.rb b/app/models/loan.rb index 14b4d084..283e112e 100644 --- a/app/models/loan.rb +++ b/app/models/loan.rb @@ -3,20 +3,24 @@ class Loan < ApplicationRecord def monthly_payment return nil if term_months.nil? || interest_rate.nil? || rate_type.nil? || rate_type != "fixed" - return Money.new(0, account.currency) if account.original_balance.amount.zero? || term_months.zero? + return Money.new(0, account.currency) if account.loan.original_balance.amount.zero? || term_months.zero? annual_rate = interest_rate / 100.0 monthly_rate = annual_rate / 12.0 if monthly_rate.zero? - payment = account.original_balance.amount / term_months + payment = account.loan.original_balance.amount / term_months else - payment = (account.original_balance.amount * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1) + payment = (account.loan.original_balance.amount * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1) end Money.new(payment.round, account.currency) end + def original_balance + Money.new(account.first_valuation_amount, account.currency) + end + class << self def color "#D444F1" diff --git a/app/views/loans/_form.html.erb b/app/views/loans/_form.html.erb index bceee968..73dd7bb7 100644 --- a/app/views/loans/_form.html.erb +++ b/app/views/loans/_form.html.erb @@ -5,6 +5,13 @@
<%= form.fields_for :accountable do |loan_form| %> +
+ <%= loan_form.money_field :initial_balance, + label: t("loans.form.initial_balance"), + default_currency: Current.family.currency, + required: true %> +
+
<%= loan_form.number_field :interest_rate, label: t("loans.form.interest_rate"), diff --git a/app/views/loans/_overview.html.erb b/app/views/loans/_overview.html.erb index db6824fb..bbeccb6e 100644 --- a/app/views/loans/_overview.html.erb +++ b/app/views/loans/_overview.html.erb @@ -2,7 +2,7 @@
<%= summary_card title: t(".original_principal") do %> - <%= format_money account.original_balance %> + <%= format_money account.loan.original_balance %> <% end %> <%= summary_card title: t(".remaining_principal") do %> diff --git a/config/locales/views/loans/en.yml b/config/locales/views/loans/en.yml index 930af8df..33eb76f3 100644 --- a/config/locales/views/loans/en.yml +++ b/config/locales/views/loans/en.yml @@ -6,6 +6,7 @@ en: form: interest_rate: Interest rate interest_rate_placeholder: '5.25' + initial_balance: Original loan balance rate_type: Rate type term_months: Term (months) term_months_placeholder: '360' diff --git a/db/migrate/20250405210514_add_initial_balance_field.rb b/db/migrate/20250405210514_add_initial_balance_field.rb new file mode 100644 index 00000000..5ddb974b --- /dev/null +++ b/db/migrate/20250405210514_add_initial_balance_field.rb @@ -0,0 +1,5 @@ +class AddInitialBalanceField < ActiveRecord::Migration[7.2] + def change + add_column :loans, :initial_balance, :decimal, precision: 19, scale: 4 + end +end diff --git a/db/schema.rb b/db/schema.rb index 8c37862a..a4252e7e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -378,6 +378,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_11_140604) do t.string "rate_type" t.decimal "interest_rate", precision: 10, scale: 3 t.integer "term_months" + t.decimal "initial_balance", precision: 19, scale: 4 end create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/test/controllers/loans_controller_test.rb b/test/controllers/loans_controller_test.rb index 627d82ac..774c5d1a 100644 --- a/test/controllers/loans_controller_test.rb +++ b/test/controllers/loans_controller_test.rb @@ -22,7 +22,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest accountable_attributes: { interest_rate: 5.5, term_months: 60, - rate_type: "fixed" + rate_type: "fixed", + initial_balance: 50000 } } } @@ -36,6 +37,7 @@ class LoansControllerTest < ActionDispatch::IntegrationTest assert_equal 5.5, created_account.accountable.interest_rate assert_equal 60, created_account.accountable.term_months assert_equal "fixed", created_account.accountable.rate_type + assert_equal 50000, created_account.accountable.initial_balance assert_redirected_to created_account assert_equal "Loan account created", flash[:notice] @@ -54,7 +56,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest id: @account.accountable_id, interest_rate: 4.5, term_months: 48, - rate_type: "fixed" + rate_type: "fixed", + initial_balance: 48000 } } } @@ -67,6 +70,7 @@ class LoansControllerTest < ActionDispatch::IntegrationTest assert_equal 4.5, @account.accountable.interest_rate assert_equal 48, @account.accountable.term_months assert_equal "fixed", @account.accountable.rate_type + assert_equal 48000, @account.accountable.initial_balance assert_redirected_to @account assert_equal "Loan account updated", flash[:notice] diff --git a/test/system/accounts_test.rb b/test/system/accounts_test.rb index ff8d4500..70ed7492 100644 --- a/test/system/accounts_test.rb +++ b/test/system/accounts_test.rb @@ -59,6 +59,7 @@ class AccountsTest < ApplicationSystemTestCase test "can create loan account" do assert_account_created "Loan" do + fill_in "account[accountable_attributes][initial_balance]", with: 1000 fill_in "Interest rate", with: 5.25 select "Fixed", from: "Rate type" fill_in "Term (months)", with: 360