Added taxes to simulation and fixed LoanBalance calculation from inCollege logic.
This commit is contained in:
parent
ab7e318492
commit
2f9dc03f57
@ -27,7 +27,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId })
|
|||||||
loan_term = 10,
|
loan_term = 10,
|
||||||
extra_payment = '',
|
extra_payment = '',
|
||||||
expected_salary = '',
|
expected_salary = '',
|
||||||
is_in_state = true,
|
is_in_state = false,
|
||||||
is_in_district = false,
|
is_in_district = false,
|
||||||
loan_deferral_until_graduation = false,
|
loan_deferral_until_graduation = false,
|
||||||
credit_hours_per_year = '',
|
credit_hours_per_year = '',
|
||||||
@ -335,6 +335,46 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId })
|
|||||||
{(college_enrollment_status === 'currently_enrolled' ||
|
{(college_enrollment_status === 'currently_enrolled' ||
|
||||||
college_enrollment_status === 'prospective_student') ? (
|
college_enrollment_status === 'prospective_student') ? (
|
||||||
<>
|
<>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_in_district"
|
||||||
|
checked={is_in_district}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
In District?
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_in_state"
|
||||||
|
checked={is_in_state}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
In State Tuition?
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="is_online"
|
||||||
|
checked={is_online}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
Program is Fully Online
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="loan_deferral_until_graduation"
|
||||||
|
checked={loan_deferral_until_graduation}
|
||||||
|
onChange={handleParentFieldChange}
|
||||||
|
/>
|
||||||
|
Defer Loan Payments until Graduation?
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>School Name*</label>
|
<label>School Name*</label>
|
||||||
<input
|
<input
|
||||||
name="selected_school"
|
name="selected_school"
|
||||||
@ -509,45 +549,7 @@ function CollegeOnboarding({ nextStep, prevStep, data, setData, careerPathId })
|
|||||||
placeholder="e.g. 65000"
|
placeholder="e.g. 65000"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="is_in_district"
|
|
||||||
checked={is_in_district}
|
|
||||||
onChange={handleParentFieldChange}
|
|
||||||
/>
|
|
||||||
In District?
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="is_in_state"
|
|
||||||
checked={is_in_state}
|
|
||||||
onChange={handleParentFieldChange}
|
|
||||||
/>
|
|
||||||
In State Tuition?
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="is_online"
|
|
||||||
checked={is_online}
|
|
||||||
onChange={handleParentFieldChange}
|
|
||||||
/>
|
|
||||||
Program is Fully Online
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="loan_deferral_until_graduation"
|
|
||||||
checked={loan_deferral_until_graduation}
|
|
||||||
onChange={handleParentFieldChange}
|
|
||||||
/>
|
|
||||||
Defer Loan Payments until Graduation?
|
|
||||||
</label>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p>Not currently enrolled or prospective student. Skipping college onboarding.</p>
|
<p>Not currently enrolled or prospective student. Skipping college onboarding.</p>
|
||||||
|
@ -1,12 +1,70 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
// Example fields in userProfile that matter here:
|
/**
|
||||||
// - academicCalendar: 'semester' | 'quarter' | 'trimester' | 'monthly'
|
* Single-filer federal tax calculation (2023).
|
||||||
// - annualFinancialAid: amount of scholarships/grants per year
|
* Includes standard deduction ($13,850).
|
||||||
// - inCollege, loanDeferralUntilGraduation, graduationDate, etc.
|
* If you need to update the brackets/deduction, edit here.
|
||||||
//
|
*/
|
||||||
// Additional logic now for lumps instead of monthly tuition payments.
|
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) {
|
export function simulateFinancialProjection(userProfile) {
|
||||||
const {
|
const {
|
||||||
// Income & expenses
|
// Income & expenses
|
||||||
@ -18,8 +76,8 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
|
|
||||||
// Loan info
|
// Loan info
|
||||||
studentLoanAmount = 0,
|
studentLoanAmount = 0,
|
||||||
interestRate = 5, // %
|
interestRate = 5, // %
|
||||||
loanTerm = 10, // years
|
loanTerm = 10, // years
|
||||||
loanDeferralUntilGraduation = false,
|
loanDeferralUntilGraduation = false,
|
||||||
|
|
||||||
// College & tuition
|
// College & tuition
|
||||||
@ -27,10 +85,10 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
programType,
|
programType,
|
||||||
hoursCompleted = 0,
|
hoursCompleted = 0,
|
||||||
creditHoursPerYear,
|
creditHoursPerYear,
|
||||||
calculatedTuition, // e.g. annual tuition
|
calculatedTuition,
|
||||||
gradDate, // known graduation date, or null
|
gradDate,
|
||||||
startDate, // when sim starts
|
startDate,
|
||||||
academicCalendar = 'monthly', // new
|
academicCalendar = 'monthly',
|
||||||
annualFinancialAid = 0,
|
annualFinancialAid = 0,
|
||||||
|
|
||||||
// Salary after graduation
|
// Salary after graduation
|
||||||
@ -45,15 +103,17 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
monthlyEmergencyContribution = 0,
|
monthlyEmergencyContribution = 0,
|
||||||
|
|
||||||
// Surplus allocation
|
// Surplus allocation
|
||||||
surplusEmergencyAllocation = 50,
|
surplusEmergencyAllocation = 50,
|
||||||
surplusRetirementAllocation = 50,
|
surplusRetirementAllocation = 50,
|
||||||
|
|
||||||
// Potential override
|
// Potential override
|
||||||
programLength
|
programLength
|
||||||
} = userProfile;
|
} = userProfile;
|
||||||
|
|
||||||
// 1. Calculate standard monthly loan payment
|
// 1. Calculate standard monthly loan payment (if not deferring)
|
||||||
const monthlyLoanPayment = calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
let monthlyLoanPayment = loanDeferralUntilGraduation
|
||||||
|
? 0
|
||||||
|
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||||
|
|
||||||
// 2. Determine how many credit hours remain
|
// 2. Determine how many credit hours remain
|
||||||
let requiredCreditHours = 120;
|
let requiredCreditHours = 120;
|
||||||
@ -83,14 +143,11 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
|
|
||||||
// 4. Setup lumps per year based on academicCalendar
|
// 4. Setup lumps per year based on academicCalendar
|
||||||
let lumpsPerYear = 12; // monthly fallback
|
let lumpsPerYear = 12; // monthly fallback
|
||||||
let lumpsSchedule = []; // which months from start of academic year
|
let lumpsSchedule = [];
|
||||||
|
|
||||||
// We'll store an array of month offsets in a single year (0-based)
|
|
||||||
// for semester, quarter, trimester
|
|
||||||
switch (academicCalendar) {
|
switch (academicCalendar) {
|
||||||
case 'semester':
|
case 'semester':
|
||||||
lumpsPerYear = 2;
|
lumpsPerYear = 2;
|
||||||
lumpsSchedule = [0, 6]; // months 0 & 6 from start of each academic year
|
lumpsSchedule = [0, 6];
|
||||||
break;
|
break;
|
||||||
case 'quarter':
|
case 'quarter':
|
||||||
lumpsPerYear = 4;
|
lumpsPerYear = 4;
|
||||||
@ -106,102 +163,132 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
lumpsSchedule = [...Array(12).keys()]; // 0..11
|
lumpsSchedule = [...Array(12).keys()]; // 0..11
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each academic year is 12 months, for finalProgramLength years => totalAcademicMonths
|
|
||||||
const totalAcademicMonths = finalProgramLength * 12;
|
const totalAcademicMonths = finalProgramLength * 12;
|
||||||
// Each lump sum = totalTuitionCost / (lumpsPerYear * finalProgramLength)
|
|
||||||
const lumpAmount = 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;
|
const maxMonths = 240;
|
||||||
let date = startDate ? new Date(startDate) : new Date();
|
let date = startDate ? new Date(startDate) : new Date();
|
||||||
let loanBalance = studentLoanAmount;
|
|
||||||
|
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 = [];
|
|
||||||
|
|
||||||
// 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;
|
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++) {
|
for (let month = 0; month < maxMonths; month++) {
|
||||||
date.setMonth(date.getMonth() + 1);
|
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) {
|
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;
|
let stillInCollege = false;
|
||||||
if (inCollege) {
|
if (inCollege) {
|
||||||
if (graduationDate) {
|
if (graduationDate) {
|
||||||
stillInCollege = date < graduationDate;
|
stillInCollege = date < graduationDate;
|
||||||
} else {
|
} else {
|
||||||
// approximate by how many months since start
|
|
||||||
const simStart = startDate ? new Date(startDate) : new Date();
|
const simStart = startDate ? new Date(startDate) : new Date();
|
||||||
const elapsedMonths =
|
const elapsedMonths =
|
||||||
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||||
(date.getMonth() - simStart.getMonth());
|
(date.getMonth() - simStart.getMonth());
|
||||||
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
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
|
// 6. Check if we owe tuition lumps this month
|
||||||
// We'll find how many academic years have passed since they started
|
|
||||||
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();
|
||||||
const elapsedMonths =
|
const elapsedMonths =
|
||||||
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
(date.getFullYear() - simStart.getFullYear()) * 12 +
|
||||||
(date.getMonth() - simStart.getMonth());
|
(date.getMonth() - simStart.getMonth());
|
||||||
|
|
||||||
// Which academic year index are we in?
|
|
||||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||||
// Within that year, which month are we in? (0..11)
|
|
||||||
const monthInYear = elapsedMonths % 12;
|
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) {
|
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||||
tuitionCostThisMonth = lumpAmount;
|
tuitionCostThisMonth = lumpAmount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Decide if user defers or pays out of pocket
|
// 7. Detect if we are now exiting college this month
|
||||||
// If deferring, add lumps to loan
|
const nowExitingCollege = (wasInDeferral && !stillInCollege);
|
||||||
|
|
||||||
|
// 8. If in deferral, lumps get added to the loan principal
|
||||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
console.log(" deferral is on, lumps => loan?");
|
|
||||||
// Instead of user paying out of pocket, add to loan
|
|
||||||
if (tuitionCostThisMonth > 0) {
|
if (tuitionCostThisMonth > 0) {
|
||||||
console.log(" tuitionCostThisMonth=", tuitionCostThisMonth);
|
|
||||||
loanBalance += tuitionCostThisMonth;
|
loanBalance += tuitionCostThisMonth;
|
||||||
tuitionCostThisMonth = 0; // paid by the loan
|
tuitionCostThisMonth = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. monthly income
|
// 9. Gross monthly income depends on college status
|
||||||
let monthlyIncome = 0;
|
let grossMonthlyIncome = 0;
|
||||||
if (!inCollege || !stillInCollege) {
|
if (!inCollege || !stillInCollege) {
|
||||||
// user has graduated or never in college
|
// Graduated or never was in college => use expectedSalary if given
|
||||||
monthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
||||||
} else {
|
} else {
|
||||||
// in college => currentSalary + partTimeIncome
|
// Still in college => currentSalary + part-time
|
||||||
monthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
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 thisMonthLoanPayment = 0;
|
||||||
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth;
|
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) {
|
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||||
// Accrue interest only
|
|
||||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||||
loanBalance += interestForMonth;
|
loanBalance += interestForMonth;
|
||||||
} else {
|
} else {
|
||||||
// Normal loan repayment if loan > 0
|
// 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(
|
||||||
@ -210,16 +297,15 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
);
|
);
|
||||||
loanBalance -= principalForMonth;
|
loanBalance -= principalForMonth;
|
||||||
loanBalance = Math.max(loanBalance, 0);
|
loanBalance = Math.max(loanBalance, 0);
|
||||||
|
|
||||||
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
||||||
totalMonthlyExpenses += thisMonthLoanPayment;
|
totalMonthlyExpenses += thisMonthLoanPayment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. leftover after mandatory expenses
|
// 13. leftover after mandatory expenses
|
||||||
let leftover = monthlyIncome - totalMonthlyExpenses;
|
let leftover = netMonthlyIncome - totalMonthlyExpenses;
|
||||||
if (leftover < 0) {
|
if (leftover < 0) leftover = 0;
|
||||||
leftover = 0; // can't do partial negative leftover; they simply can't afford it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Baseline monthly contributions
|
// Baseline monthly contributions
|
||||||
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
||||||
@ -231,91 +317,69 @@ export function simulateFinancialProjection(userProfile) {
|
|||||||
effectiveEmergencyContribution = monthlyEmergencyContribution;
|
effectiveEmergencyContribution = monthlyEmergencyContribution;
|
||||||
leftover -= baselineContributions;
|
leftover -= baselineContributions;
|
||||||
} else {
|
} else {
|
||||||
// not enough leftover
|
// Not enough leftover => zero out contributions
|
||||||
// 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.
|
|
||||||
effectiveRetirementContribution = 0;
|
effectiveRetirementContribution = 0;
|
||||||
effectiveEmergencyContribution = 0;
|
effectiveEmergencyContribution = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Now see if leftover is negative => shortfall from mandatory expenses
|
// Check for shortfall vs. 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;
|
|
||||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||||
let shortfall = actualExpensesPaid - monthlyIncome; // if positive => can't pay
|
let shortfall = actualExpensesPaid - netMonthlyIncome;
|
||||||
console.log(" end of month: loanBal=", loanBalance, " shortfall=", shortfall);
|
|
||||||
if (shortfall > 0) {
|
if (shortfall > 0) {
|
||||||
console.log(" Breaking out - bankrupt scenario");
|
// 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)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. If leftover > 0 after baseline contributions, allocate surplus
|
// 14. Surplus allocation if leftover > 0
|
||||||
// (we do it after we've handled shortfall)
|
const newLeftover = leftover;
|
||||||
const newLeftover = leftover; // leftover not used for baseline
|
|
||||||
let surplusUsed = 0;
|
|
||||||
if (newLeftover > 0) {
|
if (newLeftover > 0) {
|
||||||
// Allocate by percent
|
// Allocate by percentage
|
||||||
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
||||||
const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct);
|
const emergencyPortion = newLeftover * (surplusEmergencyAllocation / totalPct);
|
||||||
const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct);
|
const retirementPortion = newLeftover * (surplusRetirementAllocation / totalPct);
|
||||||
|
|
||||||
currentEmergencySavings += emergencyPortion;
|
currentEmergencySavings += emergencyPortion;
|
||||||
currentRetirementSavings += retirementPortion;
|
currentRetirementSavings += retirementPortion;
|
||||||
surplusUsed = newLeftover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. netSavings is monthlyIncome - actual expenses - all contributions
|
// 15. netSavings for the month (could be leftover minus contributions, etc.)
|
||||||
// But we must recalc actual final expenses paid
|
// But we already subtracted everything from netMonthlyIncome except what ended up surplus
|
||||||
const finalExpensesPaid = totalMonthlyExpenses + (effectiveRetirementContribution + effectiveEmergencyContribution);
|
const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||||
const netSavings = monthlyIncome - 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')}`,
|
||||||
monthlyIncome,
|
grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100,
|
||||||
totalExpenses: finalExpensesPaid,
|
monthlyTax: Math.round(monthlyTax * 100) / 100,
|
||||||
effectiveRetirementContribution,
|
netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100,
|
||||||
effectiveEmergencyContribution,
|
totalExpenses: Math.round(finalExpensesPaid * 100) / 100,
|
||||||
netSavings,
|
effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100,
|
||||||
emergencySavings: currentEmergencySavings,
|
effectiveEmergencyContribution: Math.round(effectiveEmergencyContribution * 100) / 100,
|
||||||
retirementSavings: currentRetirementSavings,
|
netSavings: Math.round(netSavings * 100) / 100,
|
||||||
|
emergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
||||||
|
retirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
||||||
loanBalance: Math.round(loanBalance * 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 {
|
return {
|
||||||
projectionData,
|
projectionData,
|
||||||
loanPaidOffMonth,
|
loanPaidOffMonth,
|
||||||
finalEmergencySavings: currentEmergencySavings,
|
finalEmergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
||||||
finalRetirementSavings: currentRetirementSavings,
|
finalRetirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
||||||
finalLoanBalance: Math.round(loanBalance * 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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user