diff --git a/app/controllers/concerns/accountable_resource.rb b/app/controllers/concerns/accountable_resource.rb index 9daa0ae2..a7537a5a 100644 --- a/app/controllers/concerns/accountable_resource.rb +++ b/app/controllers/concerns/accountable_resource.rb @@ -2,7 +2,7 @@ module AccountableResource extend ActiveSupport::Concern included do - include ScrollFocusable, Periodable + include ScrollFocusable, Periodable, StreamExtensions before_action :set_account, only: [ :show, :edit, :update, :destroy ] before_action :set_link_options, only: :new @@ -39,7 +39,10 @@ module AccountableResource @account = Current.family.accounts.create_and_sync(account_params.except(:return_to)) @account.lock_saved_attributes! - redirect_to account_params[:return_to].presence || @account, notice: t("accounts.create.success", type: accountable_type.name.underscore.humanize) + respond_to do |format| + format.html { redirect_to account_params[:return_to].presence || @account, notice: accountable_type.name.underscore.humanize + " account created" } + format.turbo_stream { stream_redirect_to account_params[:return_to].presence || account_path(@account), notice: accountable_type.name.underscore.humanize + " account created" } + end end def update @@ -54,7 +57,7 @@ module AccountableResource end # Update remaining account attributes - update_params = account_params.except(:return_to, :balance, :currency) + update_params = account_params.except(:return_to, :balance, :currency, :tracking_start_date) unless @account.update(update_params) @error_message = @account.errors.full_messages.join(", ") render :edit, status: :unprocessable_entity @@ -62,7 +65,11 @@ module AccountableResource end @account.lock_saved_attributes! - redirect_back_or_to @account, notice: t("accounts.update.success", type: accountable_type.name.underscore.humanize) + + respond_to do |format| + format.html { redirect_back_or_to @account, notice: accountable_type.name.underscore.humanize + " account updated" } + format.turbo_stream { stream_redirect_to @account, notice: accountable_type.name.underscore.humanize + " account updated" } + end end def destroy @@ -90,7 +97,7 @@ module AccountableResource def account_params params.require(:account).permit( - :name, :balance, :subtype, :currency, :accountable_type, :return_to, + :name, :balance, :subtype, :currency, :accountable_type, :return_to, :tracking_start_date, accountable_attributes: self.class.permitted_accountable_attributes ) end diff --git a/app/models/account.rb b/app/models/account.rb index 09800168..4421a690 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -57,20 +57,20 @@ class Account < ApplicationRecord class << self def create_and_sync(attributes) + start_date = attributes.delete(:tracking_start_date) || 2.years.ago.to_date 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 || account.balance account.entries.build( name: Valuation::Name.new("opening_anchor", account.accountable_type).to_s, - date: 2.years.ago.to_date, + date: start_date, amount: initial_balance, currency: account.currency, entryable: Valuation.new( kind: "opening_anchor", balance: initial_balance, - cash_balance: initial_balance, - currency: account.currency + cash_balance: initial_balance ) ) diff --git a/app/models/account/balance_updater.rb b/app/models/account/balance_updater.rb index 1b50a9b5..d1e9f01e 100644 --- a/app/models/account/balance_updater.rb +++ b/app/models/account/balance_updater.rb @@ -18,12 +18,16 @@ class Account::BalanceUpdater end valuation_entry = account.entries.valuations.find_or_initialize_by(date: date) do |entry| - entry.entryable = Valuation.new + entry.entryable = Valuation.new( + kind: "recon", + balance: balance, + cash_balance: balance + ) end valuation_entry.amount = balance valuation_entry.currency = currency if currency.present? - valuation_entry.name = valuation_name(valuation_entry.entryable, account) + valuation_entry.name = valuation_name(valuation_entry, account) valuation_entry.notes = notes if notes.present? valuation_entry.save! end diff --git a/app/models/valuation.rb b/app/models/valuation.rb index b3a823cf..3f6a57b5 100644 --- a/app/models/valuation.rb +++ b/app/models/valuation.rb @@ -11,6 +11,7 @@ class Valuation < ApplicationRecord # Each account can have at most 1 opening anchor and 1 current anchor. All valuations between these anchors should # be either "recon" or "snapshot". This ensures we can reliably construct the account balance history solely from Entries. validate :unique_anchor_per_account, if: -> { opening_anchor? || current_anchor? } + validate :manual_accounts_cannot_have_current_anchor private def unique_anchor_per_account @@ -26,4 +27,12 @@ class Valuation < ApplicationRecord errors.add(:kind, "#{kind.humanize} already exists for this account") end end + + def manual_accounts_cannot_have_current_anchor + return unless entry&.account + + if entry.account.unlinked? && current_anchor? + errors.add(:kind, "Manual accounts cannot have a current anchor") + end + end end diff --git a/app/views/accounts/_form.html.erb b/app/views/accounts/_form.html.erb index ef2e0af5..0f6b8eda 100644 --- a/app/views/accounts/_form.html.erb +++ b/app/views/accounts/_form.html.erb @@ -1,10 +1,12 @@ <%# locals: (account:, url:) %> <% if @error_message.present? %> - <%= render AlertComponent.new(message: @error_message, variant: :error) %> +
+ <%= render AlertComponent.new(message: @error_message, variant: :error) %> +
<% end %> -<%= styled_form_with model: account, url: url, scope: :account, data: { turbo: false }, class: "flex flex-col gap-4 justify-between grow text-primary" do |form| %> +<%= styled_form_with model: account, url: url, scope: :account, class: "flex flex-col gap-4 justify-between grow text-primary" do |form| %>
<%= form.hidden_field :accountable_type %> <%= form.hidden_field :return_to, value: params[:return_to] %> @@ -12,7 +14,19 @@ <%= form.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %> <% unless account.linked? %> - <%= form.money_field :balance, label: t(".balance"), required: true, default_currency: Current.family.currency %> + <%= form.money_field :balance, + label: t(".balance"), + required: true, + default_currency: Current.family.currency, + label_tooltip: "The current balance or value of the account, which is typically the balance reported by your financial institution." %> + + <% unless account.persisted? %> + <%= form.date_field :tracking_start_date, + label: "Tracking start date", + required: true, + value: 2.years.ago.to_date, + label_tooltip: "The date we will start tracking the balance for this account. If you're not sure, we recommend using the default of 2 years ago so net worth graphs have adequate historical data." %> + <% end %> <% end %> <%= yield form %> diff --git a/db/migrate/20250707130134_add_valuation_kind_field_for_anchors.rb b/db/migrate/20250707130134_add_valuation_kind_field_for_anchors.rb index 1152ad37..9d7caa9c 100644 --- a/db/migrate/20250707130134_add_valuation_kind_field_for_anchors.rb +++ b/db/migrate/20250707130134_add_valuation_kind_field_for_anchors.rb @@ -3,21 +3,18 @@ class AddValuationKindFieldForAnchors < ActiveRecord::Migration[7.2] add_column :valuations, :kind, :string, default: "recon" add_column :valuations, :balance, :decimal, precision: 19, scale: 4 add_column :valuations, :cash_balance, :decimal, precision: 19, scale: 4 - add_column :valuations, :currency, :string # Copy `amount` from Entry, set both `balance` and `cash_balance` to the same value on all Valuation records, and `currency` from Entry to Valuation execute <<-SQL UPDATE valuations SET balance = entries.amount, - cash_balance = entries.amount, - currency = entries.currency + cash_balance = entries.amount FROM entries WHERE entries.entryable_type = 'Valuation' AND entries.entryable_id = valuations.id SQL change_column_null :valuations, :kind, false - change_column_null :valuations, :currency, false change_column_null :valuations, :balance, false change_column_null :valuations, :cash_balance, false end @@ -26,6 +23,5 @@ class AddValuationKindFieldForAnchors < ActiveRecord::Migration[7.2] remove_column :valuations, :kind remove_column :valuations, :balance remove_column :valuations, :cash_balance - remove_column :valuations, :currency end end diff --git a/db/schema.rb b/db/schema.rb index af85f677..5e5f0ce7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -783,7 +783,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_07_07_130134) do t.string "kind", default: "recon", null: false t.decimal "balance", precision: 19, scale: 4, null: false t.decimal "cash_balance", precision: 19, scale: 4, null: false - t.string "currency", null: false end create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/test/fixtures/valuations.yml b/test/fixtures/valuations.yml index 21aeae24..f1a49c5d 100644 --- a/test/fixtures/valuations.yml +++ b/test/fixtures/valuations.yml @@ -1,2 +1,3 @@ -one: { } -two: { } \ No newline at end of file +one: + balance: 4995 + cash_balance: 4995 diff --git a/test/support/entries_test_helper.rb b/test/support/entries_test_helper.rb index a4f2013f..5fd328a6 100644 --- a/test/support/entries_test_helper.rb +++ b/test/support/entries_test_helper.rb @@ -16,16 +16,19 @@ module EntriesTestHelper end def create_valuation(attributes = {}) + entry_attributes = attributes.except(:kind) + valuation_attributes = attributes.slice(:kind) + entry_defaults = { account: accounts(:depository), name: "Valuation", date: 1.day.ago.to_date, currency: "USD", amount: 5000, - entryable: Valuation.new + entryable: Valuation.new(valuation_attributes.merge(balance: 5000, cash_balance: 5000)) } - Entry.create! entry_defaults.merge(attributes) + Entry.create! entry_defaults.merge(entry_attributes) end def create_trade(security, account:, qty:, date:, price: nil, currency: "USD")