Harden resource cost evidence contract
This commit is contained in:
parent
080f756fff
commit
00343307fd
22 changed files with 1623 additions and 130 deletions
81
src/fin_hub/money.py
Normal file
81
src/fin_hub/money.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""Shared financial-domain validation and decimal arithmetic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_EVEN
|
||||
|
||||
from fin_hub.attribution import ClientAttribution
|
||||
|
||||
MONEY_QUANTUM = Decimal("0.01")
|
||||
MONEY_ROUNDING = ROUND_HALF_EVEN
|
||||
|
||||
|
||||
def money(value: Decimal | str | int | float, *, non_negative: bool = False) -> Decimal:
|
||||
try:
|
||||
normalized = Decimal(str(value)).quantize(MONEY_QUANTUM, rounding=MONEY_ROUNDING)
|
||||
except (InvalidOperation, ValueError) as exc:
|
||||
raise ValueError("money must be a finite decimal value") from exc
|
||||
if not normalized.is_finite():
|
||||
raise ValueError("money must be a finite decimal value")
|
||||
if non_negative and normalized < 0:
|
||||
raise ValueError("money cannot be negative")
|
||||
return normalized
|
||||
|
||||
|
||||
def money_minor(value: Decimal | str | int | float, *, non_negative: bool = False) -> int:
|
||||
return int(money(value, non_negative=non_negative) / MONEY_QUANTUM)
|
||||
|
||||
|
||||
def minor_money(value: int) -> Decimal:
|
||||
return (Decimal(value) * MONEY_QUANTUM).quantize(MONEY_QUANTUM)
|
||||
|
||||
|
||||
def currency_code(value: str) -> str:
|
||||
normalized = value.strip().upper()
|
||||
if len(normalized) != 3 or not normalized.isascii() or not normalized.isalpha():
|
||||
raise ValueError("currency must be a three-letter ASCII code")
|
||||
return normalized
|
||||
|
||||
|
||||
def reporting_month(value: str) -> str:
|
||||
if len(value) != 7 or value[4] != "-":
|
||||
raise ValueError("period_month must use YYYY-MM")
|
||||
try:
|
||||
date.fromisoformat(f"{value}-01")
|
||||
except ValueError as exc:
|
||||
raise ValueError("period_month must use YYYY-MM") from exc
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EngagementPriceTerms:
|
||||
attribution: ClientAttribution
|
||||
period_month: str
|
||||
amount: Decimal
|
||||
currency: str
|
||||
source: str
|
||||
|
||||
@classmethod
|
||||
def validate(
|
||||
cls,
|
||||
*,
|
||||
client_id: str,
|
||||
application_id: str,
|
||||
app_instance_id: str,
|
||||
period_month: str,
|
||||
amount: Decimal | str | int | float,
|
||||
currency: str,
|
||||
source: str,
|
||||
) -> "EngagementPriceTerms":
|
||||
normalized_source = source.strip()
|
||||
if not normalized_source:
|
||||
raise ValueError("source is required")
|
||||
return cls(
|
||||
attribution=ClientAttribution(client_id, application_id, app_instance_id),
|
||||
period_month=reporting_month(period_month),
|
||||
amount=money(amount, non_negative=True),
|
||||
currency=currency_code(currency),
|
||||
source=normalized_source,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue