import moment from 'moment'; // Example fields in userProfile that matter here: // - academicCalendar: 'semester' | 'quarter' | 'trimester' | 'monthly' // - annualFinancialAid: amount of scholarships/grants per year // - inCollege, loanDeferralUntilGraduation, graduationDate, etc. // // Additional logic now for lumps instead of monthly tuition payments. export function simulateFinancialProjection(userProfile) { const { // Income & expenses currentSalary = 0, monthlyExpenses = 0, monthlyDebtPayments = 0, partTimeIncome = 0, extraPayment = 0, // Loan info studentLoanAmount = 0, interestRate = 5, // % loanTerm = 10, // years loanDeferralUntilGraduation = false, // College & tuition inCollege = false, programType, hoursCompleted = 0, creditHoursPerYear, calculatedTuition, // e.g. annual tuition gradDate, // known graduation date, or null startDate, // when sim starts academicCalendar = 'monthly', // new annualFinancialAid = 0, // Salary after graduation expectedSalary = 0, // Savings emergencySavings = 0, retirementSavings = 0, // Monthly contributions monthlyRetirementContribution = 0, monthlyEmergencyContribution = 0, // Surplus allocation surplusEmergencyAllocation = 50, surplusRetirementAllocation = 50, // Potential override programLength } = userProfile; // 1. Calculate standard monthly loan payment const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); // 2. Determine how many credit hours remain let requiredCreditHours = 120; switch (programType) { case "Associate's Degree": requiredCreditHours = 60; break; case "Bachelor's Degree": requiredCreditHours = 120; break; case "Master's Degree": requiredCreditHours = 30; break; case "Doctoral Degree": requiredCreditHours = 60; break; default: requiredCreditHours = 120; } const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted); const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear); const finalProgramLength = programLength || dynamicProgramLength; // 3. Net annual tuition after financial aid const netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid); const totalTuitionCost = netAnnualTuition * finalProgramLength; // 4. Setup lumps per year based on academicCalendar let lumpsPerYear = 12; // monthly fallback let lumpsSchedule = []; // which months from start of academic year // We'll store an array of month offsets in a single year (0-based) // for semester, quarter, trimester switch (academicCalendar) { case 'semester': lumpsPerYear = 2; lumpsSchedule = [0, 6]; // months 0 & 6 from start of each academic year 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()]; // 0..11 break; } // Each academic year is 12 months, for finalProgramLength years => totalAcademicMonths const totalAcademicMonths = finalProgramLength * 12; // Each lump sum = totalTuitionCost / (lumpsPerYear * finalProgramLength) const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength); // 5. We'll loop for up to 20 years const maxMonths = 240; let date = startDate ? new Date(startDate) : new Date(); let loanBalance = studentLoanAmount; let loanPaidOffMonth = null; let currentEmergencySavings = emergencySavings; let currentRetirementSavings = retirementSavings; let projectionData = []; // Convert gradDate to actual if present const graduationDate = gradDate ? new Date(gradDate) : null; for (let month = 0; month < maxMonths; month++) { date.setMonth(date.getMonth() + 1); // If loan is fully paid, record if not done already if (loanBalance <= 0 && !loanPaidOffMonth) { loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; } // Are we still in college? We either trust gradDate or approximate finalProgramLength let stillInCollege = false; if (inCollege) { if (graduationDate) { stillInCollege = date < graduationDate; } else { // approximate by how many months since start const simStart = startDate ? new Date(startDate) : new Date(); const elapsedMonths = (date.getFullYear() - simStart.getFullYear()) * 12 + (date.getMonth() - simStart.getMonth()); stillInCollege = (elapsedMonths < totalAcademicMonths); } } // 6. If we pay lumps: check if this is a "lump" month within the user's academic year // We'll find how many academic years have passed since they started let tuitionCostThisMonth = 0; if (stillInCollege && lumpsPerYear > 0) { const simStart = startDate ? new Date(startDate) : new Date(); const elapsedMonths = (date.getFullYear() - simStart.getFullYear()) * 12 + (date.getMonth() - simStart.getMonth()); // Which academic year index are we in? const academicYearIndex = Math.floor(elapsedMonths / 12); // Within that year, which month are we in? (0..11) const monthInYear = elapsedMonths % 12; // If we find monthInYear in lumpsSchedule, then lumps are due if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) { tuitionCostThisMonth = lumpAmount; } } // 7. Decide if user defers or pays out of pocket // If deferring, add lumps to loan if (stillInCollege && loanDeferralUntilGraduation) { // Instead of user paying out of pocket, add to loan if (tuitionCostThisMonth > 0) { loanBalance += tuitionCostThisMonth; tuitionCostThisMonth = 0; // paid by the loan } } // 8. monthly income let monthlyIncome = 0; if (!inCollege || !stillInCollege) { // user has graduated or never in college monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12; } else { // in college => currentSalary + partTimeIncome monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); } // 9. mandatory expenses (excluding student loan if deferring) let thisMonthLoanPayment = 0; let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth; if (stillInCollege && loanDeferralUntilGraduation) { // Accrue interest only const interestForMonth = loanBalance * (interestRate / 100 / 12); loanBalance += interestForMonth; } else { // Normal loan repayment if loan > 0 if (loanBalance > 0) { const interestForMonth = loanBalance * (interestRate / 100 / 12); const principalForMonth = Math.min( loanBalance, (monthlyLoanPayment + extraPayment) - interestForMonth ); loanBalance -= principalForMonth; loanBalance = Math.max(loanBalance, 0); thisMonthLoanPayment = monthlyLoanPayment + extraPayment; totalMonthlyExpenses += thisMonthLoanPayment; } } // 10. leftover after mandatory expenses let leftover = monthlyIncome - totalMonthlyExpenses; if (leftover < 0) { leftover = 0; // can't do partial negative leftover; they simply can't afford it } // Baseline monthly contributions const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution; let effectiveRetirementContribution = 0; let effectiveEmergencyContribution = 0; if (leftover >= baselineContributions) { effectiveRetirementContribution = monthlyRetirementContribution; effectiveEmergencyContribution = monthlyEmergencyContribution; leftover -= baselineContributions; } else { // not enough leftover // for real life, we typically set them to 0 if we can't afford them // or reduce proportionally. We'll do the simpler approach: set them to 0 // as requested. effectiveRetirementContribution = 0; effectiveEmergencyContribution = 0; } // 11. Now see if leftover is negative => shortfall from mandatory expenses // Actually we zeroed leftover if it was negative. So let's check if the user // truly can't afford mandatoryExpenses const totalMandatoryPlusContrib = monthlyIncome - leftover; const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay if (shortfall > 0) { // We can reduce from emergency savings const canCover = Math.min(shortfall, currentEmergencySavings); currentEmergencySavings -= canCover; shortfall -= canCover; if (shortfall > 0) { // user is effectively bankrupt // we can break out or keep going to show negative net worth // For demonstration, let's break break; } } // 12. If leftover > 0 after baseline contributions, allocate surplus // (we do it after we've handled shortfall) const newLeftover = leftover; // leftover not used for baseline let surplusUsed = 0; if (newLeftover > 0) { // Allocate by percent const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation; const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct); const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct); currentEmergencySavings += emergencyPortion; currentRetirementSavings += retirementPortion; surplusUsed = newLeftover; } // 13. netSavings is monthlyIncome - actual expenses - all contributions // But we must recalc actual final expenses paid const finalExpensesPaid = totalMonthlyExpenses + (effectiveRetirementContribution + effectiveEmergencyContribution); const netSavings = monthlyIncome - finalExpensesPaid; projectionData.push({ month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, monthlyIncome, totalExpenses: finalExpensesPaid, effectiveRetirementContribution, effectiveEmergencyContribution, netSavings, emergencySavings: currentEmergencySavings, retirementSavings: currentRetirementSavings, loanBalance: Math.round(loanBalance * 100) / 100, loanPaymentThisMonth: thisMonthLoanPayment }); } // Return final return { projectionData, loanPaidOffMonth, finalEmergencySavings: currentEmergencySavings, finalRetirementSavings: currentRetirementSavings, finalLoanBalance: Math.round(loanBalance * 100) / 100 }; } /** * Calculate the standard monthly loan payment for principal, annualRate (%) and term (years) */ function calculateLoanPayment(principal, annualRate, years) { if (principal <= 0) return 0; const monthlyRate = annualRate / 100 / 12; const numPayments = years * 12; if (monthlyRate === 0) { // no interest return principal / numPayments; } return ( (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -numPayments)) ); }