from decimal import Decimal from typing import Annotated, Literal from pydantic import BaseModel, Field, field_validator from app.domain import REOPEN_TARGET_STAGES, Stage from app.schemas.common import AppModel, UTCDateTime from app.schemas.user import UserRef Money = Annotated[Decimal, Field(ge=0, max_digits=12, decimal_places=2)] # ── Inputs ─────────────────────────────────────────────────────────────────── class NcrCreateIn(BaseModel): job_number: str = Field(min_length=1, max_length=100) department_id: int deviation_category_id: int disposition_authority_id: int deviation_detail: str = Field(min_length=5, max_length=20000) class InitialDispositionIn(BaseModel): qc_authority: str | None = Field(default=None, max_length=255) work_order: str | None = Field(default=None, max_length=100) disposition_notes: str | None = Field(default=None, max_length=100000) secondary_review_needed: bool # "Notify These People" — required when secondary_review_needed is true. secondary_authority_ids: list[int] = [] class SecondaryDispositionIn(BaseModel): qc_authority: str | None = Field(default=None, max_length=255) work_order: str | None = Field(default=None, max_length=100) disposition_notes: str | None = Field(default=None, max_length=100000) # False = save updates and keep in my queue; True = release to Operations. release: bool = False class InspectionIn(BaseModel): qc_approval: Literal["yes", "no"] | None = None inspection_notes: str | None = Field(default=None, max_length=20000) # False = save and revisit later; True = advance to Costing. qc_closed: bool = False class CostingIn(BaseModel): labor_cost: Money material_cost: Money service_cost: Money other_cost: Money class ReopenIn(BaseModel): to_stage: Stage reason: str = Field(min_length=5, max_length=2000) @field_validator("to_stage") @classmethod def _valid_target(cls, v: Stage) -> Stage: if v not in REOPEN_TARGET_STAGES: raise ValueError("Reopen target must be a prior (non-closed) stage.") return v # ── Outputs ────────────────────────────────────────────────────────────────── class AttachmentOut(AppModel): id: int original_filename: str content_type: str size_bytes: int is_image: bool uploaded_at: UTCDateTime uploaded_by: UserRef class TransitionOut(AppModel): id: int from_stage: str | None to_stage: str action: str acted_at: UTCDateTime acted_by: UserRef note: str | None class JobInfoOut(AppModel): part_id: str | None part_description: str | None customer_name: str | None work_order_status: str | None source: str class NcrListItem(BaseModel): id: int ncr_number: str job_number: str department: str deviation_category: str requester: str disposition_authority: str stage: str stage_label: str days_in_stage: int created_at: UTCDateTime class NcrListOut(BaseModel): items: list[NcrListItem] total: int page: int page_size: int class NcrDetailOut(BaseModel): id: int ncr_number: str job_number: str created_at: UTCDateTime stage: str stage_label: str stage_entered_at: UTCDateTime days_in_stage: int department: str department_id: int deviation_category: str deviation_category_id: int deviation_detail: str requester: UserRef disposition_authority: UserRef qc_authority: str | None work_order: str | None disposition_notes: str | None secondary_review_needed: bool | None secondary_authorities: list[UserRef] operations_complete: bool operations_completed_at: UTCDateTime | None operations_completed_by: UserRef | None qc_approval: str | None inspection_notes: str | None qc_closed: bool qc_closed_at: UTCDateTime | None qc_closed_by: UserRef | None labor_cost: Decimal | None material_cost: Decimal | None service_cost: Decimal | None other_cost: Decimal | None total_cost: Decimal | None costing_completed_at: UTCDateTime | None costing_completed_by: UserRef | None closed_at: UTCDateTime | None closed_by: UserRef | None job_info: JobInfoOut | None attachments: list[AttachmentOut] transitions: list[TransitionOut] # Actions the *current* user may take right now (informs the UI; the API # re-enforces every one of these server-side). available_actions: list[str] class NcrMutationOut(BaseModel): ncr: NcrDetailOut warnings: list[str] = [] class AuditEntryOut(AppModel): id: int created_at: UTCDateTime user: UserRef action: str field_name: str | None old_value: str | None new_value: str | None detail: str | None class AuditListOut(BaseModel): items: list[AuditEntryOut] total: int