95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
from decimal import ROUND_HALF_UP, Decimal
|
|
|
|
MONEY = Decimal("0.01")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LoanPayment:
|
|
number: int
|
|
payment_date: date | None
|
|
payment: Decimal
|
|
principal: Decimal
|
|
interest: Decimal
|
|
remaining_principal: Decimal
|
|
|
|
|
|
def quantize_money(value: Decimal) -> Decimal:
|
|
return value.quantize(MONEY, rounding=ROUND_HALF_UP)
|
|
|
|
|
|
def annuity_payment(principal: Decimal, months: int, annual_rate: Decimal) -> Decimal:
|
|
if principal <= 0:
|
|
raise ValueError("principal must be positive")
|
|
if months <= 0:
|
|
raise ValueError("months must be positive")
|
|
if annual_rate < 0:
|
|
raise ValueError("annual_rate must be non-negative")
|
|
if annual_rate == 0:
|
|
return quantize_money(principal / Decimal(months))
|
|
monthly_rate = annual_rate / Decimal("12") / Decimal("100")
|
|
factor = (Decimal("1") + monthly_rate) ** months
|
|
payment = principal * monthly_rate * factor / (factor - Decimal("1"))
|
|
return quantize_money(payment)
|
|
|
|
|
|
def loan_summary(principal: Decimal, months: int, annual_rate: Decimal) -> dict:
|
|
payment = annuity_payment(principal, months, annual_rate)
|
|
total_payment = quantize_money(payment * Decimal(months))
|
|
total_interest = max(total_payment - principal, Decimal("0")).quantize(MONEY)
|
|
return {
|
|
"monthly_payment": payment,
|
|
"total_payment": total_payment,
|
|
"overpayment": total_interest,
|
|
"total_interest": total_interest,
|
|
"principal": principal,
|
|
"months": months,
|
|
"annual_rate": annual_rate,
|
|
}
|
|
|
|
|
|
def generate_annuity_schedule(
|
|
*,
|
|
principal: Decimal,
|
|
months: int,
|
|
annual_rate: Decimal,
|
|
first_payment_date: date | None = None,
|
|
) -> list[LoanPayment]:
|
|
payment = annuity_payment(principal, months, annual_rate)
|
|
monthly_rate = annual_rate / Decimal("12") / Decimal("100")
|
|
remaining = principal
|
|
rows: list[LoanPayment] = []
|
|
for number in range(1, months + 1):
|
|
interest = quantize_money(remaining * monthly_rate) if annual_rate else Decimal("0.00")
|
|
principal_part = payment - interest
|
|
if number == months or principal_part > remaining:
|
|
principal_part = remaining
|
|
payment_for_row = principal_part + interest
|
|
else:
|
|
payment_for_row = payment
|
|
remaining = max(remaining - principal_part, Decimal("0"))
|
|
rows.append(
|
|
LoanPayment(
|
|
number=number,
|
|
payment_date=None if first_payment_date is None else add_months(first_payment_date, number - 1),
|
|
payment=quantize_money(payment_for_row),
|
|
principal=quantize_money(principal_part),
|
|
interest=quantize_money(interest),
|
|
remaining_principal=quantize_money(remaining),
|
|
)
|
|
)
|
|
return rows
|
|
|
|
|
|
def add_months(value: date, months: int) -> date:
|
|
import calendar
|
|
|
|
month = value.month - 1 + months
|
|
year = value.year + month // 12
|
|
month = month % 12 + 1
|
|
day = min(value.day, calendar.monthrange(year, month)[1])
|
|
return date(year, month, day)
|