import moment from 'moment'; /*************************************************** * 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 ***************************************************/ const { // Basic incomes currentSalary = 0, monthlyExpenses = 0, monthlyDebtPayments = 0, partTimeIncome = 0, extraPayment = 0, // Student loan config studentLoanAmount = 0, interestRate = 5, loanTerm = 10, loanDeferralUntilGraduation = false, // College config inCollege = false, // <<==== user-provided programType, hoursCompleted = 0, creditHoursPerYear, calculatedTuition, gradDate, startDate, academicCalendar = 'monthly', annualFinancialAid = 0, // Post-college salary expectedSalary = 0, // Savings & monthly contributions emergencySavings = 0, retirementSavings = 0, monthlyRetirementContribution = 0, monthlyEmergencyContribution = 0, // Surplus distribution surplusEmergencyAllocation = 50, surplusRetirementAllocation = 50, // Program length override programLength, // State code stateCode = 'GA', // Financial milestone impacts milestoneImpacts = [], // Simulation duration simulationYears = 20 } = userProfile; /*************************************************** * 2) CLAMP THE SCENARIO START TO MONTH-BEGIN ***************************************************/ const scenarioStartClamped = moment(startDate || new Date()).startOf('month'); /*************************************************** * 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; /*************************************************** * 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) ***************************************************/ let monthlyLoanPayment = loanDeferralUntilGraduation ? 0 : calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); // Log the initial loan info: console.log("Initial loan payment setup:", { studentLoanAmount, interestRate, loanTerm, loanDeferralUntilGraduation, monthlyLoanPayment }); /*************************************************** * 6) SETUP FOR THE SIMULATION LOOP ***************************************************/ const maxMonths = simulationYears * 12; let loanBalance = Math.max(studentLoanAmount, 0); 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; /*************************************************** * 7) THE MONTHLY LOOP ***************************************************/ for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) { const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months'); // Check if loan is fully paid if (loanBalance <= 0 && !loanPaidOffMonth) { loanPaidOffMonth = currentSimDate.format('YYYY-MM'); } // Are we still in college for this month? let stillInCollege = false; if (inCollege) { if (graduationDateObj) { stillInCollege = currentSimDate.isBefore(graduationDateObj, 'month'); } else { stillInCollege = (monthIndex < totalAcademicMonths); } } /************************************************ * 7.1 TUITION lumps ************************************************/ let tuitionCostThisMonth = 0; if (stillInCollege && lumpsPerYear > 0) { const academicYearIndex = Math.floor(monthIndex / 12); const monthInYear = monthIndex % 12; if (lumpsSchedule.includes(monthInYear) && 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 ************************************************/ let baseMonthlyIncome = 0; if (!stillInCollege) { // user out of college => expected or current baseMonthlyIncome = (expectedSalary || currentSalary) / 12; } else { // in college => might have partTimeIncome + current baseMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); } /************************************************ * 7.3 MILESTONE IMPACTS ************************************************/ let extraImpactsThisMonth = 0; milestoneImpacts.forEach((impact) => { const startDateClamped = moment(impact.start_date).startOf('month'); let startOffset = startDateClamped.diff(scenarioStartClamped, 'months'); if (startOffset < 0) startOffset = 0; let endOffset = Infinity; if (impact.end_date && impact.end_date.trim() !== '') { const endDateClamped = moment(impact.end_date).startOf('month'); endOffset = endDateClamped.diff(scenarioStartClamped, 'months'); if (endOffset < 0) endOffset = 0; } if (impact.impact_type === 'ONE_TIME') { if (monthIndex === startOffset) { if (impact.direction === 'add') { baseMonthlyIncome += impact.amount; } else { extraImpactsThisMonth += impact.amount; } } } else { // 'MONTHLY' if (monthIndex >= startOffset && monthIndex <= endOffset) { if (impact.direction === 'add') { baseMonthlyIncome += impact.amount; } else { extraImpactsThisMonth += impact.amount; } } } }); /************************************************ * 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 LOAN + EXPENSES ************************************************/ // Check if we're now exiting college & deferral ended => recalc monthlyLoanPayment const nowExitingCollege = wasInDeferral && !stillInCollege; if (nowExitingCollege) { // recalc monthlyLoanPayment with the current loanBalance monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm); } // sum up all monthly expenses let totalMonthlyExpenses = monthlyExpenses + 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; } } let leftover = netMonthlyIncome - totalMonthlyExpenses; // baseline contributions const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution; let effectiveRetirementContribution = 0; let effectiveEmergencyContribution = 0; if (leftover >= baselineContributions) { effectiveRetirementContribution = monthlyRetirementContribution; effectiveEmergencyContribution = monthlyEmergencyContribution; 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 (leftover > 0) { const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation; const emergPortion = leftover * (surplusEmergencyAllocation / totalPct); const retPortion = leftover * (surplusRetirementAllocation / totalPct); currentEmergencySavings += emergPortion; currentRetirementSavings += retPortion; } 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), fedYTDgross: +fedYTDgross.toFixed(2), fedYTDtax: +fedYTDtax.toFixed(2), stateYTDgross: +stateYTDgross.toFixed(2), stateYTDtax: +stateYTDtax.toFixed(2) }); wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); } // final loanPaidOffMonth if never set if (loanBalance <= 0 && !loanPaidOffMonth) { loanPaidOffMonth = scenarioStartClamped.clone().add(maxMonths, 'months').format('YYYY-MM'); } return { projectionData, loanPaidOffMonth, 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) }; }