From 922674eca4869fab0f746a0c5846a664cec96857 Mon Sep 17 00:00:00 2001 From: olsch01 Date: Fri, 22 May 2026 09:42:56 -0400 Subject: [PATCH] fix: cash flow forecast drops investments purchased within the charted window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/modules/reports/reports.service.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/backend/src/modules/reports/reports.service.ts b/backend/src/modules/reports/reports.service.ts index cc9a393..9bb7918 100644 --- a/backend/src/modules/reports/reports.service.ts +++ b/backend/src/modules/reports/reports.service.ts @@ -1008,6 +1008,19 @@ export class ReportsService { 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 ── const projectExpenses = await this.tenant.query(` SELECT estimated_cost, target_year, target_month, fund_source @@ -1077,6 +1090,19 @@ export class ReportsService { 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 = {}; + 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 const projectIndex: Record = {}; for (const p of projectExpenses) { @@ -1129,6 +1155,12 @@ export class ReportsService { const isHistorical = isPastMonth && hasActuals; 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) { // Use actual journal entry changes from asset accounts const opChange = histIndex[`${year}-${month}-operating`] || 0;