diff --git a/src/components/PremiumOnboarding/CollegeOnboarding.js b/src/components/PremiumOnboarding/CollegeOnboarding.js index 109d521..6a569c0 100644 --- a/src/components/PremiumOnboarding/CollegeOnboarding.js +++ b/src/components/PremiumOnboarding/CollegeOnboarding.js @@ -27,7 +27,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) loan_term = 10, extra_payment = '', expected_salary = '', - is_in_state = true, + is_in_state = false, is_in_district = false, loan_deferral_until_graduation = false, credit_hours_per_year = '', @@ -335,6 +335,46 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId }) {(college_enrollment_status === 'currently_enrolled' || college_enrollment_status === 'prospective_student') ? ( <> + + + + + + + + - - - - - - - + > ) : (
Not currently enrolled or prospective student. Skipping college onboarding.
diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index 0295cc0..8a4c22f 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -1,12 +1,70 @@ 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. +/** + * Single-filer federal tax calculation (2023). + * Includes standard deduction ($13,850). + * If you need to update the brackets/deduction, edit here. + */ +function calculateAnnualFederalTaxSingle(annualIncome) { + // 1. Subtract standard deduction + const STANDARD_DEDUCTION_SINGLE = 13850; + const taxableIncome = Math.max(0, annualIncome - STANDARD_DEDUCTION_SINGLE); + // 2. Define bracket thresholds & rates for single filers (2023) + // The 'limit' is the upper bound for that bracket segment. + // Use Infinity for the top bracket. + 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; + + // 3. Accumulate tax across brackets + for (let i = 0; i < brackets.length; i++) { + const { limit, rate } = brackets[i]; + if (taxableIncome <= limit) { + // only tax the portion within this bracket + tax += (taxableIncome - lastLimit) * rate; + break; + } else { + // tax the entire bracket range, then continue + tax += (limit - lastLimit) * rate; + lastLimit = limit; + } + } + + return tax; +} + +/** + * 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)) + ); +} + +/** + * Main projection function with bracket-based tax logic for single filers. + */ export function simulateFinancialProjection(userProfile) { const { // Income & expenses @@ -18,8 +76,8 @@ export function simulateFinancialProjection(userProfile) { // Loan info studentLoanAmount = 0, - interestRate = 5, // % - loanTerm = 10, // years + interestRate = 5, // % + loanTerm = 10, // years loanDeferralUntilGraduation = false, // College & tuition @@ -27,10 +85,10 @@ export function simulateFinancialProjection(userProfile) { programType, hoursCompleted = 0, creditHoursPerYear, - calculatedTuition, // e.g. annual tuition - gradDate, // known graduation date, or null - startDate, // when sim starts - academicCalendar = 'monthly', // new + calculatedTuition, + gradDate, + startDate, + academicCalendar = 'monthly', annualFinancialAid = 0, // Salary after graduation @@ -45,15 +103,17 @@ export function simulateFinancialProjection(userProfile) { monthlyEmergencyContribution = 0, // Surplus allocation - surplusEmergencyAllocation = 50, + surplusEmergencyAllocation = 50, surplusRetirementAllocation = 50, // Potential override programLength } = userProfile; - // 1. Calculate standard monthly loan payment - const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); + // 1. Calculate standard monthly loan payment (if not deferring) + let monthlyLoanPayment = loanDeferralUntilGraduation + ? 0 + : calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); // 2. Determine how many credit hours remain let requiredCreditHours = 120; @@ -83,14 +143,11 @@ export function simulateFinancialProjection(userProfile) { // 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 + let lumpsSchedule = []; switch (academicCalendar) { case 'semester': lumpsPerYear = 2; - lumpsSchedule = [0, 6]; // months 0 & 6 from start of each academic year + lumpsSchedule = [0, 6]; break; case 'quarter': lumpsPerYear = 4; @@ -106,102 +163,132 @@ export function simulateFinancialProjection(userProfile) { 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 + // 5. Simulation loop up to 20 years const maxMonths = 240; let date = startDate ? new Date(startDate) : new Date(); - let loanBalance = studentLoanAmount; + + let loanBalance = Math.max(studentLoanAmount, 0); let loanPaidOffMonth = null; + let currentEmergencySavings = emergencySavings; let currentRetirementSavings = retirementSavings; - let projectionData = []; - // Convert gradDate to actual if present + let projectionData = []; + let wasInDeferral = inCollege && loanDeferralUntilGraduation; + + // If gradDate is provided, parse it to Date const graduationDate = gradDate ? new Date(gradDate) : null; + // Keep a map of year => { ytdGross, ytdTaxSoFar } for bracket-based taxes + const taxStateByYear = {}; + for (let month = 0; month < maxMonths; month++) { date.setMonth(date.getMonth() + 1); - + const currentYear = date.getFullYear(); - // If loan is fully paid, record if not done already + // If loan is fully paid, record if not already done if (loanBalance <= 0 && !loanPaidOffMonth) { - loanPaidOffMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + loanPaidOffMonth = `${currentYear}-${String(date.getMonth() + 1).padStart(2, '0')}`; } - // Are we still in college? We either trust gradDate or approximate finalProgramLength + // Determine if user is still in college (trusted gradDate or approximate) 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); } - console.log(`MONTH ${month} start: inCollege=${stillInCollege}, loanBal=${loanBalance}`); } - // 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 + // 6. Check if we owe tuition lumps this month 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; - console.log(" lumps logic check: academicYearIndex=", academicYearIndex, "monthInYear=", monthInYear); - // 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 + // 7. Detect if we are now exiting college this month + const nowExitingCollege = (wasInDeferral && !stillInCollege); + + // 8. If in deferral, lumps get added to the loan principal if (stillInCollege && loanDeferralUntilGraduation) { - console.log(" deferral is on, lumps => loan?"); - // Instead of user paying out of pocket, add to loan if (tuitionCostThisMonth > 0) { - console.log(" tuitionCostThisMonth=", tuitionCostThisMonth); loanBalance += tuitionCostThisMonth; - tuitionCostThisMonth = 0; // paid by the loan + tuitionCostThisMonth = 0; } } - // 8. monthly income - let monthlyIncome = 0; + // 9. Gross monthly income depends on college status + let grossMonthlyIncome = 0; if (!inCollege || !stillInCollege) { - // user has graduated or never in college - monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12; + // Graduated or never was in college => use expectedSalary if given + grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12; } else { - // in college => currentSalary + partTimeIncome - monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); + // Still in college => currentSalary + part-time + grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); } - // 9. mandatory expenses (excluding student loan if deferring) + /** + * 10. Calculate monthly TAX via bracket-based approach: + * We track year-to-date (YTD) for the current calendar year. + */ + if (!taxStateByYear[currentYear]) { + taxStateByYear[currentYear] = { ytdGross: 0, ytdTaxSoFar: 0 }; + } + + // Add this month’s gross to YTD + taxStateByYear[currentYear].ytdGross += grossMonthlyIncome; + + // Calculate total tax for YTD + const annualTaxSoFar = calculateAnnualFederalTaxSingle( + taxStateByYear[currentYear].ytdGross + ); + + // This month’s tax = (new YTD tax) - (old YTD tax) + const monthlyTax = annualTaxSoFar - taxStateByYear[currentYear].ytdTaxSoFar; + + // Update YTD tax + taxStateByYear[currentYear].ytdTaxSoFar = annualTaxSoFar; + + // Net monthly income after tax + const netMonthlyIncome = grossMonthlyIncome - monthlyTax; + + // 11. Monthly expenses (excluding student loan if deferring) let thisMonthLoanPayment = 0; let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth; + // Re-amortize if we're just now exiting college + if (nowExitingCollege) { + monthlyLoanPayment = calculateLoanPayment( + loanBalance, + interestRate, + 10 // fresh 10-year term post-college + ); + } + + // 12. If still deferring, just accrue interest if (stillInCollege && loanDeferralUntilGraduation) { - // Accrue interest only const interestForMonth = loanBalance * (interestRate / 100 / 12); loanBalance += interestForMonth; } else { - // Normal loan repayment if loan > 0 + // Normal repayment if loan > 0 if (loanBalance > 0) { const interestForMonth = loanBalance * (interestRate / 100 / 12); const principalForMonth = Math.min( @@ -210,16 +297,15 @@ export function simulateFinancialProjection(userProfile) { ); 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 - } + // 13. leftover after mandatory expenses + let leftover = netMonthlyIncome - totalMonthlyExpenses; + if (leftover < 0) leftover = 0; // Baseline monthly contributions const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution; @@ -231,91 +317,69 @@ export function simulateFinancialProjection(userProfile) { 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. + // Not enough leftover => zero out contributions 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; + // Check for shortfall vs. mandatory expenses const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; - let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay - console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall); + let shortfall = actualExpensesPaid - netMonthlyIncome; if (shortfall > 0) { - console.log(" Breaking out - bankrupt scenario"); + // Attempt to cover from emergency savings const canCover = Math.min(shortfall, currentEmergencySavings); currentEmergencySavings -= canCover; shortfall -= canCover; if (shortfall > 0) { + // Even after emergency, we can't cover => break (bankrupt scenario) 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; + // 14. Surplus allocation if leftover > 0 + const newLeftover = leftover; if (newLeftover > 0) { - // Allocate by percent + // Allocate by percentage 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; + // 15. netSavings for the month (could be leftover minus contributions, etc.) + // But we already subtracted everything from netMonthlyIncome except what ended up surplus + const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions; + const netSavings = netMonthlyIncome - finalExpensesPaid; + // Record in the projection data projectionData.push({ month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, - monthlyIncome, - totalExpenses: finalExpensesPaid, - effectiveRetirementContribution, - effectiveEmergencyContribution, - netSavings, - emergencySavings: currentEmergencySavings, - retirementSavings: currentRetirementSavings, + grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100, + monthlyTax: Math.round(monthlyTax * 100) / 100, + netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100, + totalExpenses: Math.round(finalExpensesPaid * 100) / 100, + effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100, + effectiveEmergencyContribution: Math.round(effectiveEmergencyContribution * 100) / 100, + netSavings: Math.round(netSavings * 100) / 100, + emergencySavings: Math.round(currentEmergencySavings * 100) / 100, + retirementSavings: Math.round(currentRetirementSavings * 100) / 100, loanBalance: Math.round(loanBalance * 100) / 100, - loanPaymentThisMonth: thisMonthLoanPayment + loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100 }); + + // Update deferral flag for next iteration + wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); } - // Return final + // Return the final output return { projectionData, loanPaidOffMonth, - finalEmergencySavings: currentEmergencySavings, - finalRetirementSavings: currentRetirementSavings, + finalEmergencySavings: Math.round(currentEmergencySavings * 100) / 100, + finalRetirementSavings: Math.round(currentRetirementSavings * 100) / 100, 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)) - ); -} diff --git a/user_profile.db b/user_profile.db index a53b042..8b437d7 100644 Binary files a/user_profile.db and b/user_profile.db differ