"""Fin-hub reporting allocation of a booked AI-plan fact onto work.""" from __future__ import annotations from datetime import date from decimal import Decimal from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from fin_hub.money import money REPORTING_METHODS = ( "measured_token_share", "measured_estimated_token_share", "session_count_share", "even_split", ) class ReportingAllocationShare(BaseModel): model_config = ConfigDict(extra="forbid") target_key: str = Field(min_length=1, max_length=256) share: Decimal = Field(ge=0, le=1) driver_value: int = Field(ge=0) @field_validator("share", mode="before") @classmethod def validate_share(cls, value): return money(value, non_negative=True) class ReportingAllocation(BaseModel): """Local reporting allocation. Not resource-control AllocationEvidence.""" model_config = ConfigDict(extra="forbid") schema_version: Literal["0.1"] = "0.1" record_type: Literal["reporting_allocation"] = "reporting_allocation" record_id: str = Field(min_length=1, max_length=256) revision_of: str | None = None financial_fact_id: str = Field(min_length=1) method: Literal[ "measured_token_share", "measured_estimated_token_share", "session_count_share", "even_split", ] estimator_version: str = Field(min_length=1, max_length=64) session_token_record_id: str | None = None period_start: date period_end: date currency: str booked_amount: Decimal shares: list[ReportingAllocationShare] residual_share: Decimal = Field(ge=0, le=1) residual_reason: Literal["none", "unmeasured", "unattributed", "no_targets"] source_evidence: list[str] = Field(min_length=1) @field_validator("booked_amount", "residual_share", mode="before") @classmethod def validate_money_fields(cls, value): return money(value, non_negative=True) @model_validator(mode="after") def validate_shares(self) -> "ReportingAllocation": keys = [share.target_key for share in self.shares] if len(set(keys)) != len(keys): raise ValueError("reporting allocation target keys must be unique") total = sum((share.share for share in self.shares), Decimal("0.00")) if total + self.residual_share != Decimal("1.00"): raise ValueError("allocation shares plus residual_share must equal 1") if self.residual_share == Decimal("0.00") and self.residual_reason != "none": raise ValueError("zero residual must use residual_reason=none") if self.residual_share > Decimal("0.00") and self.residual_reason == "none": raise ValueError("nonzero residual requires a residual_reason") return self