"""Pydantic schemas for the platform_tips domain — request bodies + response shapes for the admin CRUD surface and the public read-list.""" from __future__ import annotations import uuid from datetime import datetime from pydantic import BaseModel, ConfigDict, Field class SecurityTipPublic(BaseModel): """Shape returned to ANY signed-in user from ``GET /security-tips``. Only the bilingual text + id is exposed — order is implicit in response order (we sort server-side), enabled rows only, and timestamps / actor IDs stay admin-only. """ id: uuid.UUID text_zh: str text_en: str # Optional (migration 598). NULL = not translated yet; the renderer # falls back ja → en → zh rather than showing a blank. text_ja: str | None = None model_config = ConfigDict(from_attributes=True) class SecurityTipAdmin(SecurityTipPublic): """Admin-side shape returned from ``GET /admin/security-tips`` — adds enable flag, ordering, audit metadata.""" enabled: bool display_order: int created_at: datetime updated_at: datetime created_by_person_id: uuid.UUID | None = None updated_by_person_id: uuid.UUID | None = None class SecurityTipCreate(BaseModel): """Body for ``POST /admin/security-tips``. zh + en required, ja optional.""" text_zh: str = Field(..., min_length=1, max_length=500) text_en: str = Field(..., min_length=1, max_length=500) # Optional on purpose. Requiring it would make every existing admin # workflow fail on save; leaving it blank is an honest "no Japanese # copy" that the renderer handles. text_ja: str | None = Field(default=None, max_length=500) enabled: bool = True class SecurityTipUpdate(BaseModel): """Body for ``PUT /admin/security-tips/{id}``. All fields optional — a partial update only mutates the keys present.""" text_zh: str | None = Field(default=None, min_length=1, max_length=500) text_en: str | None = Field(default=None, min_length=1, max_length=500) text_ja: str | None = Field(default=None, max_length=500) enabled: bool | None = None class SecurityTipReorder(BaseModel): """Body for ``POST /admin/security-tips/reorder``. The ``ordered_ids`` list dictates the new ``display_order`` (0..n-1). Any ids in the DB not present in the list are pushed to the end of the order in their existing relative sequence (graceful handling of races where an admin deleted/added a tip in another tab).""" ordered_ids: list[uuid.UUID] = Field(..., min_length=1)