Skip to content

Commit

Permalink
Add start balance to manual accounts (#735)
Browse files Browse the repository at this point in the history
* Add start_balance to accounts

* Add tests

* Cleanup

* Refactor code and add tests

* Update physical cash demo account to be manual

* Do not populate start_balance in migration

* Cleanup

* Review fixes

* Revert calc change

* Update app/models/exchange_rate.rb

Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
Signed-off-by: Jakub Kottnauer <jk@jakubkottnauer.com>

* Add test

* Fix syncable bug and update csv tests

---------

Signed-off-by: Jakub Kottnauer <jk@jakubkottnauer.com>
Co-authored-by: Zach Gollwitzer <zach.gollwitzer@gmail.com>
  • Loading branch information
jakubkottnauer and zachgoll committed May 16, 2024
1 parent daf7ff8 commit 3d9ff3a
Show file tree
Hide file tree
Showing 13 changed files with 50 additions and 17 deletions.
7 changes: 5 additions & 2 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ def update
end

def create
@account = Current.family.accounts.build(account_params.except(:accountable_type))
@account = Current.family.accounts.build(account_params.except(:accountable_type, :start_date))
@account.accountable = Accountable.from_type(account_params[:accountable_type])&.new

if @account.save
@valuation = @account.valuations.new(date: account_params[:start_date] || Date.today, value: @account.balance, currency: @account.currency)
@valuation.save!

redirect_to accounts_path, notice: t(".success")
else
render "new", status: :unprocessable_entity
Expand Down Expand Up @@ -86,6 +89,6 @@ def set_account
end

def account_params
params.require(:account).permit(:name, :accountable_type, :balance, :currency, :subtype, :is_active)
params.require(:account).permit(:name, :accountable_type, :balance, :start_date, :currency, :subtype, :is_active)
end
end
10 changes: 3 additions & 7 deletions app/models/account/balance/calculator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(account, options = {})
def calculate
prior_balance = implied_start_balance

calculated_balances = ((@calc_start_date + 1.day)...Date.current).map do |date|
calculated_balances = ((@calc_start_date + 1.day)..Date.current).map do |date|
valuation = normalized_valuations.find { |v| v["date"] == date }

if valuation
Expand All @@ -30,8 +30,7 @@ def calculate

@daily_balances = [
{ date: @calc_start_date, balance: implied_start_balance, currency: @account.currency, updated_at: Time.current },
*calculated_balances,
{ date: Date.current, balance: @account.balance, currency: @account.currency, updated_at: Time.current } # Last balance must always match "source of truth"
*calculated_balances
]

if @account.foreign_currency?
Expand Down Expand Up @@ -66,10 +65,7 @@ def normalize_entries_to_account_currency(entries, value_key)
value = entry.send(value_key)

if currency != @account.currency
rate = ExchangeRate.find_by(base_currency: currency, converted_currency: @account.currency, date: date)
raise "Rate for #{currency} to #{@account.currency} not found" unless rate

value *= rate.rate
value = ExchangeRate.convert(value:, from: currency, to: @account.currency, date:)
currency = @account.currency
end

Expand Down
4 changes: 4 additions & 0 deletions app/models/account/syncable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def sync(start_date = nil)
calculator.calculate
self.balances.upsert_all(calculator.daily_balances, unique_by: :index_account_balances_on_account_id_date_currency_unique)
self.balances.where("date < ?", effective_start_date).delete_all
new_balance = calculator.daily_balances.select { |b| b[:currency] == self.currency }.last[:balance]
self.balance = new_balance
self.save!

update!(status: "ok", last_sync_date: Date.today)
rescue => e
update!(status: "error")
Expand Down
7 changes: 7 additions & 0 deletions app/models/exchange_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@ def find_rate_or_fetch(from:, to:, date:)
def get_rate_series(from, to, date_range)
where(base_currency: from, converted_currency: to, date: date_range).order(:date)
end

def convert(value:, from:, to:, date:)
rate = ExchangeRate.find_by(base_currency: from, converted_currency: to, date:)
raise "Conversion from: #{from} to: #{to} on: #{date} not found" unless rate

value * rate.rate
end
end
end
5 changes: 3 additions & 2 deletions app/views/accounts/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@
<%= form_with model: @account, url: accounts_path, scope: :account, html: { class: "m-5 mt-1 flex flex-col justify-between grow", data: { turbo: false } } do |f| %>
<div class="space-y-4 grow">
<%= f.hidden_field :accountable_type %>
<%= f.text_field :name, placeholder: t("accounts.new.name.placeholder"), required: "required", label: t("accounts.new.name.label"), autofocus: true %>
<%= f.text_field :name, placeholder: t(".name.placeholder"), required: "required", label: t(".name.label"), autofocus: true %>
<%= render "accounts/#{permitted_accountable_partial(@account.accountable_type)}", f: f %>
<%= f.money_field :balance_money, label: "Balance", required: "required" %>
<%= f.money_field :balance_money, label: t(".balance.label"), required: "required" %>
<%= f.date_field :date, label: t(".start_date.label"), required: true, max: Date.today, value: Date.today %>
</div>
<%= f.submit "Add #{@account.accountable.model_name.human.downcase}" %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/transactions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<%= f.collection_select :account_id, Current.family.accounts.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true %>
<%= f.money_field :amount_money, label: t(".amount"), required: true %>
<%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_prompt"), label: t(".category") }, required: true %>
<%= f.date_field :date, label: t(".date"), required: true %>
<%= f.date_field :date, label: t(".date"), required: true, max: Date.today %>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion app/views/transactions/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<%= lucide_icon("chevron-right", class: "group-open:hidden text-gray-500 w-5 h-5") %>
</div>
</summary>
<%= f.date_field :date, label: "Date", "data-auto-submit-form-target": "auto" %>
<%= f.date_field :date, label: "Date", max: Date.today, "data-auto-submit-form-target": "auto" %>
<div class="h-2"></div>
<%= f.collection_select :account_id, Current.family.accounts, :id, :name, { prompt: "Select an Account", label: "Account", class: "text-gray-400" }, {class: "form-field__input cursor-not-allowed text-gray-400", disabled: "disabled"} %>
</details>
Expand Down
2 changes: 1 addition & 1 deletion app/views/valuations/_form_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</div>
</div>
<div class="flex items-center justify-between grow">
<%= f.date_field :date, required: "required", class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
<%= f.date_field :date, required: "required", max: Date.today, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
<%= f.number_field :value, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
</div>
<div class="w-[296px] flex gap-2 justify-end items-center">
Expand Down
4 changes: 4 additions & 0 deletions config/locales/views/account/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ en:
index:
new_account: New account
new:
balance:
label: Balance
currency:
all_others: All Others
popular: Popular
name:
label: Account name
placeholder: Example account name
select_accountable_type: What would you like to add?
start_date:
label: Start date
title: Add an account
show:
confirm_accept: Delete "%{name}"
Expand Down
12 changes: 11 additions & 1 deletion test/controllers/accounts_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
assert_response :ok
end

test "create" do
test "should create account" do
assert_difference -> { Account.count }, +1 do
post accounts_path, params: { account: { accountable_type: "Account::Credit" } }
assert_redirected_to accounts_url
end
end

test "should create a valuation together with account" do
balance = 700
start_date = 3.days.ago.to_date
post accounts_path, params: { account: { accountable_type: "Account::Credit", balance:, start_date: } }

new_valuation = Valuation.order(:created_at).last
assert new_valuation.value == balance
assert new_valuation.date == start_date
end
end
2 changes: 1 addition & 1 deletion test/fixtures/account/expected_balances.csv
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ date_offset,collectable,checking,savings_with_valuation_overrides,credit_card,eu
-3,550,5000,20500,1000,12000,13046.4,10000
-2,550,5000,20500,1000,12000,12982.8,10000
-1,550,5000,20500,1000,12000,13014,10000
0,550,5000,20000,1000,12000,13000.8,10000
0,550,5000,20500,1000,12000,13000.8,10000
2 changes: 1 addition & 1 deletion test/fixtures/family/expected_snapshots.csv
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ date_offset,net_worth,assets,liabilities,depositories,investments,loans,credits,
-3,48096.40,49096.40,1000.00,48546.40,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-2,48032.80,49032.80,1000.00,48482.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
-1,48064.00,49064.00,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
0,47550.80,48550.80,1000.00,48000.80,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
0,48050.80,49050.80,1000.00,48514.00,0.00,0.00,1000.00,0.00,0.00,550.00,0.00,0,0,3214.48,2447.715,-0.3132574667
8 changes: 8 additions & 0 deletions test/models/account/syncable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,12 @@ class Account::SyncableTest < ActiveSupport::TestCase

assert_equal 31, account.balances.count
end

test "account balance is updated after sync" do
account = accounts(:savings_with_valuation_overrides)

assert_changes -> { account.balance }, to: 20500 do
account.sync
end
end
end

0 comments on commit 3d9ff3a

Please sign in to comment.