From bbde9f2da2d055bdb1ed3a20efb934b7556c0bab Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 26 Sep 2025 15:46:04 +0000 Subject: [PATCH] financial projection fix for no college profile --- .build.hash | 2 +- src/components/CareerRoadmap.js | 16 ++-- src/utils/FinancialProjectionService.js | 97 ++++++++++++++----------- 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/.build.hash b/.build.hash index a6e3ca4..f29013c 100644 --- a/.build.hash +++ b/.build.hash @@ -1 +1 @@ -620419ccaec3e3b8f78ba81554844bd12f8a110d-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b +be65400be96a473622c09b3df6073c5837dacc82-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index b934adb..cf3f764 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -23,7 +23,7 @@ import MilestoneEditModal from './MilestoneEditModal.js'; import buildChartMarkers from '../utils/buildChartMarkers.js'; import getMissingFields, { MISSING_LABELS } from '../utils/getMissingFields.js'; import 'chartjs-adapter-date-fns'; -import apiFetch from '../auth/apiFetch.js'; +import authFetch from '../utils/authFetch.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import { getFullStateName } from '../utils/stateUtils.js'; @@ -38,7 +38,6 @@ import InfoTooltip from "./ui/infoTooltip.js"; import "../styles/legacy/MilestoneTimeline.legacy.css"; -const authFetch = apiFetch; // -------------- // Register ChartJS Plugins // -------------- @@ -626,12 +625,8 @@ useEffect(() => { useEffect(() => { - if ( - financialProfile && - scenarioRow && - collegeProfile - ) { - buildProjection(scenarioMilestones); // uses the latest scenarioMilestones + if (financialProfile && scenarioRow && collegeProfile !== null) { + buildProjection(scenarioMilestones); } }, [ financialProfile, @@ -1360,10 +1355,11 @@ const profRes = await authFetch(`/api/premium/milestones?careerProfileId=${caree const { milestones: profMs = [] } = ct.includes('application/json') ? await profRes.json() : { milestones: [] }; const merged = profMs; setScenarioMilestones(merged); - if (financialProfile && scenarioRow && collegeProfile) { + // use explicit null-check so {} works, and ensure we see latest value + if (financialProfile && scenarioRow && collegeProfile !== null) { buildProjection(merged); } // single rebuild -}, [financialProfile, scenarioRow, careerProfileId]); // ← NOTICE: no buildProjection here +}, [financialProfile, scenarioRow, collegeProfile, careerProfileId]); // ← NOTICE: no buildProjection here const handleMilestonesCreated = useCallback( (count = 0) => { diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index 041810d..50b1dae 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -86,10 +86,13 @@ function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') { ***************************************************/ function calculateLoanPayment(principal, annualRate, years) { if (principal <= 0) return 0; + // Guard: if term missing/zero, treat as no payment schedule + if (!Number.isFinite(years) || years <= 0) return 0; const monthlyRate = annualRate / 100 / 12; const numPayments = years * 12; if (monthlyRate === 0) { + // numPayments is guaranteed > 0 by the guard above return principal / numPayments; } return ( @@ -310,31 +313,29 @@ function simulateDrawdown(opts){ /*************************************************** * 4) TUITION CALC ***************************************************/ - const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0)); + const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0)); const totalTuitionCost = netAnnualTuition * finalProgramLength; - let lumpsPerYear, lumpsSchedule; - switch (academicCalendar) { - case 'semester': - lumpsPerYear = 2; - lumpsSchedule = [0, 6]; - break; - case 'quarter': - lumpsPerYear = 4; - lumpsSchedule = [0, 3, 6, 9]; - break; - case 'trimester': - lumpsPerYear = 3; - lumpsSchedule = [0, 4, 8]; - break; - case 'monthly': - default: - lumpsPerYear = 12; - lumpsSchedule = [...Array(12).keys()]; - break; + // Guard: if no college program, or no valid enrollment start, no tuition lumps. + let lumpsPerYear = 0; + let lumpsSchedule = []; + let lumpAmount = 0; + if (finalProgramLength > 0 && enrollmentStart && enrollmentStart.isValid()) { + switch (academicCalendar) { + case 'semester': + lumpsPerYear = 2; lumpsSchedule = [0, 6]; break; + case 'quarter': + lumpsPerYear = 4; lumpsSchedule = [0, 3, 6, 9]; break; + case 'trimester': + lumpsPerYear = 3; lumpsSchedule = [0, 4, 8]; break; + case 'monthly': + default: + lumpsPerYear = 12; lumpsSchedule = [...Array(12).keys()]; + break; + } + const lumpsTotal = lumpsPerYear * finalProgramLength; + lumpAmount = lumpsTotal > 0 ? (totalTuitionCost / lumpsTotal) : 0; } - - const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength); /*************************************************** * 5) LOAN PAYMENT (if not deferring) @@ -403,7 +404,7 @@ for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) { * 7.1 TUITION lumps ************************************************/ let tuitionCostThisMonth = 0; -if (stillInCollege && lumpsPerYear > 0 && enrollmentStart && enrollmentStart.isValid()) { + if (stillInCollege && lumpsPerYear > 0 && enrollmentStart && enrollmentStart.isValid()) { const monthsSinceEnroll = Math.max(0, currentSimDate.diff(enrollmentStart, 'months')); const academicYearIndex = Math.floor(monthsSinceEnroll / 12); const monthInAcadYear = monthsSinceEnroll % 12; @@ -588,43 +589,51 @@ if (loanDeferralUntilGraduation && wasInDeferral && !stillInCollege) { } const netSavings = netMonthlyIncome - actualExpensesPaid; - // (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result + // Clamp any non-finite numbers before pushing (belt & suspenders) + const fnum = (v) => (Number.isFinite(v) ? v : 0); + + if (![baseMonthlyIncome, monthlyFederalTax, monthlyStateTax, combinedTax, netMonthlyIncome, totalMonthlyExpenses, loanBalance].every(Number.isFinite)) { + console.warn('NON-FINITE @', currentSimDate.format('YYYY-MM'), { + baseMonthlyIncome, monthlyFederalTax, monthlyStateTax, combinedTax, netMonthlyIncome, + tuitionCostThisMonth, monthlyLoanPayment, extraPayment, totalMonthlyExpenses, loanBalance + }); +} + projectionData.push({ month: currentSimDate.format('YYYY-MM'), inCollege, // new stillInCollege, // new loanDeferralUntilGraduation, // new - grossMonthlyIncome: +baseMonthlyIncome.toFixed(2), - monthlyFederalTax: +monthlyFederalTax.toFixed(2), - monthlyStateTax: +monthlyStateTax.toFixed(2), - combinedTax: +combinedTax.toFixed(2), - netMonthlyIncome: +netMonthlyIncome.toFixed(2), - - totalExpenses: +actualExpensesPaid.toFixed(2), + grossMonthlyIncome: +fnum(baseMonthlyIncome).toFixed(2), + monthlyFederalTax: +fnum(monthlyFederalTax).toFixed(2), + monthlyStateTax: +fnum(monthlyStateTax).toFixed(2), + combinedTax: +fnum(combinedTax).toFixed(2), + netMonthlyIncome: +fnum(netMonthlyIncome).toFixed(2), + totalExpenses: +fnum(actualExpensesPaid).toFixed(2), - effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2), - effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2), + effectiveRetirementContribution: +fnum(effectiveRetirementContribution).toFixed(2), + effectiveEmergencyContribution: +fnum(effectiveEmergencyContribution).toFixed(2), - netSavings: +netSavings.toFixed(2), + netSavings: +fnum(netSavings).toFixed(2), // If you want to show the new running values, // you can keep them as is or store them: emergencySavings: (typeof currentEmergencySavings === 'number') - ? +currentEmergencySavings.toFixed(2) + ? +fnum(currentEmergencySavings).toFixed(2) : currentEmergencySavings, retirementSavings: (typeof currentRetirementSavings === 'number') - ? +currentRetirementSavings.toFixed(2) + ? +fnum(currentRetirementSavings).toFixed(2) : currentRetirementSavings, - loanBalance: +loanBalance.toFixed(2), + loanBalance: +fnum(loanBalance).toFixed(2), - loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2), - cashBalance: +cashBalance.toFixed(2), - totalSavings: +(currentEmergencySavings + currentRetirementSavings + cashBalance).toFixed(2), + loanPaymentThisMonth: +fnum(monthlyLoanPayment + extraPayment).toFixed(2), + cashBalance: +fnum(cashBalance).toFixed(2), + totalSavings: +fnum(currentEmergencySavings + currentRetirementSavings + cashBalance).toFixed(2), - fedYTDgross: +fedYTDgross.toFixed(2), - fedYTDtax: +fedYTDtax.toFixed(2), - stateYTDgross: +stateYTDgross.toFixed(2), - stateYTDtax: +stateYTDtax.toFixed(2) + fedYTDgross: +fnum(fedYTDgross).toFixed(2), + fedYTDtax: +fnum(fedYTDtax).toFixed(2), + stateYTDgross: +fnum(stateYTDgross).toFixed(2), + stateYTDtax: +fnum(stateYTDtax).toFixed(2) }); wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);