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

@ -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: users 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 months 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 months 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,