mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 05:09:38 +02:00
loan: Set the first valuation as the original principal. (#2088)
Fix: #1645.
This commit is contained in:
parent
5cb2183bdf
commit
f181ba941f
10 changed files with 59 additions and 13 deletions
|
@ -2,6 +2,6 @@ class LoansController < ApplicationController
|
||||||
include AccountableResource
|
include AccountableResource
|
||||||
|
|
||||||
permitted_accountable_attributes(
|
permitted_accountable_attributes(
|
||||||
:id, :rate_type, :interest_rate, :term_months
|
:id, :rate_type, :interest_rate, :term_months, :initial_balance
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,6 +34,7 @@ class Account < ApplicationRecord
|
||||||
def create_and_sync(attributes)
|
def create_and_sync(attributes)
|
||||||
attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty
|
attributes[:accountable_attributes] ||= {} # Ensure accountable is created, even if empty
|
||||||
account = new(attributes.merge(cash_balance: attributes[:balance]))
|
account = new(attributes.merge(cash_balance: attributes[:balance]))
|
||||||
|
initial_balance = attributes.dig(:accountable_attributes, :initial_balance)&.to_d || 0
|
||||||
|
|
||||||
transaction do
|
transaction do
|
||||||
# Create 2 valuations for new accounts to establish a value history for users to see
|
# 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(
|
account.entries.build(
|
||||||
name: "Initial Balance",
|
name: "Initial Balance",
|
||||||
date: 1.day.ago.to_date,
|
date: 1.day.ago.to_date,
|
||||||
amount: 0,
|
amount: initial_balance,
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
entryable: Account::Valuation.new
|
entryable: Account::Valuation.new
|
||||||
)
|
)
|
||||||
|
@ -92,11 +93,6 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def original_balance
|
|
||||||
balance_amount = balances.chronological.first&.balance || balance
|
|
||||||
Money.new(balance_amount, currency)
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_holdings
|
def current_holdings
|
||||||
holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc)
|
holdings.where(currency: currency, date: holdings.maximum(:date)).order(amount: :desc)
|
||||||
end
|
end
|
||||||
|
@ -104,9 +100,13 @@ class Account < ApplicationRecord
|
||||||
def update_with_sync!(attributes)
|
def update_with_sync!(attributes)
|
||||||
should_update_balance = attributes[:balance] && attributes[:balance].to_d != balance
|
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
|
transaction do
|
||||||
update!(attributes)
|
update!(attributes)
|
||||||
update_balance!(attributes[:balance]) if should_update_balance
|
update_balance!(attributes[:balance]) if should_update_balance
|
||||||
|
update_inital_balance!(attributes[:accountable_attributes][:initial_balance]) if should_update_initial_balance
|
||||||
end
|
end
|
||||||
|
|
||||||
sync_later
|
sync_later
|
||||||
|
@ -127,11 +127,34 @@ class Account < ApplicationRecord
|
||||||
end
|
end
|
||||||
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
|
def start_date
|
||||||
first_entry_date = entries.minimum(:date) || Date.current
|
first_entry_date = entries.minimum(:date) || Date.current
|
||||||
first_entry_date - 1.day
|
first_entry_date - 1.day
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def first_valuation
|
||||||
|
entries.account_valuations.order(:date).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_valuation_amount
|
||||||
|
first_valuation&.amount_money || balance_money
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def sync_balances
|
def sync_balances
|
||||||
strategy = linked? ? :reverse : :forward
|
strategy = linked? ? :reverse : :forward
|
||||||
|
|
|
@ -3,20 +3,24 @@ class Loan < ApplicationRecord
|
||||||
|
|
||||||
def monthly_payment
|
def monthly_payment
|
||||||
return nil if term_months.nil? || interest_rate.nil? || rate_type.nil? || rate_type != "fixed"
|
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
|
annual_rate = interest_rate / 100.0
|
||||||
monthly_rate = annual_rate / 12.0
|
monthly_rate = annual_rate / 12.0
|
||||||
|
|
||||||
if monthly_rate.zero?
|
if monthly_rate.zero?
|
||||||
payment = account.original_balance.amount / term_months
|
payment = account.loan.original_balance.amount / term_months
|
||||||
else
|
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
|
end
|
||||||
|
|
||||||
Money.new(payment.round, account.currency)
|
Money.new(payment.round, account.currency)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def original_balance
|
||||||
|
Money.new(account.first_valuation_amount, account.currency)
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def color
|
def color
|
||||||
"#D444F1"
|
"#D444F1"
|
||||||
|
|
|
@ -5,6 +5,13 @@
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<%= form.fields_for :accountable do |loan_form| %>
|
<%= form.fields_for :accountable do |loan_form| %>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<%= loan_form.money_field :initial_balance,
|
||||||
|
label: t("loans.form.initial_balance"),
|
||||||
|
default_currency: Current.family.currency,
|
||||||
|
required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<%= loan_form.number_field :interest_rate,
|
<%= loan_form.number_field :interest_rate,
|
||||||
label: t("loans.form.interest_rate"),
|
label: t("loans.form.interest_rate"),
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<%= summary_card title: t(".original_principal") do %>
|
<%= summary_card title: t(".original_principal") do %>
|
||||||
<%= format_money account.original_balance %>
|
<%= format_money account.loan.original_balance %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= summary_card title: t(".remaining_principal") do %>
|
<%= summary_card title: t(".remaining_principal") do %>
|
||||||
|
|
|
@ -6,6 +6,7 @@ en:
|
||||||
form:
|
form:
|
||||||
interest_rate: Interest rate
|
interest_rate: Interest rate
|
||||||
interest_rate_placeholder: '5.25'
|
interest_rate_placeholder: '5.25'
|
||||||
|
initial_balance: Original loan balance
|
||||||
rate_type: Rate type
|
rate_type: Rate type
|
||||||
term_months: Term (months)
|
term_months: Term (months)
|
||||||
term_months_placeholder: '360'
|
term_months_placeholder: '360'
|
||||||
|
|
5
db/migrate/20250405210514_add_initial_balance_field.rb
Normal file
5
db/migrate/20250405210514_add_initial_balance_field.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddInitialBalanceField < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :loans, :initial_balance, :decimal, precision: 19, scale: 4
|
||||||
|
end
|
||||||
|
end
|
1
db/schema.rb
generated
1
db/schema.rb
generated
|
@ -378,6 +378,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_04_11_140604) do
|
||||||
t.string "rate_type"
|
t.string "rate_type"
|
||||||
t.decimal "interest_rate", precision: 10, scale: 3
|
t.decimal "interest_rate", precision: 10, scale: 3
|
||||||
t.integer "term_months"
|
t.integer "term_months"
|
||||||
|
t.decimal "initial_balance", precision: 19, scale: 4
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
|
|
@ -22,7 +22,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
|
||||||
accountable_attributes: {
|
accountable_attributes: {
|
||||||
interest_rate: 5.5,
|
interest_rate: 5.5,
|
||||||
term_months: 60,
|
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 5.5, created_account.accountable.interest_rate
|
||||||
assert_equal 60, created_account.accountable.term_months
|
assert_equal 60, created_account.accountable.term_months
|
||||||
assert_equal "fixed", created_account.accountable.rate_type
|
assert_equal "fixed", created_account.accountable.rate_type
|
||||||
|
assert_equal 50000, created_account.accountable.initial_balance
|
||||||
|
|
||||||
assert_redirected_to created_account
|
assert_redirected_to created_account
|
||||||
assert_equal "Loan account created", flash[:notice]
|
assert_equal "Loan account created", flash[:notice]
|
||||||
|
@ -54,7 +56,8 @@ class LoansControllerTest < ActionDispatch::IntegrationTest
|
||||||
id: @account.accountable_id,
|
id: @account.accountable_id,
|
||||||
interest_rate: 4.5,
|
interest_rate: 4.5,
|
||||||
term_months: 48,
|
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 4.5, @account.accountable.interest_rate
|
||||||
assert_equal 48, @account.accountable.term_months
|
assert_equal 48, @account.accountable.term_months
|
||||||
assert_equal "fixed", @account.accountable.rate_type
|
assert_equal "fixed", @account.accountable.rate_type
|
||||||
|
assert_equal 48000, @account.accountable.initial_balance
|
||||||
|
|
||||||
assert_redirected_to @account
|
assert_redirected_to @account
|
||||||
assert_equal "Loan account updated", flash[:notice]
|
assert_equal "Loan account updated", flash[:notice]
|
||||||
|
|
|
@ -59,6 +59,7 @@ class AccountsTest < ApplicationSystemTestCase
|
||||||
|
|
||||||
test "can create loan account" do
|
test "can create loan account" do
|
||||||
assert_account_created "Loan" do
|
assert_account_created "Loan" do
|
||||||
|
fill_in "account[accountable_attributes][initial_balance]", with: 1000
|
||||||
fill_in "Interest rate", with: 5.25
|
fill_in "Interest rate", with: 5.25
|
||||||
select "Fixed", from: "Rate type"
|
select "Fixed", from: "Rate type"
|
||||||
fill_in "Term (months)", with: 360
|
fill_in "Term (months)", with: 360
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue