"""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, )