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