From e3d804e01a372b00938b71f004185516fef028b3 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 17 Apr 2025 15:53:32 +0000 Subject: [PATCH] State taxes added to simulation --- src/utils/FinancialProjectionService.js | 162 +++++++++++++----------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/utils/FinancialProjectionService.js b/src/utils/FinancialProjectionService.js index 8a4c22f..e710c85 100644 --- a/src/utils/FinancialProjectionService.js +++ b/src/utils/FinancialProjectionService.js @@ -2,17 +2,12 @@ import moment from 'moment'; /** * Single-filer federal tax calculation (2023). - * Includes standard deduction ($13,850). - * If you need to update the brackets/deduction, edit here. + * Includes standard deduction ($13,850). */ 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 }, @@ -26,23 +21,37 @@ function calculateAnnualFederalTaxSingle(annualIncome) { 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; } +/** + * Example state tax calculation. + * Currently a simple flat rate based on `stateCode` from a small dictionary. + * You can replace with bracket-based logic if desired. + */ +function calculateAnnualStateTax(annualIncome, stateCode) { + // Example dictionary of flat rates (not real data!) + const stateTaxInfo = { + CA: 0.08, // 8% + NY: 0.06, // 6% + TX: 0.00, + FL: 0.00, + GA: 0.05 + }; + const rate = stateTaxInfo[stateCode] ?? 0.05; // default 5% if not found + return annualIncome * rate; +} + /** * Calculate the standard monthly loan payment for principal, annualRate (%) and term (years). */ @@ -53,7 +62,6 @@ function calculateLoanPayment(principal, annualRate, years) { const numPayments = years * 12; if (monthlyRate === 0) { - // no interest return principal / numPayments; } return ( @@ -63,7 +71,7 @@ function calculateLoanPayment(principal, annualRate, years) { } /** - * Main projection function with bracket-based tax logic for single filers. + * Main projection function with bracket-based FEDERAL + optional STATE tax logic. */ export function simulateFinancialProjection(userProfile) { const { @@ -85,9 +93,9 @@ export function simulateFinancialProjection(userProfile) { programType, hoursCompleted = 0, creditHoursPerYear, - calculatedTuition, - gradDate, - startDate, + calculatedTuition, + gradDate, + startDate, academicCalendar = 'monthly', annualFinancialAid = 0, @@ -107,10 +115,13 @@ export function simulateFinancialProjection(userProfile) { surplusRetirementAllocation = 50, // Potential override - programLength + programLength, + + // NEW: user’s state code (e.g. 'CA', 'NY', 'TX', etc.) + stateCode = 'TX', // default to TX (no state income tax) } = userProfile; - // 1. Calculate standard monthly loan payment (if not deferring) + // 1. Monthly loan payment if not deferring let monthlyLoanPayment = loanDeferralUntilGraduation ? 0 : calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); @@ -137,17 +148,16 @@ export function simulateFinancialProjection(userProfile) { const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear); const finalProgramLength = programLength || dynamicProgramLength; - // 3. Net annual tuition after financial aid + // 3. Net annual tuition after 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 = []; + // 4. Setup lumps per year + let lumpsPerYear, lumpsSchedule; switch (academicCalendar) { case 'semester': lumpsPerYear = 2; - lumpsSchedule = [0, 6]; + lumpsSchedule = [0, 6]; break; case 'quarter': lumpsPerYear = 4; @@ -160,41 +170,39 @@ export function simulateFinancialProjection(userProfile) { case 'monthly': default: lumpsPerYear = 12; - lumpsSchedule = [...Array(12).keys()]; // 0..11 + lumpsSchedule = [...Array(12).keys()]; break; } const totalAcademicMonths = finalProgramLength * 12; const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength); - // 5. Simulation loop up to 20 years + // 5. Simulation loop const maxMonths = 240; let date = startDate ? new Date(startDate) : new Date(); let loanBalance = Math.max(studentLoanAmount, 0); let loanPaidOffMonth = null; - let currentEmergencySavings = emergencySavings; let currentRetirementSavings = retirementSavings; 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 + // YTD tracking for each year (federal + state) + // e.g. taxStateByYear[2025] = { federalYtdGross, federalYtdTaxSoFar, stateYtdGross, stateYtdTaxSoFar } 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 already done + // Check if loan is fully paid if (loanBalance <= 0 && !loanPaidOffMonth) { loanPaidOffMonth = `${currentYear}-${String(date.getMonth() + 1).padStart(2, '0')}`; } - // Determine if user is still in college (trusted gradDate or approximate) + // Are we still in college? let stillInCollege = false; if (inCollege) { if (graduationDate) { @@ -208,7 +216,7 @@ export function simulateFinancialProjection(userProfile) { } } - // 6. Check if we owe tuition lumps this month + // 6. Tuition lumps let tuitionCostThisMonth = 0; if (stillInCollege && lumpsPerYear > 0) { const simStart = startDate ? new Date(startDate) : new Date(); @@ -224,71 +232,79 @@ export function simulateFinancialProjection(userProfile) { } } - // 7. Detect if we are now exiting college this month + // 7. Exiting college? const nowExitingCollege = (wasInDeferral && !stillInCollege); - // 8. If in deferral, lumps get added to the loan principal + // 8. Deferral lumps get added to loan if (stillInCollege && loanDeferralUntilGraduation) { if (tuitionCostThisMonth > 0) { loanBalance += tuitionCostThisMonth; - tuitionCostThisMonth = 0; + tuitionCostThisMonth = 0; } } - // 9. Gross monthly income depends on college status + // 9. Gross monthly income let grossMonthlyIncome = 0; if (!inCollege || !stillInCollege) { - // Graduated or never was in college => use expectedSalary if given grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12; } else { - // Still in college => currentSalary + part-time grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); } - /** - * 10. Calculate monthly TAX via bracket-based approach: - * We track year-to-date (YTD) for the current calendar year. - */ + // 10. Tax calculations if (!taxStateByYear[currentYear]) { - taxStateByYear[currentYear] = { ytdGross: 0, ytdTaxSoFar: 0 }; + taxStateByYear[currentYear] = { + federalYtdGross: 0, + federalYtdTaxSoFar: 0, + stateYtdGross: 0, + stateYtdTaxSoFar: 0 + }; } - // Add this month’s gross to YTD - taxStateByYear[currentYear].ytdGross += grossMonthlyIncome; + // Update YTD gross for federal + state + taxStateByYear[currentYear].federalYtdGross += grossMonthlyIncome; + taxStateByYear[currentYear].stateYtdGross += grossMonthlyIncome; - // Calculate total tax for YTD - const annualTaxSoFar = calculateAnnualFederalTaxSingle( - taxStateByYear[currentYear].ytdGross + // Compute total fed tax for the year so far + const newFedTaxTotal = calculateAnnualFederalTaxSingle( + taxStateByYear[currentYear].federalYtdGross ); + // Monthly fed tax = difference + const monthlyFederalTax = newFedTaxTotal - taxStateByYear[currentYear].federalYtdTaxSoFar; + taxStateByYear[currentYear].federalYtdTaxSoFar = newFedTaxTotal; - // This month’s tax = (new YTD tax) - (old YTD tax) - const monthlyTax = annualTaxSoFar - taxStateByYear[currentYear].ytdTaxSoFar; + // Compute total state tax for the year so far + const newStateTaxTotal = calculateAnnualStateTax( + taxStateByYear[currentYear].stateYtdGross, + stateCode + ); + const monthlyStateTax = newStateTaxTotal - taxStateByYear[currentYear].stateYtdTaxSoFar; + taxStateByYear[currentYear].stateYtdTaxSoFar = newStateTaxTotal; - // Update YTD tax - taxStateByYear[currentYear].ytdTaxSoFar = annualTaxSoFar; + // Combined monthly tax + const combinedTax = monthlyFederalTax + monthlyStateTax; - // Net monthly income after tax - const netMonthlyIncome = grossMonthlyIncome - monthlyTax; + // Net monthly income after taxes + const netMonthlyIncome = grossMonthlyIncome - combinedTax; - // 11. Monthly expenses (excluding student loan if deferring) + // 11. Expenses & loan let thisMonthLoanPayment = 0; let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth; - // Re-amortize if we're just now exiting college + // Re-amortize if just exited college if (nowExitingCollege) { monthlyLoanPayment = calculateLoanPayment( loanBalance, interestRate, - 10 // fresh 10-year term post-college + 10 ); } - // 12. If still deferring, just accrue interest + // If not deferring, we do normal payments if (stillInCollege && loanDeferralUntilGraduation) { const interestForMonth = loanBalance * (interestRate / 100 / 12); loanBalance += interestForMonth; } else { - // Normal repayment if loan > 0 if (loanBalance > 0) { const interestForMonth = loanBalance * (interestRate / 100 / 12); const principalForMonth = Math.min( @@ -303,11 +319,11 @@ export function simulateFinancialProjection(userProfile) { } } - // 13. leftover after mandatory expenses + // 12. leftover after mandatory expenses let leftover = netMonthlyIncome - totalMonthlyExpenses; if (leftover < 0) leftover = 0; - // Baseline monthly contributions + // Baseline contributions const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution; let effectiveRetirementContribution = 0; let effectiveEmergencyContribution = 0; @@ -317,48 +333,44 @@ export function simulateFinancialProjection(userProfile) { effectiveEmergencyContribution = monthlyEmergencyContribution; leftover -= baselineContributions; } else { - // Not enough leftover => zero out contributions effectiveRetirementContribution = 0; effectiveEmergencyContribution = 0; } - // Check for shortfall vs. mandatory expenses + // Check shortfall const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; let shortfall = actualExpensesPaid - netMonthlyIncome; if (shortfall > 0) { - // 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) + // bankrupt scenario break; } } - // 14. Surplus allocation if leftover > 0 - const newLeftover = leftover; - if (newLeftover > 0) { - // Allocate by percentage + // 13. Surplus + if (leftover > 0) { const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation; - const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct); - const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct); + const emergencyPortion = leftover * (surplusEmergencyAllocation / totalPct); + const retirementPortion = leftover * (surplusRetirementAllocation / totalPct); currentEmergencySavings += emergencyPortion; currentRetirementSavings += retirementPortion; } - // 15. netSavings for the month (could be leftover minus contributions, etc.) - // But we already subtracted everything from netMonthlyIncome except what ended up surplus + // netSavings for display 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')}`, grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100, - monthlyTax: Math.round(monthlyTax * 100) / 100, + monthlyFederalTax: Math.round(monthlyFederalTax * 100) / 100, + monthlyStateTax: Math.round(monthlyStateTax * 100) / 100, + combinedTax: Math.round(combinedTax * 100) / 100, netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100, totalExpenses: Math.round(finalExpensesPaid * 100) / 100, effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100, @@ -370,11 +382,9 @@ export function simulateFinancialProjection(userProfile) { loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100 }); - // Update deferral flag for next iteration wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); } - // Return the final output return { projectionData, loanPaidOffMonth,