1
0
mirror of https://gitlab.com/MisterBiggs/secure-act-2.0.git synced 2025-08-17 16:24:43 +00:00

init commit

This commit is contained in:
2025-08-10 23:00:30 -06:00
commit 34fa4764a9
6 changed files with 1738 additions and 0 deletions

11
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,11 @@
pages:
stage: deploy
script:
- mkdir public
- cp index.html public/
- cp calculations.js public/
artifacts:
paths:
- public
only:
- main

20
AGENTS.md Normal file
View File

@@ -0,0 +1,20 @@
# AGENTS.md — secure_student_loan
## Important: Agents cannot run/view this project
**Testing**: User must open `index.html` in browser. For debugging, add `console.log()` statements and ask user for console output.
## Commands
- **Run tests**: `node calculations.test.js` (custom assert functions)
- **Single test**: Comment out other tests in file, then run
- **Lint**: None (maintain consistency manually)
## Code Style
- **No package manager**: CDN-only (Tailwind, Alpine.js v3, ApexCharts)
- **HTML**: 4-space indent, semantic tags, ARIA labels, mobile-first
- **CSS**: Tailwind utilities only; responsive: `md:` `lg:` `xl:`
- **JS**: ES6, Alpine reactivity (`x-data`, `x-model.number`, `x-show`)
- **Imports**: CDN scripts with `defer` before `</body>`
- **Naming**: kebab-case for x-data; camelCase for JS functions
- **Testing**: assert/assertClose functions; test edge cases
- **Structure**: index.html (UI), calculations.js (logic), calculations.test.js
- **Constants**: 7% investment return, 5% loan interest hardcoded

145
README.md Normal file
View File

@@ -0,0 +1,145 @@
# SECURE Act 2.0 Student Loan Matching Calculator
A free, interactive web calculator that helps employees and employers understand the financial impact of the SECURE Act 2.0 student loan matching program. This program allows employers to make 401(k) matching contributions based on employee student loan payments.
## 🚀 Live Demo
Visit the calculator at: [https://secure-student-loan.gitlab.io](https://secure-student-loan.gitlab.io)
## 📋 Features
- **Interactive Calculator**: Real-time calculations showing net worth comparison over time
- **Visual Charts**: Dynamic Chart.js visualizations comparing scenarios with and without the program
- **Multiple Personas**: Pre-loaded examples for different professions (teacher, engineer, attorney, etc.)
- **Export Options**: Download results as CSV or print for records
- **Share Functionality**: Share results via Web Share API or clipboard
- **Input Validation**: Comprehensive validation to prevent calculation errors
- **Responsive Design**: Mobile-first design that works on all devices
- **Accessibility**: ARIA labels and semantic HTML for screen reader support
## 🛠️ Technology Stack
- **HTML5**: Semantic markup for accessibility
- **Tailwind CSS**: Utility-first CSS framework (via CDN)
- **Alpine.js**: Lightweight reactive framework for interactivity
- **Chart.js**: Beautiful, responsive charts
- **GitLab Pages**: Static site hosting with CI/CD
## 💻 Local Development
### Prerequisites
- Any modern web browser
- Python (optional, for local server)
### Running Locally
1. Clone the repository:
```bash
git clone https://gitlab.com/your-username/secure-student-loan.git
cd secure-student-loan
```
2. Open directly in browser:
```bash
open index.html # macOS
xdg-open index.html # Linux
start index.html # Windows
```
Or use a local server:
```bash
python -m http.server 8000
# Visit http://localhost:8000
```
## 🚢 Deployment
The site automatically deploys to GitLab Pages when changes are pushed to the `main` branch.
### Manual Deployment
1. Push changes to the main branch:
```bash
git add .
git commit -m "Your commit message"
git push origin main
```
2. GitLab CI/CD will automatically:
- Copy `index.html` to the `public/` directory
- Deploy to GitLab Pages
## 📊 How the Calculator Works
### Key Assumptions
- **Investment Returns**: 7% average annual return (historical S&P 500 average)
- **Loan Interest Rate**: 5% annual rate (federal student loan average)
- **Retirement Age**: 65 years old
- **Compound Growth**: Each year's contribution grows from the time it's contributed until retirement
### Calculation Formula
1. **Annual Match Amount**:
```
min(Monthly Loan Payment × 12, Salary × Match Rate)
```
2. **Future Value with Compound Growth**:
```
FV = Σ(Annual Match × (1 + 0.07)^(65 - Current Age - Year))
```
3. **Net Worth Comparison**:
- With Program: Retirement Account Balance - Remaining Loan Balance
- Without Program: -Remaining Loan Balance (until loans paid off)
## 🤝 Contributing
We welcome contributions! Here's how you can help:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Test thoroughly in different browsers
5. Commit your changes (`git commit -m 'Add amazing feature'`)
6. Push to your branch (`git push origin feature/amazing-feature`)
7. Open a Merge Request
### Development Guidelines
- Maintain 4-space indentation in HTML
- Use Tailwind utility classes for styling
- Keep all functionality in the single `index.html` file
- Test on mobile devices and different screen sizes
- Ensure accessibility with proper ARIA labels
- Validate inputs to prevent calculation errors
## 📈 Future Enhancements
- [ ] Add inflation adjustment options
- [ ] Include tax implications calculator
- [ ] Add employer cost-benefit analysis
- [ ] Create embeddable widget version
- [ ] Add more detailed amortization schedules
- [ ] Include state-specific student loan programs
- [ ] Add comparison with other retirement strategies
## 📄 License
This project is open source and available under the MIT License.
## 🙏 Acknowledgments
- SECURE Act 2.0 legislation for making this benefit possible
- The millions of Americans managing student debt while trying to save for retirement
- Employers implementing this innovative benefit program
## 📞 Support
For questions, suggestions, or issues:
- Open an issue in the GitLab repository
- Contact via [your-email@example.com]
---
**Disclaimer**: This calculator provides estimates for educational purposes only. Actual results will vary based on your specific loan terms, employer plan details, investment performance, and contribution consistency. Consult with a financial advisor for personalized advice.

204
calculations.js Normal file
View File

@@ -0,0 +1,204 @@
/**
* 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]);
}
}
return {
ages,
withProgram,
withoutProgram,
payoffYears,
annualMatch,
totalRetirementWith: allWithProgram[allWithProgram.length - 1],
totalRetirementWithout: allWithoutProgram[allWithoutProgram.length - 1],
retirementDifference: allWithProgram[allWithProgram.length - 1] - allWithoutProgram[allWithoutProgram.length - 1],
// 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
};
}

201
calculations.test.js Normal file
View File

@@ -0,0 +1,201 @@
/**
* Tests for Student Loan Matching Calculator
* Run with: node calculations.test.js
*/
const {
calculatePayoffYears,
calculateRemainingBalance,
calculateRetirementValue,
calculateNetWorthComparison
} = require('./calculations.js');
// Test utilities
let testsPassed = 0;
let testsFailed = 0;
function assert(condition, message) {
if (condition) {
console.log(`${message}`);
testsPassed++;
} else {
console.error(`${message}`);
testsFailed++;
}
}
function assertClose(actual, expected, tolerance = 0.01, message = '') {
const diff = Math.abs(actual - expected);
const pass = diff <= tolerance;
if (pass) {
console.log(`${message} (${actual}${expected})`);
testsPassed++;
} else {
console.error(`${message} (${actual} != ${expected}, diff: ${diff})`);
testsFailed++;
}
}
// Test calculatePayoffYears
console.log('\n=== Testing calculatePayoffYears ===');
assert(
calculatePayoffYears(30000, 500) === 6,
'Should calculate ~6 years for $30k debt at $500/month (5% interest)'
);
assert(
calculatePayoffYears(30000, 0) === 0,
'Should return 0 for zero payment'
);
assert(
calculatePayoffYears(30000, 100) === 50,
'Should return 50 (max) for payment below interest'
);
// Test calculateRemainingBalance
console.log('\n=== Testing calculateRemainingBalance ===');
const balance5Years = calculateRemainingBalance(30000, 500, 60, 84);
assertClose(
calculateRemainingBalance(30000, 500, 60, 6 * 12),
4498,
100,
'After 5 years of $500 payments on $30k, should have ~$4.5k remaining (5% interest)'
);
assert(
calculateRemainingBalance(30000, 500, 0, 84) === 30000,
'At month 0, balance should equal original'
);
assert(
calculateRemainingBalance(30000, 500, 84, 84) === 0,
'At final month, balance should be 0'
);
assert(
calculateRemainingBalance(30000, 500, 100, 84) === 0,
'After loan term, balance should be 0'
);
// Test calculateRetirementValue
console.log('\n=== Testing calculateRetirementValue ===');
const value10Years = calculateRetirementValue(5000, 10, 10);
assertClose(
value10Years,
73918,
100,
'$5k/year for 10 years at 7% should grow to ~$74k'
);
const value30Years = calculateRetirementValue(5000, 10, 30);
assertClose(
value30Years,
286039,
100,
'$5k/year for 10 years, growing for 30 total years should be ~$286k'
);
// Test full calculation scenario
console.log('\n=== Testing Full Scenario ===');
const scenario1 = calculateNetWorthComparison({
salary: 60000,
totalDebt: 30000,
monthlyPayment: 500,
matchRate: 5,
currentAge: 25
});
console.log('\nScenario 1 Results:');
console.log(` Payoff years: ${scenario1.payoffYears}`);
console.log(` Annual match: $${scenario1.annualMatch}`);
console.log(` Retirement with program: $${scenario1.totalRetirementWith.toLocaleString()}`);
console.log(` Retirement without program: $${scenario1.totalRetirementWithout.toLocaleString()}`);
console.log(` Difference: $${scenario1.retirementDifference.toLocaleString()}`);
assert(
scenario1.payoffYears === 6,
'Should take 6 years to pay off $30k at $500/month (5% interest)'
);
assert(
scenario1.annualMatch === 3000,
'Annual match should be $3000 (5% of $60k salary)'
);
assert(
scenario1.retirementDifference > 0,
'With program should result in more retirement savings'
);
// Test edge case: very high payment
const scenario2 = calculateNetWorthComparison({
salary: 100000,
totalDebt: 20000,
monthlyPayment: 2000,
matchRate: 4,
currentAge: 30
});
console.log('\nScenario 2 Results (High Payment):');
console.log(` Payoff years: ${scenario2.payoffYears}`);
console.log(` Annual match: $${scenario2.annualMatch}`);
assert(
scenario2.payoffYears <= 2,
'High payment should result in quick payoff'
);
assert(
scenario2.annualMatch === 4000,
'Annual match should be capped at 4% of salary'
);
// Test the growth rates after payoff
console.log('\n=== Testing Growth Rates After Payoff ===');
const scenario3 = calculateNetWorthComparison({
salary: 70000,
totalDebt: 40000,
monthlyPayment: 600,
matchRate: 5,
currentAge: 28
});
// Find the index right after payoff
const payoffIndex = Math.ceil((scenario3.payoffYears / 2));
if (payoffIndex < scenario3.withProgram.length - 1) {
const withGrowth = scenario3.withProgram[payoffIndex + 1] - scenario3.withProgram[payoffIndex];
const withoutGrowth = scenario3.withoutProgram[payoffIndex + 1] - scenario3.withoutProgram[payoffIndex];
console.log(`\nGrowth rate comparison after payoff:`);
console.log(` With program growth: $${withGrowth}`);
console.log(` Without program growth: $${withoutGrowth}`);
// After payoff, both should be contributing the same amount, so growth should be similar
// The "with" program will have a larger base, but the rate of change should be close
const growthRatio = withoutGrowth > 0 ? withGrowth / withoutGrowth : 0;
assertClose(
growthRatio,
1.5,
1.0,
'Growth rates after payoff should be relatively similar'
);
}
// Summary
console.log('\n=== Test Summary ===');
console.log(`Tests passed: ${testsPassed}`);
console.log(`Tests failed: ${testsFailed}`);
if (testsFailed === 0) {
console.log('\n✅ All tests passed!');
process.exit(0);
} else {
console.log('\n❌ Some tests failed');
process.exit(1);
}

1157
index.html Normal file

File diff suppressed because it is too large Load Diff