From df367b420a6bb9b63d1c658867e35eb505db821a Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Fri, 18 Jul 2025 07:33:03 -0400 Subject: [PATCH] Initial data objects --- app/models/account/activity_feed_data.rb | 70 +++++++++++++++++++ app/models/account/reconcileable.rb | 4 +- app/models/transactions_feed_data.rb | 11 +++ .../_confirmation_contents.html.erb | 4 +- 4 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 app/models/account/activity_feed_data.rb create mode 100644 app/models/transactions_feed_data.rb diff --git a/app/models/account/activity_feed_data.rb b/app/models/account/activity_feed_data.rb new file mode 100644 index 00000000..d6e5b14c --- /dev/null +++ b/app/models/account/activity_feed_data.rb @@ -0,0 +1,70 @@ +# Data used to build the paginated feed of account "activity" (events like transfers, deposits, withdrawals, etc.) +# This data object is useful for avoiding N+1 queries and having an easy way to pass around the required data to the +# activity feed component in controllers and background jobs that refresh it. +class Account::ActivityFeedData + attr_reader :account + + def initialize(account, entries) + @account = account + @entries = entries + end + + # We read balances so we can show "start of day" -> "end of day" balances for each entry date group in the feed + def balances + end + + + def transfers + return [] unless has_transfers? + + @transfers ||= Transfer.where(inflow_transaction_id: transaction_ids).or(Transfer.where(outflow_transaction_id: transaction_ids)) + end + + # If the account has entries denominated in a different currency than the main account, we attach necessary + # exchange rates required to "roll up" the entry group balance into the normal account currency. + def exchange_rates + return [] unless needs_exchange_rates? + + @exchange_rates ||= begin + rate_requirements = required_exchange_rates + return [] if rate_requirements.empty? + + # Build a single SQL query with all date/currency pairs + conditions = rate_requirements.map do |req| + "(date = ? AND from_currency = ? AND to_currency = ?)" + end.join(" OR ") + + # Flatten the parameters array in the same order + params = rate_requirements.flat_map do |req| + [ req.date, req.from, req.to ] + end + + ExchangeRate.where(conditions, *params) + end + end + + private + attr_reader :entries + + RequiredExchangeRate = Data.define(:date, :from, :to) + + def needs_exchange_rates? + entries.any? { |entry| entry.currency != account.currency } + end + + def required_exchange_rates + multi_currency_entries = entries.select { |entry| entry.currency != account.currency } + + multi_currency_entries.map do |entry| + RequiredExchangeRate.new(date: entry.date, from: entry.currency, to: account.currency) + end.uniq + end + + def has_transfers? + entries.any? { |entry| entry.transaction? && entry.transaction.transfer? } + end + + def transaction_ids + entries.select { |entry| entry.transaction? }.pluck(:entryable_id) + end +end diff --git a/app/models/account/reconcileable.rb b/app/models/account/reconcileable.rb index b8805236..67a6b52c 100644 --- a/app/models/account/reconcileable.rb +++ b/app/models/account/reconcileable.rb @@ -3,13 +3,13 @@ module Account::Reconcileable def create_reconciliation(balance:, date:, dry_run: false) result = reconciliation_manager.reconcile_balance(balance: balance, date: date, dry_run: dry_run) - sync_later if result.success? + sync_later if result.success? && !dry_run result end def update_reconciliation(existing_valuation_entry, balance:, date:, dry_run: false) result = reconciliation_manager.reconcile_balance(balance: balance, date: date, existing_valuation_entry: existing_valuation_entry, dry_run: dry_run) - sync_later if result.success? + sync_later if result.success? && !dry_run result end diff --git a/app/models/transactions_feed_data.rb b/app/models/transactions_feed_data.rb new file mode 100644 index 00000000..c42ea2a3 --- /dev/null +++ b/app/models/transactions_feed_data.rb @@ -0,0 +1,11 @@ +class TransactionsFeedData + attr_reader :family + + def initialize(family, transactions) + @family = family + @transactions = transactions + end + + private + attr_reader :transactions +end diff --git a/app/views/valuations/_confirmation_contents.html.erb b/app/views/valuations/_confirmation_contents.html.erb index 19d2ff5f..d8fdf032 100644 --- a/app/views/valuations/_confirmation_contents.html.erb +++ b/app/views/valuations/_confirmation_contents.html.erb @@ -2,8 +2,8 @@
<% if account.investment? %> - <% holdings_value = reconciliation_dry_run.new_balance - reconciliation_dry_run.new_cash_balance %> - <% brokerage_cash = reconciliation_dry_run.new_cash_balance %> + <% brokerage_cash = reconciliation_dry_run.new_cash_balance || 0 %> + <% holdings_value = reconciliation_dry_run.new_balance - brokerage_cash %>

This will <%= action_verb %> the account value on <%= entry.date.strftime("%B %d, %Y") %> to: