"""AI tool / function introduction request ORM model. Mirrors :class:`AnnouncementRequest` but with the multi-field form the security-review template asks for: AI (text — name of function/product) How to use (multi-select: API / Product / Module-Plugin) Data Level (multi-select: L1 / L2 / L3 / L4) Purpose (textarea) Data type (textarea — what sensitive data is touched) Privacy Policy (textarea — URLs of SOC2 / MSA / DPA / SLA) Attachment (optional file count) Once approved an entry is published company-wide on the Approved AI Tools listing — same idea as the Feishu spreadsheet IT was maintaining manually before. The status set keeps the Feishu language ("已撤回" → cancelled, "已拒绝" → rejected, "已通过" → approved) so historical xlsx rows backfill cleanly. Legacy backfill columns let us import the pre-Helix Feishu records (``legacy_request_no`` + ``source_id``) without rewriting the actor linkage when the original applicant isn't yet a HRIS-synced ``IdentityPerson`` (e.g. on local dev seeded with only a couple of users). New live submissions populate ``person_id`` and leave the legacy columns ``NULL``. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, ForeignKey, String, Text, func from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base AI_TOOL_STATUS_PENDING = "pending" AI_TOOL_STATUS_APPROVED = "approved" AI_TOOL_STATUS_REJECTED = "rejected" AI_TOOL_STATUS_CANCELLED = "cancelled" # Admin-initiated revoke (typically via offboarding force-revoke # at T+0). Distinct from ``cancelled`` (applicant withdrew) so # leaver-driven revocations stay auditable + filterable. AI_TOOL_STATUS_REVOKED = "revoked" AI_TOOL_HOW_TO_USE_OPTIONS = ("API", "Product", "Module/Plugin") AI_TOOL_DATA_LEVEL_OPTIONS = ("L1", "L2", "L3", "L4") class AiToolApplication(Base): __tablename__ = "ai_tool_applications" id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()), ) person_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("identity_persons.id"), nullable=True, ) ai_name: Mapped[str] = mapped_column(String(255), nullable=False) # Comma-separated multi-select values, stored verbatim from the # Feishu widget (e.g. "API, Product, Module/Plugin"). We don't # split into a join table because the option set is small and the # listing page just renders chips from the raw string. how_to_use: Mapped[str] = mapped_column(String(255), nullable=False) data_level: Mapped[str] = mapped_column(String(64), nullable=False) purpose: Mapped[str] = mapped_column(Text, nullable=False) data_type: Mapped[str] = mapped_column(Text, nullable=False) privacy_policy: Mapped[str] = mapped_column(Text, nullable=False) attachment_note: Mapped[str | None] = mapped_column( String(255), nullable=True, ) status: Mapped[str] = mapped_column( String(32), nullable=False, default=AI_TOOL_STATUS_PENDING, ) feishu_instance_code: Mapped[str | None] = mapped_column( String(64), nullable=True, ) approval_request_id: Mapped[str | None] = mapped_column( String(36), ForeignKey("approval_requests.id", ondelete="SET NULL"), nullable=True, ) granted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) # Backfill / Feishu source-of-truth columns. legacy_request_no: Mapped[str | None] = mapped_column( String(32), nullable=True, ) legacy_applicant_name: Mapped[str | None] = mapped_column( String(128), nullable=True, ) legacy_applicant_department: Mapped[str | None] = mapped_column( String(128), nullable=True, ) source_id: Mapped[str | None] = mapped_column( String(255), nullable=True, unique=True, ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False, )