dev1/src/utils/FinancialProjectionService.js
Josh fb2e0522d3
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Password resets - Signin and UserProfile
2025-08-12 16:57:16 +00:00

669 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import moment from 'moment';
/** -------------------------------------------------
* Utility: coerce ANY input to a safe number
* - Empty string, null, undefined, NaN → 0
* - Valid numeric string → Number(value)
* - Already-a-number → Number(value)
* Keeps everything finite so .toFixed() is safe
* -------------------------------------------------*/
const n = (val, fallback = 0) => {
const num = Number(val);
return Number.isFinite(num) ? num : fallback;
};
const num = (v) =>
v === null || v === undefined || v === '' || Number.isNaN(+v) ? 0 : +v;
/***************************************************
* HELPER: Approx State Tax Rates
***************************************************/
const APPROX_STATE_TAX_RATES = {
AL: 0.05, AK: 0.00, AZ: 0.025, AR: 0.05, CA: 0.07, CO: 0.045, CT: 0.055, DE: 0.05,
FL: 0.00, GA: 0.05, HI: 0.06, ID: 0.058, IL: 0.05, IN: 0.035, IA: 0.05, KS: 0.05,
KY: 0.05, LA: 0.04, ME: 0.055, MD: 0.05, MA: 0.05, MI: 0.0425, MN: 0.06, MS: 0.04,
MO: 0.05, MT: 0.05, NE: 0.05, NV: 0.00, NH: 0.00, NJ: 0.057, NM: 0.045, NY: 0.06,
NC: 0.0475, ND: 0.02, OH: 0.04, OK: 0.045, OR: 0.07, PA: 0.03, RI: 0.045, SC: 0.04,
SD: 0.00, TN: 0.00, TX: 0.00, UT: 0.045, VT: 0.055, VA: 0.05, WA: 0.00, WV: 0.05,
WI: 0.05, WY: 0.00, DC: 0.05
};
/***************************************************
* HELPER: Federal Tax Brackets
***************************************************/
const STANDARD_DEDUCTION_SINGLE = 13850;
function calculateAnnualFederalTaxSingle(annualTaxable) {
const brackets = [
{ limit: 11000, rate: 0.10 },
{ limit: 44725, rate: 0.12 },
{ limit: 95375, rate: 0.22 },
{ limit: 182100, rate: 0.24 },
{ limit: 231250, rate: 0.32 },
{ limit: 578125, rate: 0.35 },
{ limit: Infinity, rate: 0.37 }
];
let tax = 0;
let lastLimit = 0;
for (let i = 0; i < brackets.length; i++) {
const { limit, rate } = brackets[i];
if (annualTaxable <= limit) {
tax += (annualTaxable - lastLimit) * rate;
break;
} else {
tax += (limit - lastLimit) * rate;
lastLimit = limit;
}
}
return tax;
}
/***************************************************
* HELPER: Monthly Federal Tax (no YTD)
***************************************************/
function calculateMonthlyFedTaxNoYTD(monthlyGross) {
const annualGross = monthlyGross * 12;
let annualTaxable = annualGross - STANDARD_DEDUCTION_SINGLE;
if (annualTaxable < 0) annualTaxable = 0;
const annualTax = calculateAnnualFederalTaxSingle(annualTaxable);
return annualTax / 12;
}
/***************************************************
* HELPER: Monthly State Tax (no YTD)
***************************************************/
function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') {
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
return monthlyGross * rate;
}
/***************************************************
* HELPER: Loan Payment (if not deferring)
***************************************************/
function calculateLoanPayment(principal, annualRate, years) {
if (principal <= 0) return 0;
const monthlyRate = annualRate / 100 / 12;
const numPayments = years * 12;
if (monthlyRate === 0) {
return principal / numPayments;
}
return (
(principal * monthlyRate) /
(1 - Math.pow(1 + monthlyRate, -numPayments))
);
}
/***************************************************
* MAIN SIMULATION FUNCTION
***************************************************/
export function simulateFinancialProjection(userProfile) {
// 1) Show userProfile at the start
/***************************************************
* 1) DESTRUCTURE USER PROFILE
***************************************************/
/* 1⃣ Destructure exactly the same keys you already use … */
/* but add an underscore so we can soon normalise each one. */
const {
// Basic incomes ----------------------------------------------
currentSalary: _currentSalary = 0,
monthlyExpenses: _monthlyExpenses = 0,
monthlyDebtPayments: _monthlyDebtPayments = 0,
additionalIncome: _additionalIncome = 0,
extraPayment: _extraPayment = 0,
desired_retirement_income_monthly : _retSpend = 0,
retirement_start_date : _retStart,
// Student-loan config ----------------------------------------
studentLoanAmount: _studentLoanAmount = 0,
existing_college_debt: _existingCollegeDebt = 0,
interestRate: _interestRate = 5,
loanTerm: _loanTerm = 10,
loanDeferralUntilGraduation = false,
// College ----------------------------------------------------
inCollege = false,
collegeEnrollmentStatus = 'none',
programType,
hoursCompleted: _hoursCompleted = 0,
creditHoursPerYear: _creditHoursPerYear,
calculatedTuition: _calculatedTuition,
enrollmentDate,
gradDate,
startDate,
academicCalendar = 'monthly',
annualFinancialAid: _annualFinancialAid = 0,
// Post-college salary ----------------------------------------
expectedSalary: _expectedSalary = 0,
// Savings & contributions ------------------------------------
emergencySavings: _emergencySavings = 0,
retirementSavings: _retirementSavings = 0,
monthlyRetirementContribution:_monthlyRetirementContribution = 0,
monthlyEmergencyContribution:_monthlyEmergencyContribution = 0,
// Surplus distribution ---------------------------------------
surplusEmergencyAllocation: _surplusEmergencyAllocation = 50,
surplusRetirementAllocation: _surplusRetirementAllocation = 50,
// Other -------------------------------------------------------
programLength,
stateCode = 'GA',
milestoneImpacts = [],
simulationYears = 20,
interestStrategy = 'NONE',
flatAnnualRate: _flatAnnualRate = 0.06,
monthlyReturnSamples = [],
randomRangeMin: _randomRangeMin = -0.02,
randomRangeMax: _randomRangeMax = 0.02
} = userProfile;
/* 2⃣ Immediately convert every money/percentage/count field to a real Number */
const currentSalary = num(_currentSalary);
const monthlyExpenses = num(_monthlyExpenses);
const monthlyDebtPayments = num(_monthlyDebtPayments);
const additionalIncome = num(_additionalIncome);
const extraPayment = num(_extraPayment);
const studentLoanAmount = num(_studentLoanAmount);
const existingCollegeDebt = num(_existingCollegeDebt);
const interestRate = num(_interestRate);
const loanTerm = num(_loanTerm);
const isProgrammeActive =
['enrolled', 'graduated'].includes(collegeEnrollmentStatus);
const hoursCompleted = num(_hoursCompleted);
const creditHoursPerYear = num(_creditHoursPerYear);
const calculatedTuition = num(_calculatedTuition);
const annualFinancialAid = num(_annualFinancialAid);
const expectedSalary = num(_expectedSalary);
const emergencySavings = num(_emergencySavings);
const retirementSavings = num(_retirementSavings);
const monthlyRetirementContribution= num(_monthlyRetirementContribution);
const monthlyEmergencyContribution = num(_monthlyEmergencyContribution);
const surplusEmergencyAllocation = num(_surplusEmergencyAllocation);
const surplusRetirementAllocation = num(_surplusRetirementAllocation);
const flatAnnualRate = num(_flatAnnualRate);
const randomRangeMin = num(_randomRangeMin);
const randomRangeMax = num(_randomRangeMax);
/* -------------------------------------------------
* Use the “…Safe” variables below instead of the
* raw ones whenever youll call .toFixed() or do
* arithmetic. All names are preserved; only the
* “Safe” suffix distinguishes the sanitized values.
* -------------------------------------------------*/
/***************************************************
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
***************************************************/
const scenarioStartClamped = moment().startOf('month'); // always “today”
let reachedRetirement = false; // flip to true once we hit the month
let firstRetirementBalance = null; // snapshot of balance in that first month
const retirementSpendMonthly = num(_retSpend);
const retirementStartISO =
_retStart ? moment(_retStart).startOf('month')
: scenarioStartClamped.clone().add(simulationYears,'years'); // fallback
/***************************************************
* HELPER: Retirement Interest Rate
***************************************************/
function getMonthlyInterestRate() {
// e.g. a switch or if-else:
if (interestStrategy === 'NONE') {
return 0;
} else if (interestStrategy === 'FLAT') {
// e.g. 6% annual => 0.5% per month
return flatAnnualRate / 12;
} else if (interestStrategy === 'MONTE_CARLO') {
// if using a random range or historical sample
if (monthlyReturnSamples.length > 0) {
const idx = Math.floor(Math.random() * monthlyReturnSamples.length);
return monthlyReturnSamples[idx]; // already monthly
} else {
const range = randomRangeMax - randomRangeMin;
return randomRangeMin + (Math.random() * range);
}
}
return 0; // fallback
}
/***************************************************
* HELPER: Retirement draw
***************************************************/
function simulateDrawdown(opts){
const {
startingBalance, monthlySpend,
interestStrategy, flatAnnualRate,
randomRangeMin, randomRangeMax,
monthlyReturnSamples
} = opts;
let bal = startingBalance, m = 0;
while (bal > 0 && m < 1200){
const r = getMonthlyInterestRate();
bal = bal*(1+r) - monthlySpend;
m++;
}
return m;
}
/***************************************************
* 3) DETERMINE PROGRAM LENGTH (credit hours)
***************************************************/
let requiredCreditHours = 120;
switch (programType) {
case "Associate's Degree":
requiredCreditHours = 60;
break;
case "Master's Degree":
requiredCreditHours = 30;
break;
case "Doctoral Degree":
requiredCreditHours = 60;
break;
// else Bachelor's = 120
}
const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted);
const dynamicProgramLength = Math.ceil(
remainingCreditHours / (creditHoursPerYear || 30)
);
const finalProgramLength = programLength || dynamicProgramLength;
const enrollmentStart = enrollmentDate
? moment(enrollmentDate).startOf('month')
: (startDate ? moment(startDate).startOf('month') : scenarioStartClamped.clone());
const creditsPerYear = creditHoursPerYear || 30;
const creditsRemaining = Math.max(0, requiredCreditHours - hoursCompleted);
const monthsRemaining = Math.ceil((creditsRemaining / Math.max(1, creditsPerYear)) * 12);
const gradDateEffective = gradDate
? moment(gradDate).startOf('month')
: enrollmentStart.clone().add(monthsRemaining, 'months');
/***************************************************
* 4) TUITION CALC
***************************************************/
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;
}
const totalAcademicMonths = finalProgramLength * 12;
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
/***************************************************
* 5) LOAN PAYMENT (if not deferring)
***************************************************/
const initialLoanPrincipal = studentLoanAmount + existingCollegeDebt;
let monthlyLoanPayment = loanDeferralUntilGraduation
? 0
: calculateLoanPayment(initialLoanPrincipal, interestRate, loanTerm);
/***************************************************
* 6) SETUP FOR THE SIMULATION LOOP
***************************************************/
const maxMonths = simulationYears * 12;
let loanBalance = initialLoanPrincipal;
let loanPaidOffMonth = null;
let currentEmergencySavings = emergencySavings;
let currentRetirementSavings = retirementSavings;
let projectionData = [];
// Track YTD gross & tax
let fedYTDgross = 0;
let fedYTDtax = 0;
let stateYTDgross = 0;
let stateYTDtax = 0;
// We'll keep track that we started in deferral if inCollege & deferral is true
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
// If there's a gradDate, let's see if we pass it:
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
const enrollmentDateObj = enrollmentDate
? moment(enrollmentDate).startOf('month')
: scenarioStartClamped.clone(); // fallback
/***************************************************
* 7) THE MONTHLY LOOP
***************************************************/
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
if (!reachedRetirement && currentSimDate.isSameOrAfter(retirementStartISO)) {
reachedRetirement = true;
firstRetirementBalance = currentRetirementSavings; // capture once
}
const isRetiredThisMonth =
retirementSpendMonthly > 0 &&
currentSimDate.isSameOrAfter(retirementStartISO)
// figure out if we are in the college window
const stillInCollege =
inCollege &&
currentSimDate.isSameOrAfter(enrollmentStart) &&
currentSimDate.isBefore(gradDateEffective);
const hasGraduated = currentSimDate.isSameOrAfter(gradDateEffective.clone().add(1, 'month'));
/************************************************
* 7.1 TUITION lumps
************************************************/
let tuitionCostThisMonth = 0;
if (stillInCollege && lumpsPerYear > 0) {
const monthsSinceEnroll = Math.max(0, currentSimDate.diff(enrollmentStart, 'months'));
const academicYearIndex = Math.floor(monthsSinceEnroll / 12);
const monthInAcadYear = monthsSinceEnroll % 12;
if (lumpsSchedule.includes(monthInAcadYear) && academicYearIndex < finalProgramLength) {
tuitionCostThisMonth = lumpAmount;
}
}
// If deferring tuition => add to loan, no direct expense
if (stillInCollege && loanDeferralUntilGraduation && tuitionCostThisMonth > 0) {
loanBalance += tuitionCostThisMonth;
tuitionCostThisMonth = 0;
}
/************************************************
* 7.2 BASE MONTHLY INCOME (salary -or- retirement draw)
************************************************/
let baseMonthlyIncome = 0;
if (reachedRetirement) {
const withdrawal = Math.min(retirementSpendMonthly, currentRetirementSavings);
currentRetirementSavings -= withdrawal;
baseMonthlyIncome += withdrawal;
} else if (!stillInCollege) {
const monthlyFromJob = (hasGraduated && expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
baseMonthlyIncome = monthlyFromJob + (additionalIncome / 12);
} else {
baseMonthlyIncome = (currentSalary / 12) + (additionalIncome / 12);
}
/************************************************
* 7.3 MILESTONE IMPACTS safe number handling
************************************************/
let extraImpactsThisMonth = 0; // affects expenses
let salaryAdjustThisMonth = 0; // affects gross income
milestoneImpacts.forEach((rawImpact) => {
/* ---------- 1. Normalise ---------- */
const amount = Number(rawImpact.amount) || 0;
const type = (rawImpact.impact_type || 'MONTHLY').toUpperCase(); // SALARY / SALARY_ANNUAL / MONTHLY / ONE_TIME
const direction = (rawImpact.direction || 'subtract').toLowerCase(); // add / subtract
/* ---------- 2. Work out timing ---------- */
const startDate = moment(rawImpact.start_date).startOf('month');
const endDate = rawImpact.end_date ? moment(rawImpact.end_date).startOf('month') : null;
const startOffset = Math.max(0, startDate.diff(scenarioStartClamped, 'months'));
const endOffset = endDate ? Math.max(0, endDate.diff(scenarioStartClamped, 'months')) : Infinity;
const isActiveThisMonth =
(type === 'ONE_TIME' && monthIndex === startOffset) ||
(type !== 'ONE_TIME' && monthIndex >= startOffset && monthIndex <= endOffset);
if (!isActiveThisMonth) return; // skip to next impact
/* ---------- 3. Apply the impact ---------- */
if (type.startsWith('SALARY')) {
// ─── salary changes affect GROSS income ───
const monthlyDelta = type.endsWith('ANNUAL') ? amount / 12 : amount;
const salarySign = direction === 'add' ? 1 : -1; // unchanged
salaryAdjustThisMonth += salarySign * monthlyDelta;
} else {
// ─── everything else is an expense or windfall ───
// “Add” ⇒ money coming *in* ⇒ LOWER expenses
// “Subtract” ⇒ money going *out* ⇒ HIGHER expenses
const expenseSign = direction === 'add' ? -1 : 1;
extraImpactsThisMonth += expenseSign * amount;
}
});
/* ---------- 4. Reflect deltas in this months calc ---------- */
baseMonthlyIncome += salaryAdjustThisMonth; // adjust gross BEFORE tax
// `extraImpactsThisMonth` is already added to expenses later in the loop
/************************************************
* 7.4 CALCULATE TAXES
************************************************/
const monthlyFederalTax = calculateMonthlyFedTaxNoYTD(baseMonthlyIncome);
const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode);
const combinedTax = monthlyFederalTax + monthlyStateTax;
// net after tax
const netMonthlyIncome = baseMonthlyIncome - combinedTax;
// increment YTD for reference
fedYTDgross += baseMonthlyIncome;
fedYTDtax += monthlyFederalTax;
stateYTDgross += baseMonthlyIncome;
stateYTDtax += monthlyStateTax;
/************************************************
* 7.5 WITHDRAW FROM RETIREMENT
************************************************/
// From here on well use `livingCost` instead of `monthlyExpenses`
const livingCost = reachedRetirement ? retirementSpendMonthly : monthlyExpenses;
// Leaving deferral → begin repayment using current balance
if (loanDeferralUntilGraduation && wasInDeferral && !stillInCollege) {
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm);
}
/************************************************
* 7.6 LOAN + EXPENSES
************************************************/
// sum up all monthly expenses
let totalMonthlyExpenses =
livingCost + monthlyDebtPayments + tuitionCostThisMonth + extraImpactsThisMonth;
if (stillInCollege && loanDeferralUntilGraduation) {
// accumulate interest only
const interestForMonth = loanBalance * (interestRate / 100 / 12);
loanBalance += interestForMonth;
} else {
// pay principal
if (loanBalance > 0) {
const interestForMonth = loanBalance * (interestRate / 100 / 12);
const totalThisMonth = monthlyLoanPayment + extraPayment;
const principalForMonth = Math.min(loanBalance, totalThisMonth - interestForMonth);
loanBalance = Math.max(loanBalance - principalForMonth, 0);
totalMonthlyExpenses += totalThisMonth;
}
}
if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = currentSimDate.format('YYYY-MM');
}
let leftover = netMonthlyIncome - totalMonthlyExpenses;
const canSaveThisMonth = leftover > 0;
// baseline contributions
const monthlyRetContrib =
canSaveThisMonth && !isRetiredThisMonth ? monthlyRetirementContribution : 0;
const monthlyEmergContrib =
canSaveThisMonth && !isRetiredThisMonth ? monthlyEmergencyContribution : 0;
const baselineContributions = monthlyRetContrib + monthlyEmergContrib;
let effectiveRetirementContribution = 0;
let effectiveEmergencyContribution = 0;
if (leftover >= baselineContributions) {
effectiveRetirementContribution = monthlyRetContrib;
effectiveEmergencyContribution = monthlyEmergContrib;
currentRetirementSavings += effectiveRetirementContribution;
currentEmergencySavings += effectiveEmergencyContribution;
leftover -= baselineContributions;
}
const actualExpensesPaid = totalMonthlyExpenses + effectiveRetirementContribution + effectiveEmergencyContribution;
let shortfall = actualExpensesPaid - netMonthlyIncome;
// cover shortfall with emergency
if (shortfall > 0) {
const canCover = Math.min(shortfall, currentEmergencySavings);
currentEmergencySavings -= canCover;
shortfall -= canCover;
}
// Surplus => leftover
if (canSaveThisMonth && leftover > 0) {
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
const emergPortion = leftover * (surplusEmergencyAllocation / totalPct);
const retPortion = leftover * (surplusRetirementAllocation / totalPct);
currentEmergencySavings += emergPortion;
currentRetirementSavings += retPortion;
}
const monthlyReturnRate = getMonthlyInterestRate();
if (monthlyReturnRate !== 0) {
currentRetirementSavings *= (1 + monthlyReturnRate);
}
const netSavings = netMonthlyIncome - actualExpensesPaid;
// (UPDATED) add inCollege, stillInCollege, loanDeferralUntilGraduation to the result
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),
effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2),
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
netSavings: +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)
: currentEmergencySavings,
retirementSavings: (typeof currentRetirementSavings === 'number')
? +currentRetirementSavings.toFixed(2)
: currentRetirementSavings,
loanBalance: +loanBalance.toFixed(2),
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
totalSavings: (currentEmergencySavings + currentRetirementSavings).toFixed(2),
fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2),
stateYTDgross: +stateYTDgross.toFixed(2),
stateYTDtax: +stateYTDtax.toFixed(2)
});
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);
}
if (!firstRetirementBalance && reachedRetirement) {
firstRetirementBalance = currentRetirementSavings;
}
/* ---- 8) RETIREMENT DRAWDOWN ---------------------------------- */
const monthlySpend = retirementSpendMonthly || 0;
const monthsCovered = monthlySpend > 0
? simulateDrawdown({
startingBalance : firstRetirementBalance ?? currentRetirementSavings,
monthlySpend,
interestStrategy,
flatAnnualRate,
randomRangeMin,
randomRangeMax,
monthlyReturnSamples
})
: 0;
const yearsCovered = Math.round(monthsCovered / 12);
/* ---- 9) RETURN ------------------------------------------------ */
/* 9.1  »  Readiness Score (0-100, whole-number) */
const safeWithdrawalRate = flatAnnualRate; // 6 % for now (plug 0.04 if you prefer)
const projectedAnnualFlow = (firstRetirementBalance ?? currentRetirementSavings)
* safeWithdrawalRate;
const desiredAnnualIncome = retirementSpendMonthly // user override
? retirementSpendMonthly * 12
: monthlyExpenses * 12; // fallback to current spend
const readinessScore = desiredAnnualIncome > 0
? Math.min(
100,
Math.round((projectedAnnualFlow / desiredAnnualIncome) * 100)
)
: 0;
return {
projectionData,
loanPaidOffMonth,
readinessScore,
retirementAtMilestone : firstRetirementBalance ?? 0,
yearsCovered, // <-- add these two
monthsCovered, // (or just yearsCovered if thats all you need)
finalEmergencySavings: +currentEmergencySavings.toFixed(2),
finalRetirementSavings: +currentRetirementSavings.toFixed(2),
finalLoanBalance: +loanBalance.toFixed(2),
fedYTDgross: +fedYTDgross.toFixed(2),
fedYTDtax: +fedYTDtax.toFixed(2),
stateYTDgross: +stateYTDgross.toFixed(2),
stateYTDtax: +stateYTDtax.toFixed(2)
};
}