State taxes added to simulation

This commit is contained in:
Josh 2025-04-17 15:53:32 +00:00
parent 2f9dc03f57
commit e3d804e01a

View File

@ -3,16 +3,11 @@ import moment from 'moment';
/** /**
* Single-filer federal tax calculation (2023). * Single-filer federal tax calculation (2023).
* Includes standard deduction ($13,850). * Includes standard deduction ($13,850).
* If you need to update the brackets/deduction, edit here.
*/ */
function calculateAnnualFederalTaxSingle(annualIncome) { function calculateAnnualFederalTaxSingle(annualIncome) {
// 1. Subtract standard deduction
const STANDARD_DEDUCTION_SINGLE = 13850; const STANDARD_DEDUCTION_SINGLE = 13850;
const taxableIncome = Math.max(0, annualIncome - STANDARD_DEDUCTION_SINGLE); 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 = [ const brackets = [
{ limit: 11000, rate: 0.10 }, { limit: 11000, rate: 0.10 },
{ limit: 44725, rate: 0.12 }, { limit: 44725, rate: 0.12 },
@ -26,23 +21,37 @@ function calculateAnnualFederalTaxSingle(annualIncome) {
let tax = 0; let tax = 0;
let lastLimit = 0; let lastLimit = 0;
// 3. Accumulate tax across brackets
for (let i = 0; i < brackets.length; i++) { for (let i = 0; i < brackets.length; i++) {
const { limit, rate } = brackets[i]; const { limit, rate } = brackets[i];
if (taxableIncome <= limit) { if (taxableIncome <= limit) {
// only tax the portion within this bracket
tax += (taxableIncome - lastLimit) * rate; tax += (taxableIncome - lastLimit) * rate;
break; break;
} else { } else {
// tax the entire bracket range, then continue
tax += (limit - lastLimit) * rate; tax += (limit - lastLimit) * rate;
lastLimit = limit; lastLimit = limit;
} }
} }
return tax; 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). * 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; const numPayments = years * 12;
if (monthlyRate === 0) { if (monthlyRate === 0) {
// no interest
return principal / numPayments; return principal / numPayments;
} }
return ( 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) { export function simulateFinancialProjection(userProfile) {
const { const {
@ -107,10 +115,13 @@ export function simulateFinancialProjection(userProfile) {
surplusRetirementAllocation = 50, surplusRetirementAllocation = 50,
// Potential override // Potential override
programLength programLength,
// NEW: users state code (e.g. 'CA', 'NY', 'TX', etc.)
stateCode = 'TX', // default to TX (no state income tax)
} = userProfile; } = userProfile;
// 1. Calculate standard monthly loan payment (if not deferring) // 1. Monthly loan payment if not deferring
let monthlyLoanPayment = loanDeferralUntilGraduation let monthlyLoanPayment = loanDeferralUntilGraduation
? 0 ? 0
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm); : calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
@ -137,13 +148,12 @@ export function simulateFinancialProjection(userProfile) {
const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear); const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear);
const finalProgramLength = programLength || dynamicProgramLength; 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 netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid);
const totalTuitionCost = netAnnualTuition * finalProgramLength; const totalTuitionCost = netAnnualTuition * finalProgramLength;
// 4. Setup lumps per year based on academicCalendar // 4. Setup lumps per year
let lumpsPerYear = 12; // monthly fallback let lumpsPerYear, lumpsSchedule;
let lumpsSchedule = [];
switch (academicCalendar) { switch (academicCalendar) {
case 'semester': case 'semester':
lumpsPerYear = 2; lumpsPerYear = 2;
@ -160,41 +170,39 @@ export function simulateFinancialProjection(userProfile) {
case 'monthly': case 'monthly':
default: default:
lumpsPerYear = 12; lumpsPerYear = 12;
lumpsSchedule = [...Array(12).keys()]; // 0..11 lumpsSchedule = [...Array(12).keys()];
break; break;
} }
const totalAcademicMonths = finalProgramLength * 12; const totalAcademicMonths = finalProgramLength * 12;
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength); const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
// 5. Simulation loop up to 20 years // 5. Simulation loop
const maxMonths = 240; const maxMonths = 240;
let date = startDate ? new Date(startDate) : new Date(); let date = startDate ? new Date(startDate) : new Date();
let loanBalance = Math.max(studentLoanAmount, 0); let loanBalance = Math.max(studentLoanAmount, 0);
let loanPaidOffMonth = null; let loanPaidOffMonth = null;
let currentEmergencySavings = emergencySavings; let currentEmergencySavings = emergencySavings;
let currentRetirementSavings = retirementSavings; let currentRetirementSavings = retirementSavings;
let projectionData = []; let projectionData = [];
let wasInDeferral = inCollege && loanDeferralUntilGraduation; let wasInDeferral = inCollege && loanDeferralUntilGraduation;
// If gradDate is provided, parse it to Date
const graduationDate = gradDate ? new Date(gradDate) : null; 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 = {}; const taxStateByYear = {};
for (let month = 0; month < maxMonths; month++) { for (let month = 0; month < maxMonths; month++) {
date.setMonth(date.getMonth() + 1); date.setMonth(date.getMonth() + 1);
const currentYear = date.getFullYear(); const currentYear = date.getFullYear();
// If loan is fully paid, record if not already done // Check if loan is fully paid
if (loanBalance <= 0 && !loanPaidOffMonth) { if (loanBalance <= 0 && !loanPaidOffMonth) {
loanPaidOffMonth = `${currentYear}-${String(date.getMonth() + 1).padStart(2, '0')}`; 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; let stillInCollege = false;
if (inCollege) { if (inCollege) {
if (graduationDate) { 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; let tuitionCostThisMonth = 0;
if (stillInCollege && lumpsPerYear > 0) { if (stillInCollege && lumpsPerYear > 0) {
const simStart = startDate ? new Date(startDate) : new Date(); const simStart = startDate ? new Date(startDate) : new Date();
@ -224,10 +232,10 @@ export function simulateFinancialProjection(userProfile) {
} }
} }
// 7. Detect if we are now exiting college this month // 7. Exiting college?
const nowExitingCollege = (wasInDeferral && !stillInCollege); 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 (stillInCollege && loanDeferralUntilGraduation) {
if (tuitionCostThisMonth > 0) { if (tuitionCostThisMonth > 0) {
loanBalance += tuitionCostThisMonth; loanBalance += tuitionCostThisMonth;
@ -235,60 +243,68 @@ export function simulateFinancialProjection(userProfile) {
} }
} }
// 9. Gross monthly income depends on college status // 9. Gross monthly income
let grossMonthlyIncome = 0; let grossMonthlyIncome = 0;
if (!inCollege || !stillInCollege) { if (!inCollege || !stillInCollege) {
// Graduated or never was in college => use expectedSalary if given
grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12; grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
} else { } else {
// Still in college => currentSalary + part-time
grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12); grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
} }
/** // 10. Tax calculations
* 10. Calculate monthly TAX via bracket-based approach:
* We track year-to-date (YTD) for the current calendar year.
*/
if (!taxStateByYear[currentYear]) { if (!taxStateByYear[currentYear]) {
taxStateByYear[currentYear] = { ytdGross: 0, ytdTaxSoFar: 0 }; taxStateByYear[currentYear] = {
federalYtdGross: 0,
federalYtdTaxSoFar: 0,
stateYtdGross: 0,
stateYtdTaxSoFar: 0
};
} }
// Add this months gross to YTD // Update YTD gross for federal + state
taxStateByYear[currentYear].ytdGross += grossMonthlyIncome; taxStateByYear[currentYear].federalYtdGross += grossMonthlyIncome;
taxStateByYear[currentYear].stateYtdGross += grossMonthlyIncome;
// Calculate total tax for YTD // Compute total fed tax for the year so far
const annualTaxSoFar = calculateAnnualFederalTaxSingle( const newFedTaxTotal = calculateAnnualFederalTaxSingle(
taxStateByYear[currentYear].ytdGross taxStateByYear[currentYear].federalYtdGross
); );
// Monthly fed tax = difference
const monthlyFederalTax = newFedTaxTotal - taxStateByYear[currentYear].federalYtdTaxSoFar;
taxStateByYear[currentYear].federalYtdTaxSoFar = newFedTaxTotal;
// This months tax = (new YTD tax) - (old YTD tax) // Compute total state tax for the year so far
const monthlyTax = annualTaxSoFar - taxStateByYear[currentYear].ytdTaxSoFar; const newStateTaxTotal = calculateAnnualStateTax(
taxStateByYear[currentYear].stateYtdGross,
stateCode
);
const monthlyStateTax = newStateTaxTotal - taxStateByYear[currentYear].stateYtdTaxSoFar;
taxStateByYear[currentYear].stateYtdTaxSoFar = newStateTaxTotal;
// Update YTD tax // Combined monthly tax
taxStateByYear[currentYear].ytdTaxSoFar = annualTaxSoFar; const combinedTax = monthlyFederalTax + monthlyStateTax;
// Net monthly income after tax // Net monthly income after taxes
const netMonthlyIncome = grossMonthlyIncome - monthlyTax; const netMonthlyIncome = grossMonthlyIncome - combinedTax;
// 11. Monthly expenses (excluding student loan if deferring) // 11. Expenses & loan
let thisMonthLoanPayment = 0; let thisMonthLoanPayment = 0;
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth; let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth;
// Re-amortize if we're just now exiting college // Re-amortize if just exited college
if (nowExitingCollege) { if (nowExitingCollege) {
monthlyLoanPayment = calculateLoanPayment( monthlyLoanPayment = calculateLoanPayment(
loanBalance, loanBalance,
interestRate, 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) { if (stillInCollege && loanDeferralUntilGraduation) {
const interestForMonth = loanBalance * (interestRate / 100 / 12); const interestForMonth = loanBalance * (interestRate / 100 / 12);
loanBalance += interestForMonth; loanBalance += interestForMonth;
} else { } else {
// Normal repayment if loan > 0
if (loanBalance > 0) { if (loanBalance > 0) {
const interestForMonth = loanBalance * (interestRate / 100 / 12); const interestForMonth = loanBalance * (interestRate / 100 / 12);
const principalForMonth = Math.min( 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; let leftover = netMonthlyIncome - totalMonthlyExpenses;
if (leftover < 0) leftover = 0; if (leftover < 0) leftover = 0;
// Baseline monthly contributions // Baseline contributions
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution; const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
let effectiveRetirementContribution = 0; let effectiveRetirementContribution = 0;
let effectiveEmergencyContribution = 0; let effectiveEmergencyContribution = 0;
@ -317,48 +333,44 @@ export function simulateFinancialProjection(userProfile) {
effectiveEmergencyContribution = monthlyEmergencyContribution; effectiveEmergencyContribution = monthlyEmergencyContribution;
leftover -= baselineContributions; leftover -= baselineContributions;
} else { } else {
// Not enough leftover => zero out contributions
effectiveRetirementContribution = 0; effectiveRetirementContribution = 0;
effectiveEmergencyContribution = 0; effectiveEmergencyContribution = 0;
} }
// Check for shortfall vs. mandatory expenses // Check shortfall
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution; const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions; const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
let shortfall = actualExpensesPaid - netMonthlyIncome; let shortfall = actualExpensesPaid - netMonthlyIncome;
if (shortfall > 0) { if (shortfall > 0) {
// Attempt to cover from emergency savings
const canCover = Math.min(shortfall, currentEmergencySavings); const canCover = Math.min(shortfall, currentEmergencySavings);
currentEmergencySavings -= canCover; currentEmergencySavings -= canCover;
shortfall -= canCover; shortfall -= canCover;
if (shortfall > 0) { if (shortfall > 0) {
// Even after emergency, we can't cover => break (bankrupt scenario) // bankrupt scenario
break; break;
} }
} }
// 14. Surplus allocation if leftover > 0 // 13. Surplus
const newLeftover = leftover; if (leftover > 0) {
if (newLeftover > 0) {
// Allocate by percentage
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation; const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct); const emergencyPortion = leftover * (surplusEmergencyAllocation / totalPct);
const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct); const retirementPortion = leftover * (surplusRetirementAllocation / totalPct);
currentEmergencySavings += emergencyPortion; currentEmergencySavings += emergencyPortion;
currentRetirementSavings += retirementPortion; currentRetirementSavings += retirementPortion;
} }
// 15. netSavings for the month (could be leftover minus contributions, etc.) // netSavings for display
// But we already subtracted everything from netMonthlyIncome except what ended up surplus
const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions; const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
const netSavings = netMonthlyIncome - finalExpensesPaid; const netSavings = netMonthlyIncome - finalExpensesPaid;
// Record in the projection data
projectionData.push({ projectionData.push({
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`, month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100, 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, netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100,
totalExpenses: Math.round(finalExpensesPaid * 100) / 100, totalExpenses: Math.round(finalExpensesPaid * 100) / 100,
effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100, effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100,
@ -370,11 +382,9 @@ export function simulateFinancialProjection(userProfile) {
loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100 loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100
}); });
// Update deferral flag for next iteration
wasInDeferral = (stillInCollege && loanDeferralUntilGraduation); wasInDeferral = (stillInCollege && loanDeferralUntilGraduation);
} }
// Return the final output
return { return {
projectionData, projectionData,
loanPaidOffMonth, loanPaidOffMonth,