← 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,
  };
}