initial commit

This commit is contained in:
Matthias
2026-06-15 11:33:23 +02:00
commit fc0a6fb975
155 changed files with 24526 additions and 0 deletions

191
convex/lib/amortization.ts Normal file
View File

@@ -0,0 +1,191 @@
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),
);
}