mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-27 09:09:41 +02:00
Checkpoint
This commit is contained in:
parent
15f8d827b5
commit
b7acef1e7a
9 changed files with 56 additions and 23 deletions
|
@ -2,7 +2,7 @@ module AccountableResource
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
include ScrollFocusable, Periodable
|
include ScrollFocusable, Periodable, StreamExtensions
|
||||||
|
|
||||||
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
|
before_action :set_account, only: [ :show, :edit, :update, :destroy ]
|
||||||
before_action :set_link_options, only: :new
|
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 = Current.family.accounts.create_and_sync(account_params.except(:return_to))
|
||||||
@account.lock_saved_attributes!
|
@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
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -54,7 +57,7 @@ module AccountableResource
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update remaining account attributes
|
# 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)
|
unless @account.update(update_params)
|
||||||
@error_message = @account.errors.full_messages.join(", ")
|
@error_message = @account.errors.full_messages.join(", ")
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
|
@ -62,7 +65,11 @@ module AccountableResource
|
||||||
end
|
end
|
||||||
|
|
||||||
@account.lock_saved_attributes!
|
@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
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -90,7 +97,7 @@ module AccountableResource
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.require(:account).permit(
|
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
|
accountable_attributes: self.class.permitted_accountable_attributes
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,20 +57,20 @@ class Account < ApplicationRecord
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def create_and_sync(attributes)
|
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
|
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 || account.balance
|
initial_balance = attributes.dig(:accountable_attributes, :initial_balance)&.to_d || account.balance
|
||||||
|
|
||||||
account.entries.build(
|
account.entries.build(
|
||||||
name: Valuation::Name.new("opening_anchor", account.accountable_type).to_s,
|
name: Valuation::Name.new("opening_anchor", account.accountable_type).to_s,
|
||||||
date: 2.years.ago.to_date,
|
date: start_date,
|
||||||
amount: initial_balance,
|
amount: initial_balance,
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
entryable: Valuation.new(
|
entryable: Valuation.new(
|
||||||
kind: "opening_anchor",
|
kind: "opening_anchor",
|
||||||
balance: initial_balance,
|
balance: initial_balance,
|
||||||
cash_balance: initial_balance,
|
cash_balance: initial_balance
|
||||||
currency: account.currency
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,12 +18,16 @@ class Account::BalanceUpdater
|
||||||
end
|
end
|
||||||
|
|
||||||
valuation_entry = account.entries.valuations.find_or_initialize_by(date: date) do |entry|
|
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
|
end
|
||||||
|
|
||||||
valuation_entry.amount = balance
|
valuation_entry.amount = balance
|
||||||
valuation_entry.currency = currency if currency.present?
|
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.notes = notes if notes.present?
|
||||||
valuation_entry.save!
|
valuation_entry.save!
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
# 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.
|
# 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 :unique_anchor_per_account, if: -> { opening_anchor? || current_anchor? }
|
||||||
|
validate :manual_accounts_cannot_have_current_anchor
|
||||||
|
|
||||||
private
|
private
|
||||||
def unique_anchor_per_account
|
def unique_anchor_per_account
|
||||||
|
@ -26,4 +27,12 @@ class Valuation < ApplicationRecord
|
||||||
errors.add(:kind, "#{kind.humanize} already exists for this account")
|
errors.add(:kind, "#{kind.humanize} already exists for this account")
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
<%# locals: (account:, url:) %>
|
<%# locals: (account:, url:) %>
|
||||||
|
|
||||||
<% if @error_message.present? %>
|
<% if @error_message.present? %>
|
||||||
|
<div class="mb-4">
|
||||||
<%= render AlertComponent.new(message: @error_message, variant: :error) %>
|
<%= render AlertComponent.new(message: @error_message, variant: :error) %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% 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| %>
|
||||||
<div class="grow space-y-2">
|
<div class="grow space-y-2">
|
||||||
<%= form.hidden_field :accountable_type %>
|
<%= form.hidden_field :accountable_type %>
|
||||||
<%= form.hidden_field :return_to, value: params[:return_to] %>
|
<%= 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") %>
|
<%= form.text_field :name, placeholder: t(".name_placeholder"), required: "required", label: t(".name_label") %>
|
||||||
|
|
||||||
<% unless account.linked? %>
|
<% 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 %>
|
<% end %>
|
||||||
|
|
||||||
<%= yield form %>
|
<%= yield form %>
|
||||||
|
|
|
@ -3,21 +3,18 @@ class AddValuationKindFieldForAnchors < ActiveRecord::Migration[7.2]
|
||||||
add_column :valuations, :kind, :string, default: "recon"
|
add_column :valuations, :kind, :string, default: "recon"
|
||||||
add_column :valuations, :balance, :decimal, precision: 19, scale: 4
|
add_column :valuations, :balance, :decimal, precision: 19, scale: 4
|
||||||
add_column :valuations, :cash_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
|
# 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
|
execute <<-SQL
|
||||||
UPDATE valuations
|
UPDATE valuations
|
||||||
SET
|
SET
|
||||||
balance = entries.amount,
|
balance = entries.amount,
|
||||||
cash_balance = entries.amount,
|
cash_balance = entries.amount
|
||||||
currency = entries.currency
|
|
||||||
FROM entries
|
FROM entries
|
||||||
WHERE entries.entryable_type = 'Valuation' AND entries.entryable_id = valuations.id
|
WHERE entries.entryable_type = 'Valuation' AND entries.entryable_id = valuations.id
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
change_column_null :valuations, :kind, false
|
change_column_null :valuations, :kind, false
|
||||||
change_column_null :valuations, :currency, false
|
|
||||||
change_column_null :valuations, :balance, false
|
change_column_null :valuations, :balance, false
|
||||||
change_column_null :valuations, :cash_balance, false
|
change_column_null :valuations, :cash_balance, false
|
||||||
end
|
end
|
||||||
|
@ -26,6 +23,5 @@ class AddValuationKindFieldForAnchors < ActiveRecord::Migration[7.2]
|
||||||
remove_column :valuations, :kind
|
remove_column :valuations, :kind
|
||||||
remove_column :valuations, :balance
|
remove_column :valuations, :balance
|
||||||
remove_column :valuations, :cash_balance
|
remove_column :valuations, :cash_balance
|
||||||
remove_column :valuations, :currency
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
1
db/schema.rb
generated
1
db/schema.rb
generated
|
@ -783,7 +783,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_07_07_130134) do
|
||||||
t.string "kind", default: "recon", null: false
|
t.string "kind", default: "recon", null: false
|
||||||
t.decimal "balance", precision: 19, scale: 4, null: false
|
t.decimal "balance", precision: 19, scale: 4, null: false
|
||||||
t.decimal "cash_balance", precision: 19, scale: 4, null: false
|
t.decimal "cash_balance", precision: 19, scale: 4, null: false
|
||||||
t.string "currency", null: false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
|
|
5
test/fixtures/valuations.yml
vendored
5
test/fixtures/valuations.yml
vendored
|
@ -1,2 +1,3 @@
|
||||||
one: { }
|
one:
|
||||||
two: { }
|
balance: 4995
|
||||||
|
cash_balance: 4995
|
||||||
|
|
|
@ -16,16 +16,19 @@ module EntriesTestHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_valuation(attributes = {})
|
def create_valuation(attributes = {})
|
||||||
|
entry_attributes = attributes.except(:kind)
|
||||||
|
valuation_attributes = attributes.slice(:kind)
|
||||||
|
|
||||||
entry_defaults = {
|
entry_defaults = {
|
||||||
account: accounts(:depository),
|
account: accounts(:depository),
|
||||||
name: "Valuation",
|
name: "Valuation",
|
||||||
date: 1.day.ago.to_date,
|
date: 1.day.ago.to_date,
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
amount: 5000,
|
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
|
end
|
||||||
|
|
||||||
def create_trade(security, account:, qty:, date:, price: nil, currency: "USD")
|
def create_trade(security, account:, qty:, date:, price: nil, currency: "USD")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue