feat: Kredit-Mathematik (Annuitaet, endfaellig)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:12:25 +02:00
parent d37cb230c4
commit 9f50435df9
3 changed files with 85 additions and 0 deletions

View File

View File

@@ -0,0 +1,56 @@
import calendar
from dataclasses import dataclass
from datetime import date
from decimal import ROUND_HALF_UP, Decimal
CENT = Decimal("0.01")
@dataclass(frozen=True)
class Installment:
due: date
payment: Decimal
interest: Decimal
principal: Decimal
remaining: Decimal
def add_months(d: date, months: int) -> date:
y, m0 = divmod(d.year * 12 + d.month - 1 + months, 12)
m = m0 + 1
return date(y, m, min(d.day, calendar.monthrange(y, m)[1]))
def annuity_payment(principal: Decimal, annual_rate_pct: Decimal, term_months: int) -> Decimal:
i = annual_rate_pct / Decimal(100) / Decimal(12)
if i == 0:
return (principal / term_months).quantize(CENT, ROUND_HALF_UP)
q = (Decimal(1) + i) ** term_months
return (principal * i * q / (q - Decimal(1))).quantize(CENT, ROUND_HALF_UP)
def loan_schedule(principal: Decimal, annual_rate_pct: Decimal, term_months: int,
payout_date: date, repayment_type: str) -> list[Installment]:
i = annual_rate_pct / Decimal(100) / Decimal(12)
plan: list[Installment] = []
remaining = principal
if repayment_type == "bullet":
for n in range(1, term_months + 1):
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
principal_part = remaining if n == term_months else Decimal("0")
remaining = remaining - principal_part
plan.append(Installment(add_months(payout_date, n),
-(interest + principal_part), interest,
principal_part, remaining.quantize(CENT)))
return plan
rate = annuity_payment(principal, annual_rate_pct, term_months)
for n in range(1, term_months + 1):
interest = (remaining * i).quantize(CENT, ROUND_HALF_UP)
principal_part = rate - interest
if n == term_months or principal_part > remaining:
principal_part = remaining
remaining = remaining - principal_part
plan.append(Installment(add_months(payout_date, n),
-(interest + principal_part), interest,
principal_part, remaining.quantize(CENT)))
return plan

View File

@@ -0,0 +1,29 @@
from datetime import date
from decimal import Decimal
from app.engine.loans import add_months, annuity_payment, loan_schedule
def test_add_months_clamps_month_end():
assert add_months(date(2026, 1, 31), 1) == date(2026, 2, 28)
def test_annuity_payment():
# 10.000 EUR, 6 % p.a., 48 Monate -> 234,85 EUR Monatsrate
assert annuity_payment(Decimal("10000"), Decimal("6"), 48) == Decimal("234.85")
def test_annuity_schedule_amortizes_fully():
plan = loan_schedule(Decimal("10000"), Decimal("6"), 48, date(2026, 8, 1), "annuity")
assert len(plan) == 48
assert plan[-1].remaining == Decimal("0.00")
assert plan[0].due == date(2026, 9, 1)
assert plan[0].interest == Decimal("50.00") # 10000 * 0,5 %
assert plan[0].payment < 0
def test_bullet_loan():
plan = loan_schedule(Decimal("10000"), Decimal("6"), 12, date(2026, 8, 1), "bullet")
assert all(i.principal == 0 for i in plan[:-1])
assert plan[-1].principal == Decimal("10000")
assert plan[-1].remaining == Decimal("0.00")