1
0
mirror of https://gitlab.com/MisterBiggs/secure-act-2.0.git synced 2025-08-17 16:24:43 +00:00
Files
secure-act-2.0/calculations.js
Anson Biggs 8880b729d7 Fix calculation inconsistencies and chart data cutoff
- Fix chart data showing age 70 values instead of age 65 retirement values
- Update all hardcoded examples to match actual calculator output
- Recent Graduate: 75K → 94K (corrected retirement value)
- MBA Graduate: 25K → 29K (corrected retirement value)
- Medical Professional: 85K → ,065K (corrected)
- Teacher: 8K → 88K (corrected)
- Software Engineer: 12K → ,132K (corrected)
- Attorney: 5K → 54K (corrected)
- All examples now mathematically consistent with calculator logic
- Charts no longer appear to 'slow down' at retirement age
2025-08-13 12:13:04 -06:00

209 lines
7.9 KiB
JavaScript

/**
* Student Loan Matching Calculator Logic
* Calculates net worth over time with and without SECURE Act 2.0 matching
*/
const INVESTMENT_RETURN_RATE = 0.07; // 7% annual return
const LOAN_INTEREST_RATE = 0.05; // 5% annual interest (federal student loan average)
const RETIREMENT_AGE = 65;
const MAX_CALCULATION_AGE = 70; // Calculate up to age 70 for complete data
/**
* Calculate loan payoff time in years
* @param {number} totalDebt - Total loan balance
* @param {number} monthlyPayment - Monthly payment amount
* @returns {number} Years to pay off loan
*/
function calculatePayoffYears(totalDebt, monthlyPayment) {
if (!monthlyPayment || monthlyPayment <= 0) return 0;
const monthlyRate = LOAN_INTEREST_RATE / 12;
const minPayment = totalDebt * monthlyRate;
if (monthlyPayment <= minPayment) {
return 50; // Loan will never be paid off at this rate
}
const months = Math.log(monthlyPayment / (monthlyPayment - totalDebt * monthlyRate)) / Math.log(1 + monthlyRate);
return Math.ceil(months / 12);
}
/**
* Calculate remaining loan balance after a number of months
* @param {number} originalBalance - Original loan amount
* @param {number} monthlyPayment - Monthly payment
* @param {number} monthsElapsed - Months since start of loan
* @param {number} totalMonths - Total months for loan term
* @returns {number} Remaining balance
*/
function calculateRemainingBalance(originalBalance, monthlyPayment, monthsElapsed, totalMonths) {
if (monthsElapsed <= 0) return originalBalance;
if (monthsElapsed >= totalMonths) return 0;
const monthlyRate = LOAN_INTEREST_RATE / 12;
const factor = Math.pow(1 + monthlyRate, monthsElapsed);
const paymentsValue = monthlyPayment * ((factor - 1) / monthlyRate);
const remainingBalance = originalBalance * factor - paymentsValue;
return Math.max(0, remainingBalance);
}
/**
* Calculate future value of retirement contributions with compound growth
* @param {number} annualContribution - Annual contribution amount
* @param {number} yearsContributing - Number of years contributing
* @param {number} yearsToGrow - Total years for growth (from first contribution to retirement)
* @returns {number} Future value
*/
function calculateRetirementValue(annualContribution, yearsContributing, yearsToGrow) {
let totalValue = 0;
for (let year = 0; year < yearsContributing; year++) {
const growthYears = yearsToGrow - year;
totalValue += annualContribution * Math.pow(1 + INVESTMENT_RETURN_RATE, growthYears);
}
return totalValue;
}
/**
* Main calculation function for net worth comparison
* @param {Object} params - Input parameters
* @returns {Object} Chart data with ages and net worth values
*/
function calculateNetWorthComparison(params) {
const {
salary = 0,
totalDebt = 0,
monthlyPayment = 0,
matchRate = 0,
currentAge = 28
} = params;
// Calculate derived values
const payoffYears = calculatePayoffYears(totalDebt, monthlyPayment);
const annualMatch = Math.min(monthlyPayment * 12, salary * (matchRate / 100));
const payoffAge = currentAge + payoffYears;
// Arrays for full calculation data (yearly)
const allAges = [];
const allWithProgram = [];
const allWithoutProgram = [];
// Calculate for each year from current to age 70
for (let age = currentAge; age <= MAX_CALCULATION_AGE; age++) {
allAges.push(age);
const yearsElapsed = age - currentAge;
const monthsElapsed = yearsElapsed * 12;
// Calculate remaining loan balance
const totalMonths = payoffYears * 12;
const remainingDebt = calculateRemainingBalance(totalDebt, monthlyPayment, monthsElapsed, totalMonths);
if (age <= payoffAge) {
// DURING LOAN PAYMENT PERIOD
// With program: Get employer match while paying loans
const yearsContributing = Math.min(yearsElapsed, payoffYears);
const retirementValue = calculateRetirementValue(annualMatch, yearsContributing, yearsElapsed);
const netWorthWith = retirementValue - remainingDebt;
allWithProgram.push(Math.round(netWorthWith));
// Without program: No retirement savings, just debt
const netWorthWithout = -remainingDebt;
allWithoutProgram.push(Math.round(netWorthWithout));
} else {
// AFTER LOANS PAID OFF
const yearsSincePayoff = age - payoffAge;
const postPayoffContribution = Math.min(monthlyPayment * 12, salary * (matchRate / 100));
// With program: Initial match during loan period + continued contributions after
const duringLoanValue = calculateRetirementValue(annualMatch, payoffYears, yearsElapsed);
const afterLoanValue = calculateRetirementValue(postPayoffContribution, yearsSincePayoff, yearsSincePayoff);
const totalWithProgram = duringLoanValue + afterLoanValue;
allWithProgram.push(Math.round(totalWithProgram));
// Without program: Only start contributing after loans paid off
const totalWithoutProgram = calculateRetirementValue(postPayoffContribution, yearsSincePayoff, yearsSincePayoff);
allWithoutProgram.push(Math.round(totalWithoutProgram));
}
}
// Now filter the data for chart display (up to age 65)
// Show consistent points regardless of starting age: every 2 years on even ages
// Plus always include the starting age and retirement age
const ages = [];
const withProgram = [];
const withoutProgram = [];
for (let i = 0; i < allAges.length; i++) {
const age = allAges[i];
// Only include ages up to 65 for chart display
if (age > RETIREMENT_AGE) break;
// Include if:
// 1. It's the starting age (first point)
// 2. It's the retirement age (last point)
// 3. It's an even age and at least 2 years from start
const isStartAge = (i === 0);
const isRetirementAge = (age === RETIREMENT_AGE);
const isEvenInterval = (age % 2 === 0 && age > currentAge);
if (isStartAge || isRetirementAge || isEvenInterval) {
ages.push(age);
withProgram.push(allWithProgram[i]);
withoutProgram.push(allWithoutProgram[i]);
}
}
// Find age 65 index for retirement values
const age65Index = allAges.findIndex(age => age === RETIREMENT_AGE);
const retirementWith = age65Index >= 0 ? allWithProgram[age65Index] : allWithProgram[allWithProgram.length - 1];
const retirementWithout = age65Index >= 0 ? allWithoutProgram[age65Index] : allWithoutProgram[allWithoutProgram.length - 1];
return {
ages,
withProgram,
withoutProgram,
payoffYears,
annualMatch,
totalRetirementWith: retirementWith,
totalRetirementWithout: retirementWithout,
retirementDifference: retirementWith - retirementWithout,
// Include full yearly data if needed
allAges,
allWithProgram,
allWithoutProgram
};
}
// Export for use in browser
if (typeof window !== 'undefined') {
window.StudentLoanCalculator = {
calculatePayoffYears,
calculateRemainingBalance,
calculateRetirementValue,
calculateNetWorthComparison,
INVESTMENT_RETURN_RATE,
LOAN_INTEREST_RATE,
RETIREMENT_AGE,
MAX_CALCULATION_AGE
};
}
// Export for Node.js testing
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
calculatePayoffYears,
calculateRemainingBalance,
calculateRetirementValue,
calculateNetWorthComparison,
INVESTMENT_RETURN_RATE,
LOAN_INTEREST_RATE,
RETIREMENT_AGE,
MAX_CALCULATION_AGE
};
}