State taxes added to simulation
This commit is contained in:
parent
2f9dc03f57
commit
e3d804e01a
@ -3,16 +3,11 @@ 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.
|
||||
*/
|
||||
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 {
|
||||
@ -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,13 +148,12 @@ 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;
|
||||
@ -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,10 +232,10 @@ 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;
|
||||
@ -235,60 +243,68 @@ export function simulateFinancialProjection(userProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
Loading…
Reference in New Issue
Block a user