← back to StudentLoanTracker
src/lib/calculators/ibr-calculator.ts
85 lines
/**
* IBR Plan Calculator (Income-Based Repayment)
*
* Two variants:
* - New IBR (loans after July 1, 2014): 10% of discretionary income, 20-year forgiveness
* - Old IBR (loans before July 1, 2014): 15% of discretionary income, 25-year forgiveness
*
* Key rules:
* - Discretionary income = AGI - 150% of poverty guideline
* - Payment capped at 10-year Standard payment
* - Must demonstrate partial financial hardship to enroll
* - Interest capitalizes when leaving IBR or losing hardship status
*/
import { LoanInputs, PlanResult, MonthlySnapshot } from './types';
import { getDiscretionaryIncome, getPovertyGuideline, POVERTY_MULTIPLIERS } from './poverty-guidelines';
export function calculateIBR(inputs: LoanInputs, isNewBorrower: boolean = true): PlanResult {
const { totalBalance, weightedRate, agi, familySize, state } = inputs;
const paymentRate = isNewBorrower ? 0.10 : 0.15;
const forgivenessMonths = isNewBorrower ? 240 : 300; // 20 or 25 years
const povertyGuideline = getPovertyGuideline(familySize, state);
const discretionaryIncome = getDiscretionaryIncome(
agi, familySize, state, POVERTY_MULTIPLIERS.IBR
);
const monthlyPayment = Math.max(0, (discretionaryIncome * paymentRate) / 12);
// 10-year Standard payment cap
const monthlyRate = weightedRate / 12;
const standardPayment =
monthlyRate > 0
? (totalBalance * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -120))
: totalBalance / 120;
const cappedPayment = Math.min(monthlyPayment, standardPayment);
// Simulate — IBR DOES capitalize unpaid interest (unlike SAVE)
let balance = totalBalance;
let totalPaid = 0;
let totalInterest = 0;
const schedule: MonthlySnapshot[] = [];
for (let month = 1; month <= forgivenessMonths; month++) {
const interestThisMonth = balance * monthlyRate;
const payment = Math.min(cappedPayment, balance + interestThisMonth);
const principalPaid = payment - interestThisMonth;
totalPaid += payment;
totalInterest += interestThisMonth;
if (principalPaid >= 0) {
balance = Math.max(0, balance - principalPaid);
} else {
// Unpaid interest capitalizes (adds to balance)
balance += Math.abs(principalPaid);
}
schedule.push({
month,
payment: Math.round(payment * 100) / 100,
principal: Math.round(Math.max(0, principalPaid) * 100) / 100,
interest: Math.round(interestThisMonth * 100) / 100,
balance: Math.round(balance * 100) / 100,
});
if (balance <= 0) break;
}
return {
planName: isNewBorrower ? 'IBR (New)' : 'IBR (Old)',
monthlyPayment: Math.round(cappedPayment * 100) / 100,
totalPaid: Math.round(totalPaid * 100) / 100,
totalInterest: Math.round(totalInterest * 100) / 100,
forgivenessAmount: Math.round(Math.max(0, balance) * 100) / 100,
forgivenessMonths,
repaymentMonths: schedule.length,
discretionaryIncome: Math.round(discretionaryIncome * 100) / 100,
povertyGuideline,
paymentSchedule: schedule,
};
}