financial projection fix for no college profile
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
This commit is contained in:
parent
c70aa42076
commit
bbde9f2da2
@ -1 +1 @@
|
||||
620419ccaec3e3b8f78ba81554844bd12f8a110d-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
be65400be96a473622c09b3df6073c5837dacc82-372bcf506971f56c4911b429b9f5de5bc37ed008-e9eccd451b778829eb2f2c9752c670b707e1268b
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user