/// 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); }); });