Complete CarPass product flows
This commit is contained in:
94
app/services/loans.py
Normal file
94
app/services/loans.py
Normal file
@@ -0,0 +1,94 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user