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:
11
.gitlab-ci.yml
Normal file
11
.gitlab-ci.yml
Normal 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
20
AGENTS.md
Normal 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
145
README.md
Normal 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
204
calculations.js
Normal 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
201
calculations.test.js
Normal 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
1157
index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user