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 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) => {
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user