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

@@ -88,6 +88,29 @@ module LedgerTestingHelper
currency: currency,
entryable: trade
)
when "income_trade"
security = Security.find_or_create_by!(ticker: entry_data[:ticker]) do |s|
s.name = entry_data[:ticker]
end
currency = entry_data[:currency] || created_account.currency
trade = Trade.new(
qty: 0,
security: security,
price: 0,
fee: 0,
currency: currency,
investment_activity_label: entry_data[:label] || "Interest"
)
created_account.entries.create!(
name: "#{entry_data[:label] || 'Interest'}: #{entry_data[:ticker]}",
date: entry_data[:date],
amount: -(entry_data[:amount].to_d),
currency: currency,
entryable: trade
)
end
end