Fix income trades (interest/dividend) wiping security holdings to $0 (#2673)

Income trades created via Trade::CreateForm#create_income_trade set
price: 0 on the Trade record. This zero price was ingested by
PortfolioCache#load_prices as a valid price source, causing
ForwardCalculator#build_holdings to compute holding amount = qty * 0 = $0,
and overriding the security's market price on that date.

Additionally, Balance::BaseCalculator#flows_for_date misclassified income
trades (qty=0) as sells, producing spurious non_cash_outflows.

Fixes:
- PortfolioCache#load_prices: filter out zero-price trades from trade
  price sources using .select { |t| t.entryable.price&.positive? }
- Balance::BaseCalculator#flows_for_date: separate income trades from
  regular trades using their qty (income trades have qty=0), so they
  contribute only to cash flows, not non-cash flows
- Balance::SyncCache#converted_entries: preserve the entryable association
  target on duped entries so that e.entryable.qty access doesn't N+1

Fixes #2672
This commit is contained in:
StalkerSea
2026-07-13 18:33:35 -05:00
committed by GitHub
parent c42a5742c9
commit e4cfee0267
6 changed files with 124 additions and 4 deletions

View File

@@ -36,6 +36,9 @@ class Balance::SyncCache
def converted_entries
@converted_entries ||= account.entries.excluding_split_parents.includes(:entryable).order(:date).to_a.map do |e|
converted_entry = e.dup
# dup does not copy the association cache, so the entryable would
# be re-fetched on access. Copy it to keep the preload active.
converted_entry.association(:entryable).target = e.entryable
custom_rate = e.entryable.exchange_rate if e.entryable.respond_to?(:exchange_rate)