mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-07-01 19:26:53 +02:00
feat: add itemized claims and payments
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from ccma.domain.models import ContributionData
|
||||
|
||||
CENT = Decimal("0.01")
|
||||
|
||||
CLAIM_STATUS_LABELS = {
|
||||
"open": "OFFEN",
|
||||
"partially_paid": "TEILBEZAHLT",
|
||||
"paid": "BEZAHLT",
|
||||
"overpaid": "ÜBERZAHLT",
|
||||
"overdue": "ÜBERFÄLLIG",
|
||||
"cancelled": "STORNIERT",
|
||||
}
|
||||
|
||||
|
||||
def decimal_value(value: Any, field_name: str = "Betrag") -> Decimal:
|
||||
text = str(value).strip().replace(",", ".")
|
||||
try:
|
||||
return Decimal(text).quantize(CENT, rounding=ROUND_HALF_UP)
|
||||
except (InvalidOperation, ValueError) as exc:
|
||||
raise ValueError(f"{field_name} ist kein gültiger Geldbetrag.") from exc
|
||||
|
||||
|
||||
def money_text(value: Decimal | str) -> str:
|
||||
return f"{decimal_value(value):.2f}"
|
||||
|
||||
|
||||
def claim_items(claim: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
items = claim.get("items")
|
||||
if isinstance(items, list) and items:
|
||||
return items
|
||||
amount = decimal_value(claim.get("amount", "0"))
|
||||
return [
|
||||
{
|
||||
"item_id": "legacy-base",
|
||||
"type": "base",
|
||||
"description": str(claim.get("title") or "Forderung"),
|
||||
"quantity": "1.00",
|
||||
"unit_price": money_text(amount),
|
||||
"amount": money_text(amount),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def materialize_claim_items(claim: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
if not isinstance(claim.get("items"), list) or not claim["items"]:
|
||||
claim["items"] = claim_items(claim)
|
||||
return claim["items"]
|
||||
|
||||
|
||||
def claim_total(claim: dict[str, Any]) -> Decimal:
|
||||
return sum((decimal_value(item.get("amount", "0")) for item in claim_items(claim)), Decimal("0"))
|
||||
|
||||
|
||||
def allocated_total(data: ContributionData, claim_id: str) -> Decimal:
|
||||
return sum(
|
||||
(
|
||||
decimal_value(allocation.get("amount", "0"))
|
||||
for allocation in data.allocations
|
||||
if str(allocation.get("claim_id", "")) == claim_id
|
||||
),
|
||||
Decimal("0"),
|
||||
)
|
||||
|
||||
|
||||
def payment_allocated_total(data: ContributionData, payment_id: str) -> Decimal:
|
||||
return sum(
|
||||
(
|
||||
decimal_value(allocation.get("amount", "0"))
|
||||
for allocation in data.allocations
|
||||
if str(allocation.get("payment_id", "")) == payment_id
|
||||
),
|
||||
Decimal("0"),
|
||||
)
|
||||
|
||||
|
||||
def claim_balance(data: ContributionData, claim: dict[str, Any]) -> Decimal:
|
||||
return (claim_total(claim) - allocated_total(data, str(claim.get("claim_id", "")))).quantize(CENT)
|
||||
|
||||
|
||||
def claim_status(data: ContributionData, claim: dict[str, Any], *, today: date | None = None) -> str:
|
||||
if str(claim.get("status", "")) == "cancelled":
|
||||
return "cancelled"
|
||||
total = claim_total(claim)
|
||||
paid = allocated_total(data, str(claim.get("claim_id", "")))
|
||||
balance = total - paid
|
||||
if balance < 0:
|
||||
return "overpaid"
|
||||
if balance == 0:
|
||||
return "paid"
|
||||
if paid > 0:
|
||||
return "partially_paid"
|
||||
try:
|
||||
due = date.fromisoformat(str(claim.get("due_date", "")))
|
||||
except ValueError:
|
||||
due = None
|
||||
if due and due < (today or date.today()):
|
||||
return "overdue"
|
||||
return "open"
|
||||
@@ -138,6 +138,7 @@ class ContributionData:
|
||||
claims: list[dict[str, Any]] = field(default_factory=list)
|
||||
payments: list[dict[str, Any]] = field(default_factory=list)
|
||||
allocations: list[dict[str, Any]] = field(default_factory=list)
|
||||
reminders: list[dict[str, Any]] = field(default_factory=list)
|
||||
schema_version: int = 1
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -146,6 +147,7 @@ class ContributionData:
|
||||
"claims": self.claims,
|
||||
"payments": self.payments,
|
||||
"allocations": self.allocations,
|
||||
"reminders": self.reminders,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -155,6 +157,7 @@ class ContributionData:
|
||||
claims=list(data.get("claims") or []),
|
||||
payments=list(data.get("payments") or []),
|
||||
allocations=list(data.get("allocations") or []),
|
||||
reminders=list(data.get("reminders") or []),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user