192 lines
5.0 KiB
TypeScript
192 lines
5.0 KiB
TypeScript
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),
|
|
);
|
|
}
|