"""Request bodies for the approval-flow registry router.""" from __future__ import annotations import uuid from pydantic import BaseModel, Field from app.domains.approval_registry import constants as const class FlowCreateRequest(BaseModel): approval_code: str = Field(..., min_length=2, max_length=const.MAX_CODE_CHARS) name: str = Field(..., min_length=1, max_length=const.MAX_NAME_CHARS) category: str | None = Field(None, max_length=const.MAX_CATEGORY_CHARS) description: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) status: str = const.STATUS_ACTIVE published: bool = True owner_person_id: uuid.UUID | None = None class FlowUpdateRequest(BaseModel): """Every field optional; only the keys present are written. ``model_dump(exclude_unset=True)`` at the router is what makes "clear the category" (``category: null``) distinguishable from "leave the category alone" (key absent). Ownership is deliberately absent from this body — it moves through its own endpoint so no path changes accountability without leaving an event. """ name: str | None = Field(None, min_length=1, max_length=const.MAX_NAME_CHARS) category: str | None = Field(None, max_length=const.MAX_CATEGORY_CHARS) description: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) status: str | None = None published: bool | None = None class OwnerAssignRequest(BaseModel): #: ``None`` clears the owner — an explicit "we do not know", which #: the coverage read then chases. owner_person_id: uuid.UUID | None = None note: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class RetireRequest(BaseModel): note: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class DisputeRequest(BaseModel): reason: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class OwnerDescriptionRequest(BaseModel): """The owner's own words for what their flow is for. Same column IT writes (``description``) rather than a second owner-only field: the card has room for one sentence about what this flow is for, and two competing sentences would mean choosing which one to show — a choice with no right answer. Empty string clears it. """ description: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class ConfirmRequest(BaseModel): note: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class ImportRow(BaseModel): approval_code: str = Field(..., min_length=2, max_length=const.MAX_CODE_CHARS) name: str = Field(..., min_length=1, max_length=const.MAX_NAME_CHARS) category: str | None = Field(None, max_length=const.MAX_CATEGORY_CHARS) description: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) owner_email: str | None = Field(None, max_length=255) class ImportRequest(BaseModel): rows: list[ImportRow] = Field(..., max_length=const.MAX_IMPORT_ROWS) class SyncRequest(BaseModel): """Manual trigger for the Feishu sweep. ``window_days=None`` sweeps everything visible; Feishu rejects an instance window wider than 30 days, so the value is clamped rather than passed through. """ window_days: int | None = Field(None, ge=1, le=30) discover: bool = True sync_definitions: bool = True infer_owners: bool = True person_limit: int | None = Field(None, ge=1, le=5000) class TransferRequest(BaseModel): """The owner hands a flow to somebody else. ``to_person_id`` is required rather than optional: 「不是我」 without saying who leaves the register no better off than before it asked, and the register cannot guess. """ to_person_id: uuid.UUID note: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class OpenRoundRequest(BaseModel): """Open the confirmation round now, outside the boundary window. ``label`` defaults to the current half-year. An explicit one is accepted so a round missed entirely can still be run under its own name rather than being backdated into this half's numbers. """ label: str | None = Field(None, max_length=32) due_days: int | None = Field(None, ge=1, le=120) class FlowRequestCreate(BaseModel): proposed_name: str = Field(..., min_length=1, max_length=const.MAX_NAME_CHARS) #: Required, and the form says why: IT decides from it and it becomes #: the flow's published description. purpose: str = Field(..., min_length=5, max_length=const.MAX_PURPOSE_CHARS) proposed_category: str | None = Field(None, max_length=const.MAX_CATEGORY_CHARS) #: Defaults to the requester server-side — the person who needed the #: flow is usually the person who knows it. proposed_owner_person_id: uuid.UUID | None = None class FlowRequestDecision(BaseModel): approve: bool note: str | None = Field(None, max_length=const.MAX_DESCRIPTION_CHARS) class FlowRequestRegister(BaseModel): """IT built the flow in the Feishu console and is filling its code in.""" approval_code: str = Field(..., min_length=2, max_length=const.MAX_CODE_CHARS) #: Optional override when the console name ended up different from #: what the employee proposed. name: str | None = Field(None, max_length=const.MAX_NAME_CHARS)