feat: reporting-allocate AI-plan cost across work
Split a booked AI-plan fact across workplans or repos using measured tokens, measured+estimated tokens, session counts, or an even split. Unmeasured remainder stays a residual. The record is not resource-control AllocationEvidence and does not create booked spend.
This commit is contained in:
parent
8916f3c4d0
commit
efd1b31001
10 changed files with 646 additions and 3 deletions
78
src/fin_hub/schemas/reporting_allocation.py
Normal file
78
src/fin_hub/schemas/reporting_allocation.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue