fix: cash flow forecast drops investments purchased within the charted window

The Issue 2 fix made the opening investment balance point-in-time
(only CDs purchased before startYear-01-01), with a comment promising
that later purchases would be re-added "when their purchase month is
processed in the forecast loop" — but that loop code never existed.
The loop only ever subtracted maturing CDs, never added purchased ones.

Result: every CD bought during the charted window vanished from the
chart. For Pine Creek (all 5 CDs purchased in 2026) the operating
investment line showed $0 instead of $65,000 and reserve showed
$10,000 instead of $60,032.

Fix: build a purchaseIndex (mirroring maturityIndex) of investments
purchased on/after startYear-01-01, keyed by purchase year-month, and
credit each CD's value to the running investment balance in its
purchase month — applied before the historical/forecast branch so it
works for both actual and projected months.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 09:42:56 -04:00
parent ce3dc79e47
commit 922674eca4

View File

@@ -1008,6 +1008,19 @@ export class ReportsService {
WHERE is_active = true AND maturity_date IS NOT NULL AND maturity_date > CURRENT_DATE WHERE is_active = true AND maturity_date IS NOT NULL AND maturity_date > CURRENT_DATE
`); `);
// ── 5b) Get investment purchases (cash converts to an investment balance in the
// month the CD is bought). Only investments purchased on/after startYear-01-01 are
// indexed here — anything earlier is already counted in the opening investment
// balance below. Without this, point-in-time opening balances would silently drop
// every CD bought during the charted window.
const purchases = await this.tenant.query(`
SELECT fund_type, current_value, purchase_date
FROM investment_accounts
WHERE is_active = true
AND purchase_date IS NOT NULL
AND purchase_date >= $1::date
`, [`${startYear}-01-01`]);
// ── 6) Get capital project planned expenses ── // ── 6) Get capital project planned expenses ──
const projectExpenses = await this.tenant.query(` const projectExpenses = await this.tenant.query(`
SELECT estimated_cost, target_year, target_month, fund_source SELECT estimated_cost, target_year, target_month, fund_source
@@ -1077,6 +1090,19 @@ export class ReportsService {
else maturityIndex[key].reserve += maturityTotal; else maturityIndex[key].reserve += maturityTotal;
} }
// Index investment purchases by year-month — added to the running investment
// balance in the month the CD was bought (applies to both historical & forecast
// months, since a purchase is a real event regardless of where "now" falls).
const purchaseIndex: Record<string, { operating: number; reserve: number }> = {};
for (const inv of purchases) {
const d = new Date(inv.purchase_date);
const key = `${d.getFullYear()}-${d.getMonth() + 1}`;
if (!purchaseIndex[key]) purchaseIndex[key] = { operating: 0, reserve: 0 };
const val = parseFloat(inv.current_value) || 0;
if (inv.fund_type === 'operating') purchaseIndex[key].operating += val;
else purchaseIndex[key].reserve += val;
}
// Index project expenses by year-month // Index project expenses by year-month
const projectIndex: Record<string, { operating: number; reserve: number }> = {}; const projectIndex: Record<string, { operating: number; reserve: number }> = {};
for (const p of projectExpenses) { for (const p of projectExpenses) {
@@ -1129,6 +1155,12 @@ export class ReportsService {
const isHistorical = isPastMonth && hasActuals; const isHistorical = isPastMonth && hasActuals;
const label = `${monthLabels[month - 1]} ${year}`; const label = `${monthLabels[month - 1]} ${year}`;
// Apply investment purchases for this month before branching — a CD bought
// this month raises the investment balance whether the month is actual or forecast.
const purchased = purchaseIndex[key] || { operating: 0, reserve: 0 };
runOpInv += purchased.operating;
runResInv += purchased.reserve;
if (isHistorical) { if (isHistorical) {
// Use actual journal entry changes from asset accounts // Use actual journal entry changes from asset accounts
const opChange = histIndex[`${year}-${month}-operating`] || 0; const opChange = histIndex[`${year}-${month}-operating`] || 0;