export type AmortRow = { month: number; date: string; payment: number; interest: number; principal: number; balance: number; }; export type ScheduleResult = { schedule: AmortRow[]; payment: number; termMonths: number; totalInterest: number; payoffDate: Date; }; function round2(n: number): number { return Math.round(n * 100) / 100; } function addMonths(date: Date, months: number): Date { const d = new Date(date); d.setMonth(d.getMonth() + months); return d; } function formatDate(d: Date): string { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } export function computeTermFromPayment( principal: number, annualRate: number, monthlyPayment: number, ): number { const i = annualRate / 100 / 12; if (i <= 0) { return Math.ceil(principal / monthlyPayment); } const n = -Math.log(1 - (i * principal) / monthlyPayment) / Math.log(1 + i); return Math.ceil(n); } export function computePaymentFromTerm( principal: number, annualRate: number, termMonths: number, ): number { const i = annualRate / 100 / 12; if (i <= 0) { return round2(principal / termMonths); } const payment = (principal * i) / (1 - Math.pow(1 + i, -termMonths)); return round2(payment); } export function buildSchedule(input: { principal: number; annualRate: number; startDate: Date; monthlyPayment?: number; termMonths?: number; }): ScheduleResult { const P = input.principal; const i = input.annualRate / 100 / 12; let payment = input.monthlyPayment; let termMonths = input.termMonths; if (payment !== undefined && termMonths === undefined) { termMonths = computeTermFromPayment(P, input.annualRate, payment); } else if (termMonths !== undefined && payment === undefined) { payment = computePaymentFromTerm(P, input.annualRate, termMonths); } else if (payment === undefined && termMonths === undefined) { throw new Error("Entweder monthlyPayment oder termMonths erforderlich"); } payment = round2(payment!); termMonths = termMonths!; const schedule: AmortRow[] = []; let balance = P; for (let month = 1; month <= termMonths; month++) { const interest = round2(i > 0 ? balance * i : 0); let principalPart = round2(payment - interest); if (month === termMonths || principalPart > balance) { principalPart = round2(balance); payment = round2(interest + principalPart); } balance = round2(balance - principalPart); const date = formatDate(addMonths(input.startDate, month - 1)); schedule.push({ month, date, payment, interest, principal: principalPart, balance: Math.max(0, balance), }); if (balance <= 0) break; } const actualTerm = schedule.length; const totalPaid = schedule.reduce((sum, row) => sum + row.payment, 0); const payoffDate = addMonths(input.startDate, actualTerm); return { schedule, payment, termMonths: actualTerm, totalInterest: round2(totalPaid - P), payoffDate, }; } export function currentBalanceFromSchedule( schedule: AmortRow[], startDate: Date, asOf: Date = new Date(), ): number { if (schedule.length === 0) return 0; const monthsElapsed = Math.max( 0, (asOf.getFullYear() - startDate.getFullYear()) * 12 + (asOf.getMonth() - startDate.getMonth()), ); if (monthsElapsed >= schedule.length) return 0; if (monthsElapsed <= 0) return schedule[0].balance + schedule[0].principal; return schedule[monthsElapsed - 1]?.balance ?? 0; } export function remainingMonthsFromSchedule( schedule: AmortRow[], startDate: Date, asOf: Date = new Date(), ): number { const balance = currentBalanceFromSchedule(schedule, startDate, asOf); if (balance <= 0) return 0; const monthsElapsed = Math.max( 0, (asOf.getFullYear() - startDate.getFullYear()) * 12 + (asOf.getMonth() - startDate.getMonth()), ); return Math.max(0, schedule.length - monthsElapsed); } export function totalMonthlyPaymentActiveLoans( loans: Array<{ status: string; monthlyPayment?: number }>, ): number { return round2( loans .filter((l) => l.status === "aktiv") .reduce((sum, l) => sum + (l.monthlyPayment ?? 0), 0), ); } export function totalRemainingDebt( loans: Array<{ status: string; principal: number; annualInterestRate: number; startDate: string; monthlyPayment?: number; termMonths?: number; currentBalance?: number; }>, ): number { return round2( loans .filter((l) => l.status === "aktiv") .reduce((sum, loan) => { if (loan.currentBalance !== undefined) { return sum + loan.currentBalance; } const start = new Date(loan.startDate); const schedule = buildSchedule({ principal: loan.principal, annualRate: loan.annualInterestRate, startDate: start, monthlyPayment: loan.monthlyPayment, termMonths: loan.termMonths, }); return sum + currentBalanceFromSchedule(schedule.schedule, start); }, 0), ); }