"""Request bodies for the finance-flux editorial + AI endpoints.""" from __future__ import annotations from decimal import Decimal from pydantic import BaseModel, Field, field_validator class LineRef(BaseModel): period: str statement: str line_key: str class CommentaryBody(LineRef): commentary: str | None = None recon_link: str | None = None class AiGenerateBody(LineRef): mode: str = "mom" class AiSummaryBody(BaseModel): period: str statement: str mode: str = "mom" class KpiOverrideBody(BaseModel): period: str kpi_key: str value: Decimal note: str | None = None class CardBody(BaseModel): period: str card_type: str emoji: str | None = None color: str | None = None title: str | None = None body: str | None = None class CardUpdateBody(BaseModel): emoji: str | None = None color: str | None = None title: str | None = None body: str | None = None sort_order: int | None = None class FollowupBody(BaseModel): period: str item: str | None = None owner_label: str | None = None action: str | None = None priority: str = "medium" class FollowupUpdateBody(BaseModel): item: str | None = None owner_label: str | None = None action: str | None = None priority: str | None = None sort_order: int | None = Field(default=None) class ConfigUpdateBody(BaseModel): # Bounded at the edge so a fat-fingered value returns 422 rather than a # MySQL/TiDB out-of-range DataError 500. amount_threshold → Numeric(20,2), # pct_threshold → Numeric(7,2) (max 99999.99); both are non-negative. amount_threshold: Decimal | None = Field(default=None, ge=0, lt=Decimal("1e17")) # Numeric(7,2) max is 99999.99 — use ``le`` (not ``lt 100000``, which lets # 99999.999 through and rounds to 100000.00 → DB overflow 500). pct_threshold: Decimal | None = Field(default=None, ge=0, le=Decimal("99999.99")) # Stored in a CHAR(3) column — reject anything that isn't a 3-letter ISO # currency code at the edge (422) instead of letting MySQL truncate or # 500 on a DataError, and normalise case so downstream formatting is stable. currency: str | None = Field(default=None, min_length=3, max_length=3) consolidation_subsidiary: str | None = None accounting_book: str | None = None comparison_modes: list[str] | None = None closed_periods_only: bool | None = None ai_enabled: bool | None = None ai_prompt_template: str | None = None @field_validator("currency") @classmethod def _upper_currency(cls, v: str | None) -> str | None: return v.upper() if v else v class LineDefUpdateBody(BaseModel): label: str | None = None section: str | None = None sort_order: int | None = None owner_person_id: str | None = None owner_label: str | None = None # Same DB-overflow guards as ConfigUpdateBody (Numeric(20,2) / Numeric(7,2)). threshold_amount: Decimal | None = Field(default=None, ge=0, lt=Decimal("1e17")) threshold_pct: Decimal | None = Field(default=None, ge=0, le=Decimal("99999.99")) is_active: bool | None = None class KpiDefUpdateBody(BaseModel): label: str | None = None source_line_key: str | None = None sort_order: int | None = None higher_is_better: bool | None = None is_active: bool | None = None class AccountMapUpdateBody(BaseModel): line_key: str | None = None sign: int | None = None is_active: bool | None = None