← back to StudentLoanTracker
src/lib/calculators/pslf-calculator.ts
117 lines
/**
* PSLF Calculator (Public Service Loan Forgiveness)
*
* Key rules:
* - 120 qualifying monthly payments (10 years)
* - Must be on qualifying IDR plan OR 10-year Standard
* - Must work full-time for qualifying employer (government, 501(c)(3) nonprofit)
* - Remaining balance forgiven tax-free after 120 payments
* - Can combine with any IDR plan (SAVE, IBR, PAYE, ICR)
*/
import { LoanInputs, PlanResult } from './types';
import { getDiscretionaryIncome, getPovertyGuideline, POVERTY_MULTIPLIERS } from './poverty-guidelines';
const PSLF_QUALIFYING_PAYMENTS = 120;
interface PSLFInputs extends LoanInputs {
qualifyingPayments?: number; // Already-made qualifying payments
selectedPlan?: 'SAVE' | 'IBR' | 'PAYE' | 'ICR' | 'Standard';
}
export interface PSLFResult extends PlanResult {
paymentsRemaining: number;
estimatedForgivenessDate: string; // ISO date string
monthlyPaymentByPlan: Record<string, number>;
}
export function calculatePSLF(inputs: PSLFInputs): PSLFResult {
const {
totalBalance,
weightedRate,
agi,
familySize,
state,
qualifyingPayments = 0,
selectedPlan = 'SAVE',
} = inputs;
const paymentsRemaining = Math.max(0, PSLF_QUALIFYING_PAYMENTS - qualifyingPayments);
// Calculate monthly payment under each IDR plan
const monthlyRate = weightedRate / 12;
const standardPayment =
monthlyRate > 0
? (totalBalance * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -120))
: totalBalance / 120;
const povertyGuideline = getPovertyGuideline(familySize, state);
const planPayments: Record<string, number> = {};
// SAVE: 5% of DI (225% poverty) / 12
const diSAVE = getDiscretionaryIncome(agi, familySize, state, POVERTY_MULTIPLIERS.SAVE);
planPayments.SAVE = Math.round(Math.min((diSAVE * 0.05) / 12, standardPayment) * 100) / 100;
// IBR: 10% of DI (150% poverty) / 12
const diIBR = getDiscretionaryIncome(agi, familySize, state, POVERTY_MULTIPLIERS.IBR);
planPayments.IBR = Math.round(Math.min((diIBR * 0.10) / 12, standardPayment) * 100) / 100;
// PAYE: 10% of DI (150% poverty) / 12
planPayments.PAYE = Math.round(Math.min((diIBR * 0.10) / 12, standardPayment) * 100) / 100;
// ICR: 20% of DI (100% poverty) / 12
const diICR = getDiscretionaryIncome(agi, familySize, state, POVERTY_MULTIPLIERS.ICR);
planPayments.ICR = Math.round(Math.min((diICR * 0.20) / 12, standardPayment) * 100) / 100;
// Standard (10-year)
planPayments.Standard = Math.round(standardPayment * 100) / 100;
const monthlyPayment = planPayments[selectedPlan] || planPayments.SAVE;
// Simulate remaining payments
let balance = totalBalance;
let totalPaid = 0;
let totalInterest = 0;
for (let month = 1; month <= paymentsRemaining; month++) {
const interestThisMonth = balance * monthlyRate;
const payment = Math.min(monthlyPayment, balance + interestThisMonth);
const principalPaid = payment - interestThisMonth;
totalPaid += payment;
totalInterest += interestThisMonth;
// SAVE: no capitalization. Others: capitalize.
if (selectedPlan === 'SAVE') {
balance = Math.max(0, balance - Math.max(0, principalPaid));
} else if (principalPaid >= 0) {
balance = Math.max(0, balance - principalPaid);
} else {
balance += Math.abs(principalPaid);
}
if (balance <= 0) break;
}
// Estimate forgiveness date
const now = new Date();
const forgivenessDate = new Date(now);
forgivenessDate.setMonth(forgivenessDate.getMonth() + paymentsRemaining);
return {
planName: `PSLF (${selectedPlan})`,
monthlyPayment,
totalPaid: Math.round(totalPaid * 100) / 100,
totalInterest: Math.round(totalInterest * 100) / 100,
forgivenessAmount: Math.round(Math.max(0, balance) * 100) / 100,
forgivenessMonths: PSLF_QUALIFYING_PAYMENTS,
repaymentMonths: paymentsRemaining,
discretionaryIncome: Math.round(diSAVE * 100) / 100,
povertyGuideline,
paymentsRemaining,
estimatedForgivenessDate: forgivenessDate.toISOString().split('T')[0],
monthlyPaymentByPlan: planPayments,
};
}