import * as plugins from './calculation.plugins.js'; import { Calculator } from './calculation.classes.calculator.js'; export interface IAmortizationPayment { period: number; payment: plugins.Decimal; principal: plugins.Decimal; interest: plugins.Decimal; balance: plugins.Decimal; cumulativePrincipal: plugins.Decimal; cumulativeInterest: plugins.Decimal; } export interface IAmortizationSchedule { payments: IAmortizationPayment[]; totalPayment: plugins.Decimal; totalPrincipal: plugins.Decimal; totalInterest: plugins.Decimal; monthlyPayment: plugins.Decimal; } export interface ILoanOptions { principal: plugins.Decimal.Value; annualRate: plugins.Decimal.Value; termYears?: plugins.Decimal.Value; termMonths?: plugins.Decimal.Value; paymentFrequency?: 'monthly' | 'biweekly' | 'weekly'; extraPayment?: plugins.Decimal.Value; } /** * Amortization calculations class for loan schedules and payment analysis */ export class Amortization extends Calculator { constructor() { super({ precision: 15 }); } /** * Generate a complete amortization schedule * @param options Loan options */ public schedule(options: ILoanOptions): IAmortizationSchedule { const principal = this.decimal(options.principal); const annualRate = this.decimal(options.annualRate); const termMonths = this.calculateTermMonths(options); const extraPayment = options.extraPayment ? this.decimal(options.extraPayment) : this.decimal(0); const monthlyRate = this.divide(annualRate, 12); const monthlyPayment = this.calculatePayment(principal, monthlyRate, termMonths); const payments: IAmortizationPayment[] = []; let balance = principal; let cumulativePrincipal = this.decimal(0); let cumulativeInterest = this.decimal(0); for (let period = 1; period <= termMonths.toNumber() && balance.greaterThan(0); period++) { const interestPayment = this.multiply(balance, monthlyRate); const scheduledPrincipal = this.subtract(monthlyPayment, interestPayment); // Apply extra payment to principal const totalPrincipal = this.add(scheduledPrincipal, extraPayment); // Ensure we don't pay more than the remaining balance const actualPrincipal = this.min(totalPrincipal, balance); const actualPayment = this.add(interestPayment, actualPrincipal); balance = this.subtract(balance, actualPrincipal); cumulativePrincipal = this.add(cumulativePrincipal, actualPrincipal); cumulativeInterest = this.add(cumulativeInterest, interestPayment); payments.push({ period, payment: actualPayment, principal: actualPrincipal, interest: interestPayment, balance: balance, cumulativePrincipal: cumulativePrincipal, cumulativeInterest: cumulativeInterest }); // If balance is paid off early, stop if (balance.isZero()) { break; } } const totalPayment = payments.reduce((sum, p) => this.add(sum, p.payment), this.decimal(0)); const totalPrincipal = cumulativePrincipal; const totalInterest = cumulativeInterest; return { payments, totalPayment, totalPrincipal, totalInterest, monthlyPayment }; } /** * Calculate monthly payment for a loan */ private calculatePayment( principal: plugins.Decimal, monthlyRate: plugins.Decimal, termMonths: plugins.Decimal ): plugins.Decimal { if (monthlyRate.isZero()) { return this.divide(principal, termMonths); } const numerator = this.multiply(principal, monthlyRate); const denominator = this.subtract( 1, this.power(this.add(1, monthlyRate), this.multiply(-1, termMonths)) ); return this.divide(numerator, denominator); } /** * Calculate total term in months */ private calculateTermMonths(options: ILoanOptions): plugins.Decimal { if (options.termMonths) { return this.decimal(options.termMonths); } else if (options.termYears) { return this.multiply(this.decimal(options.termYears), 12); } else { throw new Error('Either termYears or termMonths must be specified'); } } /** * Calculate remaining balance after a specific number of payments * @param principal Original loan amount * @param annualRate Annual interest rate * @param termMonths Total term in months * @param paymentsMade Number of payments made */ public remainingBalance( principal: plugins.Decimal.Value, annualRate: plugins.Decimal.Value, termMonths: plugins.Decimal.Value, paymentsMade: plugins.Decimal.Value ): plugins.Decimal { const p = this.decimal(principal); const r = this.divide(this.decimal(annualRate), 12); const n = this.decimal(termMonths); const m = this.decimal(paymentsMade); if (r.isZero()) { const paymentAmount = this.divide(p, n); const paidAmount = this.multiply(paymentAmount, m); return this.subtract(p, paidAmount); } const onePlusR = this.add(1, r); const factor1 = this.power(onePlusR, n); const factor2 = this.power(onePlusR, m); const numerator = this.multiply(p, this.subtract(factor1, factor2)); const denominator = this.subtract(factor1, 1); return this.divide(numerator, denominator); } /** * Calculate interest paid over a specific period * @param schedule The amortization schedule * @param startPeriod Start period (inclusive) * @param endPeriod End period (inclusive) */ public interestForPeriod( schedule: IAmortizationSchedule, startPeriod: number, endPeriod: number ): plugins.Decimal { return schedule.payments .filter(p => p.period >= startPeriod && p.period <= endPeriod) .reduce((sum, p) => this.add(sum, p.interest), this.decimal(0)); } /** * Calculate principal paid over a specific period * @param schedule The amortization schedule * @param startPeriod Start period (inclusive) * @param endPeriod End period (inclusive) */ public principalForPeriod( schedule: IAmortizationSchedule, startPeriod: number, endPeriod: number ): plugins.Decimal { return schedule.payments .filter(p => p.period >= startPeriod && p.period <= endPeriod) .reduce((sum, p) => this.add(sum, p.principal), this.decimal(0)); } /** * Calculate the payoff date with extra payments * @param options Loan options with extra payment */ public payoffDate(options: ILoanOptions, startDate: Date = new Date()): Date { const schedule = this.schedule(options); const lastPayment = schedule.payments[schedule.payments.length - 1]; const monthsToPayoff = lastPayment.period; const payoffDate = new Date(startDate); payoffDate.setMonth(payoffDate.getMonth() + monthsToPayoff); return payoffDate; } /** * Calculate interest savings with extra payments * @param regularOptions Loan options without extra payment * @param extraPaymentOptions Loan options with extra payment */ public interestSavings( regularOptions: ILoanOptions, extraPaymentOptions: ILoanOptions ): plugins.Decimal { const regularSchedule = this.schedule(regularOptions); const extraSchedule = this.schedule(extraPaymentOptions); return this.subtract(regularSchedule.totalInterest, extraSchedule.totalInterest); } /** * Calculate biweekly payment amount (26 payments per year) * @param monthlyPayment The regular monthly payment */ public biweeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal { return this.divide(this.decimal(monthlyPayment), 2); } /** * Calculate weekly payment amount (52 payments per year) * @param monthlyPayment The regular monthly payment */ public weeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal { const monthly = this.decimal(monthlyPayment); const yearlyPayment = this.multiply(monthly, 12); return this.divide(yearlyPayment, 52); } /** * Calculate the maximum loan amount based on payment capacity * @param maxPayment Maximum monthly payment * @param annualRate Annual interest rate * @param termMonths Term in months */ public maxLoanAmount( maxPayment: plugins.Decimal.Value, annualRate: plugins.Decimal.Value, termMonths: plugins.Decimal.Value ): plugins.Decimal { const payment = this.decimal(maxPayment); const r = this.divide(this.decimal(annualRate), 12); const n = this.decimal(termMonths); if (r.isZero()) { return this.multiply(payment, n); } const onePlusR = this.add(1, r); const factor = this.power(onePlusR, this.multiply(-1, n)); const numerator = this.subtract(1, factor); const denominator = this.divide(numerator, r); return this.multiply(payment, denominator); } /** * Calculate loan-to-value ratio (LTV) * @param loanAmount The loan amount * @param propertyValue The property value */ public ltv( loanAmount: plugins.Decimal.Value, propertyValue: plugins.Decimal.Value ): plugins.Decimal { const loan = this.decimal(loanAmount); const value = this.decimal(propertyValue); if (value.isZero()) { throw new Error('Property value cannot be zero'); } return this.divide(loan, value); } /** * Calculate debt-to-income ratio (DTI) * @param monthlyDebtPayments Total monthly debt payments * @param monthlyIncome Monthly gross income */ public dti( monthlyDebtPayments: plugins.Decimal.Value, monthlyIncome: plugins.Decimal.Value ): plugins.Decimal { const debt = this.decimal(monthlyDebtPayments); const income = this.decimal(monthlyIncome); if (income.isZero()) { throw new Error('Monthly income cannot be zero'); } return this.divide(debt, income); } }