"""ORM rows owned by the policy_notices domain. A single table — :class:`PlatformPolicyNotice` — backing both the employee 制度中心 (searchable policy library) and the header ribbon (a curated subset). Bilingual title + summary + shared keyword tags + Feishu doc URL + two visibility flags (``is_published`` / ``pin_to_banner``) + display order; mutations are global_admin-gated and audited at the service layer. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( JSON, Boolean, DateTime, ForeignKey, Integer, String, ) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.sql import func from app.core.database import Base from app.core.sql_types import GUID as Uuid class PlatformPolicyNotice(Base): """One company policy / manual entry in the 制度中心 (policy library). Two display surfaces, two independent visibility flags: * ``is_published`` — the entry shows in the employee-facing searchable library (``GET /api/v1/policy-notices/library``) for ANY signed-in user. * ``pin_to_banner`` — the entry ALSO rotates in the header ribbon (``GET /api/v1/policy-notices``). The banner is a *subset* of the library; the service layer enforces ``pin_to_banner ⟹ is_published`` so a notice can never rotate in the header while being un-findable in the library. Both lists order by ``display_order`` ASC, ``created_at`` ASC as a stable tiebreak so concurrent writes don't reshuffle the cycle. Content: * ``title_zh`` / ``title_en`` — both NOT NULL; the surfaces render whichever the user's language selects, and a half-translated title is worse than none. * ``summary_zh`` / ``summary_en`` — optional one-paragraph summary shown on the library card (falls back to the title when blank). * ``keywords`` — a shared JSON tag array (language-agnostic). The library search matches a query token against title + summary + keywords, so a zh OR en term surfaces the entry. * ``feishu_url`` — the click target (the full policy doc); required. Admin flow lives behind ``/api/v1/admin/policy-notices/*`` — full CRUD + reorder, gated by ``require_admin`` (global_admin only). Each mutation appends to ``audit_logs`` with the actor and the resource id. """ __tablename__ = "platform_policy_notices" __mapper_args__ = {"eager_defaults": True} id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, default=uuid.uuid4, ) title_zh: Mapped[str] = mapped_column(String(200), nullable=False) title_en: Mapped[str] = mapped_column(String(200), nullable=False) # One-paragraph bilingual summary for the library card. Optional — # NULL renders as "no summary" (card falls back to the title). summary_zh: Mapped[str | None] = mapped_column(String(500), nullable=True) summary_en: Mapped[str | None] = mapped_column(String(500), nullable=True) # Shared, language-agnostic tag list (e.g. ["报销", "expense", "差旅"]). # NULL coerces to [] at the service/schema layer. Searched as plain # text so a zh OR en token finds the entry. keywords: Mapped[list[str] | None] = mapped_column(JSON, nullable=True) # Feishu (or any https) document URL — the click target. Feishu wiki # / doc links can be long, so allow generous room. feishu_url: Mapped[str] = mapped_column(String(1000), nullable=False) # Employee can see / search this entry in the 制度中心 library. is_published: Mapped[bool] = mapped_column( Boolean, nullable=False, default=True, server_default="1", ) # ALSO rotates in the header ribbon. Service enforces # ``pin_to_banner ⟹ is_published`` (banner is a subset of library). pin_to_banner: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, server_default="0", ) # Lower values render first. Admins reorder by up/down on the # management page, which issues a single bulk reorder POST so the # whole set updates atomically (no half-saved order). display_order: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default="0", ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), ) created_by_person_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id"), nullable=True, ) updated_by_person_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id"), nullable=True, )