diff --git a/backend/src/modules/health-scores/health-scores.service.ts b/backend/src/modules/health-scores/health-scores.service.ts index 6d0e3c9..78049cf 100644 --- a/backend/src/modules/health-scores/health-scores.service.ts +++ b/backend/src/modules/health-scores/health-scores.service.ts @@ -625,14 +625,16 @@ export class HealthScoresService { .filter((b: any) => b.account_type === 'expense') .reduce((s: number, b: any) => s + parseFloat(b.annual_total || '0'), 0); - // Components needing replacement within 5 years — use whichever source has data - const urgentComponents = useComponentsTable - ? reserveComponents.filter( - (c: any) => c.remaining_life_years !== null && parseFloat(c.remaining_life_years) <= 5, - ) - : reserveProjects.filter( - (p: any) => p.remaining_life_years !== null && parseFloat(p.remaining_life_years) <= 5, - ); + // Projects due within 5 years — based on planned date (target_year/target_month), + // NOT remaining_life_years. The planned date is the board's decision on when to act; + // remaining life is documentation-only reference info. + const now = new Date(); + const fiveYearsFromNow = new Date(now.getFullYear() + 5, now.getMonth(), 1); + const urgentProjects = reserveProjects.filter((p: any) => { + if (!p.target_year) return false; + const targetDate = new Date(parseInt(p.target_year), (parseInt(p.target_month) || 6) - 1, 1); + return targetDate <= fiveYearsFromNow; + }); // ── Build 12-month forward reserve cash flow projection ── @@ -773,7 +775,7 @@ export class HealthScoresService { totalProjectCost, annualReserveContribution, annualReserveExpenses, - urgentComponents, + urgentProjects, monthlySpecialAssessmentIncome, year, forecast, @@ -940,12 +942,13 @@ SCORING GUIDELINES: KEY FACTORS TO EVALUATE: 1. Percent funded (total reserve assets vs total replacement costs) -2. Annual contribution adequacy (is annual contribution enough to keep pace with aging components?) -3. Component urgency (components due within 5 years and their funding status) -4. Capital project readiness (are planned projects adequately funded?) +2. Annual contribution adequacy (is annual contribution enough to keep pace with planned projects?) +3. Project urgency — based ONLY on the "Planned Date" field. The Planned Date is the board's decision on when a project will be executed. Do NOT use "Useful Life" or "Remaining Life" to determine urgency — those are reference information only. A project is only urgent if its Planned Date falls within the next 1-3 years. +4. Capital project readiness (are planned projects adequately funded by their planned dates?) 5. Investment strategy (are reserves earning returns through CDs, money markets, etc.?) -6. Diversity of reserve components (is the full building covered?) +6. Diversity of reserve components (is the full scope of community infrastructure tracked?) 7. CRITICAL — Projected cash flow: Use the 12-MONTH RESERVE CASH FLOW FORECAST to assess future liquidity. The forecast shows month-by-month projected income (from special assessments collected from homeowners AND budgeted reserve income), expenses, capital project costs, and investment maturities returning cash. Check whether the reserve fund will have sufficient liquidity when capital projects are due. If special assessment income arrives before project costs, the position may be adequate even if current cash seems low. +8. IMPORTANT — Projects with no Planned Date or with "Not scheduled" should be noted but NOT treated as urgent or imminent. Only assess urgency for projects with actual planned dates. RESPONSE FORMAT: Respond with ONLY valid JSON (no markdown, no code fences): @@ -974,7 +977,8 @@ Provide 3-5 factors and 1-3 actionable recommendations. Be specific with dollar `- ${i.name} | ${i.investment_type} @ ${i.institution} | $${parseFloat(i.current_value || i.principal || '0').toFixed(2)} | Rate: ${parseFloat(i.interest_rate || '0').toFixed(2)}% | Maturity: ${i.maturity_date ? new Date(i.maturity_date).toLocaleDateString() : 'N/A'}`, ).join('\n'); - // Build component lines from reserve_components if available, otherwise from reserve-funded projects + // Build component lines from reserve_components if available, otherwise from reserve-funded projects. + // Use planned date (target_year/target_month) as the authoritative timeline, not remaining_life_years. const componentSource = data.reserveComponents.length > 0 ? data.reserveComponents : data.reserveProjects; const componentLines = componentSource.length === 0 ? 'No reserve components or reserve projects tracked.' @@ -982,7 +986,8 @@ Provide 3-5 factors and 1-3 actionable recommendations. Be specific with dollar const cost = parseFloat(c.replacement_cost || c.estimated_cost || '0'); const funded = parseFloat(c.current_fund_balance || '0'); const pct = cost > 0 ? ((funded / cost) * 100).toFixed(0) : '0'; - return `- ${c.name} [${c.category || 'N/A'}] | Life: ${c.useful_life_years || '?'}yr, Remaining: ${c.remaining_life_years || '?'}yr | Cost: $${cost.toFixed(0)} | Funded: $${funded.toFixed(0)} (${pct}%) | Condition: ${c.condition_rating || '?'}/10 | Annual Contribution: $${parseFloat(c.annual_contribution || '0').toFixed(0)}`; + const plannedDate = c.target_year ? `${c.target_year}/${c.target_month || '?'}` : 'Not scheduled'; + return `- ${c.name} [${c.category || 'N/A'}] | Planned Date: ${plannedDate} | Useful Life: ${c.useful_life_years || '?'}yr (reference only) | Cost: $${cost.toFixed(0)} | Funded: $${funded.toFixed(0)} (${pct}%) | Condition: ${c.condition_rating || '?'}/10 | Annual Contribution: $${parseFloat(c.annual_contribution || '0').toFixed(0)}`; }).join('\n'); const projectLines = data.projects.length === 0 @@ -995,13 +1000,14 @@ Provide 3-5 factors and 1-3 actionable recommendations. Be specific with dollar .map((b: any) => `- ${b.name} (${b.account_number}) [${b.account_type}]: $${parseFloat(b.annual_total || '0').toFixed(2)}/yr`) .join('\n') || 'No reserve budget line items.'; - const urgentLines = data.urgentComponents.length === 0 - ? 'None — no components due within 5 years.' - : data.urgentComponents.map((c: any) => { - const cost = parseFloat(c.replacement_cost || c.estimated_cost || '0'); - const funded = parseFloat(c.current_fund_balance || '0'); + const urgentLines = data.urgentProjects.length === 0 + ? 'None — no reserve projects planned within 5 years.' + : data.urgentProjects.map((p: any) => { + const cost = parseFloat(p.estimated_cost || '0'); + const funded = parseFloat(p.current_fund_balance || '0'); const gap = cost - funded; - return `- ${c.name}: ${c.remaining_life_years} years remaining, $${gap.toFixed(0)} funding gap`; + const targetDate = `${p.target_year}/${p.target_month || '?'}`; + return `- ${p.name}: planned for ${targetDate}, Cost: $${cost.toFixed(0)}, $${gap.toFixed(0)} funding gap`; }).join('\n'); const userPrompt = `Evaluate this HOA's reserve fund health. @@ -1027,10 +1033,10 @@ ${accountLines} === RESERVE INVESTMENTS === ${investmentLines} -=== RESERVE COMPONENTS (ordered by urgency) === +=== RESERVE COMPONENTS (ordered by planned date) === ${componentLines} -=== COMPONENTS DUE WITHIN 5 YEARS (URGENT) === +=== PROJECTS PLANNED WITHIN 5 YEARS (by planned date) === ${urgentLines} === CAPITAL PROJECTS ===