Files
finanzen/convex/lib/amortization.test.ts
Matthias 4a1cbd105b Add realistic loan amortization with currentBalance back-calculation
- Extend schema with effectiveAnnualRate, totalInterest, totalAmount
- Back-calculate paid months from currentBalance and rebuild schedule
- Allow schedule calculation from termMonths without monthlyPayment
- Handle NaN form values gracefully
- Show effective rate, total interest and total amount in UI
- Add amortization unit tests
2026-06-15 20:02:44 +02:00

136 lines
4.3 KiB
TypeScript

/// <reference types="vite/client" />
import { describe, expect, test } from "vitest";
import { buildSchedule, computePaymentFromTerm, paymentsMade } from "./amortization";
describe("amortization", () => {
test("builds schedule without currentBalance from start", () => {
const result = buildSchedule({
principal: 10000,
annualRate: 3.5,
startDate: new Date("2024-01-15"),
termMonths: 24,
});
expect(result.schedule).toHaveLength(24);
expect(result.payment).toBeGreaterThan(0);
expect(result.totalAmount).toBeGreaterThan(10000);
expect(result.totalInterest).toBeGreaterThan(0);
expect(result.paidMonths).toBe(0);
expect(result.currentBalance).toBe(result.schedule[result.schedule.length - 1].balance);
});
test("computes monthly payment for a 60 month loan", () => {
const payment = computePaymentFromTerm(10000, 3.5, 60);
expect(payment).toBeCloseTo(181.92, 2);
});
test("calculates paid months from currentBalance", () => {
const principal = 10000;
const annualRate = 3.5;
const termMonths = 60;
const monthlyPayment = computePaymentFromTerm(principal, annualRate, termMonths);
const paidMonths = paymentsMade(principal, annualRate, monthlyPayment, 8135.81);
expect(paidMonths).toBe(12);
});
test("reconstructs plan from currentBalance and keeps total deviation small", () => {
const principal = 10000;
const annualRate = 3.5;
const termMonths = 60;
const monthlyPayment = computePaymentFromTerm(principal, annualRate, termMonths);
const result = buildSchedule({
principal,
annualRate,
startDate: new Date("2024-01-15"),
monthlyPayment,
currentBalance: 8135.81,
});
expect(result.paidMonths).toBe(12);
expect(result.schedule[11].balance).toBeCloseTo(8135.81, 2);
expect(result.schedule).toHaveLength(60);
expect(result.totalAmount).toBeCloseTo(principal + result.totalInterest, 0);
expect(result.totalAmount).toBeGreaterThanOrEqual(principal);
});
test("builds schedule from termMonths when monthlyPayment is omitted", () => {
const result = buildSchedule({
principal: 12000,
annualRate: 4.5,
startDate: new Date("2024-01-15"),
termMonths: 48,
});
expect(result.payment).toBeGreaterThan(0);
expect(result.schedule).toHaveLength(48);
expect(result.totalInterest).toBeGreaterThan(0);
});
test("reconstructs plan from termMonths and currentBalance without monthlyPayment", () => {
const principal = 12000;
const annualRate = 4.5;
const termMonths = 48;
const result = buildSchedule({
principal,
annualRate,
startDate: new Date("2024-01-15"),
termMonths,
currentBalance: 7634.12,
});
expect(result.payment).toBeGreaterThan(0);
expect(result.paidMonths).toBeGreaterThan(0);
expect(result.paidMonths).toBeLessThan(48);
expect(result.schedule[result.paidMonths - 1].balance).toBeCloseTo(7634.12, 2);
expect(result.schedule).toHaveLength(48);
});
test("throws when neither monthlyPayment nor termMonths is provided", () => {
expect(() =>
buildSchedule({
principal: 10000,
annualRate: 3.5,
startDate: new Date("2024-01-15"),
}),
).toThrow("Entweder monthlyPayment oder termMonths erforderlich");
});
test("handles zero interest schedule", () => {
const result = buildSchedule({
principal: 12000,
annualRate: 0,
startDate: new Date("2024-01-15"),
monthlyPayment: 200,
});
expect(result.schedule).toHaveLength(60);
expect(result.totalInterest).toBe(0);
expect(result.totalAmount).toBe(12000);
});
test("returns realistic plan for user scenario with currentBalance", () => {
const principal = 1860;
const annualRate = 5.99;
const termMonths = 24;
const monthlyPayment = computePaymentFromTerm(principal, annualRate, termMonths);
const result = buildSchedule({
principal,
annualRate,
startDate: new Date("2024-01-01"),
monthlyPayment,
currentBalance: 775.25,
asOf: new Date("2025-06-01"),
});
expect(result.paidMonths).toBeGreaterThan(0);
expect(result.paidMonths).toBeLessThan(24);
expect(result.schedule[result.paidMonths - 1].balance).toBeCloseTo(775.25, 2);
expect(result.schedule).toHaveLength(24);
});
});