Complete CarPass product flows

This commit is contained in:
VPN SaaS Dev
2026-05-14 21:19:37 +09:00
parent a83f55c646
commit c0014ab4ea
28 changed files with 3006 additions and 159 deletions

94
app/services/loans.py Normal file
View 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)