financial projection fix for no college profile
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

This commit is contained in:
Josh 2025-09-26 15:46:04 +00:00
parent c70aa42076
commit bbde9f2da2
3 changed files with 60 additions and 55 deletions

View File

@ -1 +1 @@
620419ccaec3e3b8f78ba81554844bd12f8a110d-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b be65400be96a473622c09b3df6073c5837dacc82-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b

View File

@ -23,7 +23,7 @@ import MilestoneEditModal from './MilestoneEditModal.js';
import buildChartMarkers from '../utils/buildChartMarkers.js'; import buildChartMarkers from '../utils/buildChartMarkers.js';
import getMissingFields, { MISSING_LABELS } from '../utils/getMissingFields.js'; import getMissingFields, { MISSING_LABELS } from '../utils/getMissingFields.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import apiFetch from '../auth/apiFetch.js'; import authFetch from '../utils/authFetch.js';
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js'; import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
import parseFloatOrZero from '../utils/ParseFloatorZero.js'; import parseFloatOrZero from '../utils/ParseFloatorZero.js';
import { getFullStateName } from '../utils/stateUtils.js'; import { getFullStateName } from '../utils/stateUtils.js';
@ -38,7 +38,6 @@ import InfoTooltip from "./ui/infoTooltip.js";
import "../styles/legacy/MilestoneTimeline.legacy.css"; import "../styles/legacy/MilestoneTimeline.legacy.css";
const authFetch = apiFetch;
// -------------- // --------------
// Register ChartJS Plugins // Register ChartJS Plugins
// -------------- // --------------
@ -626,12 +625,8 @@ useEffect(() => {
useEffect(() => { useEffect(() => {
if ( if (financialProfile && scenarioRow && collegeProfile !== null) {
financialProfile && buildProjection(scenarioMilestones);
scenarioRow &&
collegeProfile
) {
buildProjection(scenarioMilestones); // uses the latest scenarioMilestones
} }
}, [ }, [
financialProfile, 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 { milestones: profMs = [] } = ct.includes('application/json') ? await profRes.json() : { milestones: [] };
const merged = profMs; const merged = profMs;
setScenarioMilestones(merged); 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); buildProjection(merged);
} // single rebuild } // single rebuild
}, [financialProfile, scenarioRow, careerProfileId]); // ← NOTICE: no buildProjection here }, [financialProfile, scenarioRow, collegeProfile, careerProfileId]); // ← NOTICE: no buildProjection here
const handleMilestonesCreated = useCallback( const handleMilestonesCreated = useCallback(
(count = 0) => { (count = 0) => {

View File

@ -86,10 +86,13 @@ function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') {
***************************************************/ ***************************************************/
function calculateLoanPayment(principal, annualRate, years) { function calculateLoanPayment(principal, annualRate, years) {
if (principal <= 0) return 0; 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 monthlyRate = annualRate / 100 / 12;
const numPayments = years * 12; const numPayments = years * 12;
if (monthlyRate === 0) { if (monthlyRate === 0) {
// numPayments is guaranteed > 0 by the guard above
return principal / numPayments; return principal / numPayments;
} }
return ( return (
@ -310,31 +313,29 @@ function simulateDrawdown(opts){
/*************************************************** /***************************************************
* 4) TUITION CALC * 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; const totalTuitionCost = netAnnualTuition * finalProgramLength;
let lumpsPerYear, lumpsSchedule; // Guard: if no college program, or no valid enrollment start, no tuition lumps.
switch (academicCalendar) { let lumpsPerYear = 0;
case 'semester': let lumpsSchedule = [];
lumpsPerYear = 2; let lumpAmount = 0;
lumpsSchedule = [0, 6]; if (finalProgramLength > 0 && enrollmentStart && enrollmentStart.isValid()) {
break; switch (academicCalendar) {
case 'quarter': case 'semester':
lumpsPerYear = 4; lumpsPerYear = 2; lumpsSchedule = [0, 6]; break;
lumpsSchedule = [0, 3, 6, 9]; case 'quarter':
break; lumpsPerYear = 4; lumpsSchedule = [0, 3, 6, 9]; break;
case 'trimester': case 'trimester':
lumpsPerYear = 3; lumpsPerYear = 3; lumpsSchedule = [0, 4, 8]; break;
lumpsSchedule = [0, 4, 8]; case 'monthly':
break; default:
case 'monthly': lumpsPerYear = 12; lumpsSchedule = [...Array(12).keys()];
default: break;
lumpsPerYear = 12; }
lumpsSchedule = [...Array(12).keys()]; const lumpsTotal = lumpsPerYear * finalProgramLength;
break; lumpAmount = lumpsTotal > 0 ? (totalTuitionCost / lumpsTotal) : 0;
} }
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
/*************************************************** /***************************************************
* 5) LOAN PAYMENT (if not deferring) * 5) LOAN PAYMENT (if not deferring)
@ -403,7 +404,7 @@ for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
* 7.1 TUITION lumps * 7.1 TUITION lumps
************************************************/ ************************************************/
let tuitionCostThisMonth = 0; 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 monthsSinceEnroll = Math.max(0, currentSimDate.diff(enrollmentStart, 'months'));
const academicYearIndex = Math.floor(monthsSinceEnroll / 12); const academicYearIndex = Math.floor(monthsSinceEnroll / 12);
const monthInAcadYear = monthsSinceEnroll % 12; const monthInAcadYear = monthsSinceEnroll % 12;
@ -588,43 +589,51 @@ if (loanDeferralUntilGraduation && wasInDeferral && !stillInCollege) {
} }
const netSavings = netMonthlyIncome - actualExpensesPaid; 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({ projectionData.push({
month: currentSimDate.format('YYYY-MM'), month: currentSimDate.format('YYYY-MM'),
inCollege, // new inCollege, // new
stillInCollege, // new stillInCollege, // new
loanDeferralUntilGraduation, // new loanDeferralUntilGraduation, // new
grossMonthlyIncome: +baseMonthlyIncome.toFixed(2), grossMonthlyIncome: +fnum(baseMonthlyIncome).toFixed(2),
monthlyFederalTax: +monthlyFederalTax.toFixed(2), monthlyFederalTax: +fnum(monthlyFederalTax).toFixed(2),
monthlyStateTax: +monthlyStateTax.toFixed(2), monthlyStateTax: +fnum(monthlyStateTax).toFixed(2),
combinedTax: +combinedTax.toFixed(2), combinedTax: +fnum(combinedTax).toFixed(2),
netMonthlyIncome: +netMonthlyIncome.toFixed(2), netMonthlyIncome: +fnum(netMonthlyIncome).toFixed(2),
totalExpenses: +fnum(actualExpensesPaid).toFixed(2),
totalExpenses: +actualExpensesPaid.toFixed(2),
effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2), effectiveRetirementContribution: +fnum(effectiveRetirementContribution).toFixed(2),
effectiveEmergencyContribution: +effectiveEmergencyContribution.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, // If you want to show the new running values,
// you can keep them as is or store them: // you can keep them as is or store them:
emergencySavings: (typeof currentEmergencySavings === 'number') emergencySavings: (typeof currentEmergencySavings === 'number')
? +currentEmergencySavings.toFixed(2) ? +fnum(currentEmergencySavings).toFixed(2)
: currentEmergencySavings, : currentEmergencySavings,
retirementSavings: (typeof currentRetirementSavings === 'number') retirementSavings: (typeof currentRetirementSavings === 'number')
? +currentRetirementSavings.toFixed(2) ? +fnum(currentRetirementSavings).toFixed(2)
: currentRetirementSavings, : currentRetirementSavings,
loanBalance: +loanBalance.toFixed(2), loanBalance: +fnum(loanBalance).toFixed(2),
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2), loanPaymentThisMonth: +fnum(monthlyLoanPayment + extraPayment).toFixed(2),
cashBalance: +cashBalance.toFixed(2), cashBalance: +fnum(cashBalance).toFixed(2),
totalSavings: +(currentEmergencySavings + currentRetirementSavings + cashBalance).toFixed(2), totalSavings: +fnum(currentEmergencySavings + currentRetirementSavings + cashBalance).toFixed(2),
fedYTDgross: +fedYTDgross.toFixed(2), fedYTDgross: +fnum(fedYTDgross).toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2), fedYTDtax: +fnum(fedYTDtax).toFixed(2),
stateYTDgross: +stateYTDgross.toFixed(2), stateYTDgross: +fnum(stateYTDgross).toFixed(2),
stateYTDtax: +stateYTDtax.toFixed(2) stateYTDtax: +fnum(stateYTDtax).toFixed(2)
}); });
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);