"""HTTP surface for the Company-wide Announcement Request flow.""" from __future__ import annotations import uuid from typing import Any from fastapi import APIRouter, Depends, Query, Request from pydantic import BaseModel, Field from sqlalchemy import func as _func from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthUser from app.core.database import get_db from app.core.dependencies import ( get_current_user, ) from app.core.dependencies import ( require_active_employee_for_mutations as _leaver_runtime_gate, ) from app.core.rbac import GLOBAL_ADMIN_ROLE, has_role from app.core.response import success_response from app.domains.announcement_request.models import ( ANNOUNCEMENT_REQ_STATUS_APPROVED, ANNOUNCEMENT_REQ_STATUS_CANCELLED, ANNOUNCEMENT_REQ_STATUS_PENDING, ANNOUNCEMENT_REQ_STATUS_REJECTED, AnnouncementRequest, ) from app.domains.announcement_request.service import ( AnnouncementRequestService, ) from app.identity.person_models import IdentityPerson router = APIRouter( prefix="/api/v1/announcement-requests", tags=["announcement-request"], # Pre-launch hardening 2026-05-04: leaver_pending users cannot # submit / cancel announcement requests. dependencies=[Depends(_leaver_runtime_gate)], ) class AnnouncementRequestSubmitBody(BaseModel): description: str = Field(min_length=1, max_length=4000) def _serialize( row: AnnouncementRequest, persons: dict[str, IdentityPerson], ) -> dict[str, Any]: holder = persons.get(row.person_id) return { "id": row.id, "person_id": row.person_id, "person_label": ( (holder.display_name or holder.work_email) if holder else None ), "description": row.description, "status": row.status, "feishu_instance_code": row.feishu_instance_code, "granted_at": row.granted_at.isoformat() if row.granted_at else None, "created_at": row.created_at.isoformat(), "updated_at": row.updated_at.isoformat(), } async def _hydrate( db: AsyncSession, rows: list[AnnouncementRequest], ) -> list[dict[str, Any]]: if not rows: return [] person_ids = {r.person_id for r in rows} persons: dict[str, IdentityPerson] = {} if person_ids: rows_p = (await db.execute( select(IdentityPerson).where( IdentityPerson.id.in_([uuid.UUID(p) for p in person_ids]), ) )).scalars().all() persons = {str(p.id): p for p in rows_p} return [_serialize(r, persons) for r in rows] @router.post("", status_code=201) async def submit( body: AnnouncementRequestSubmitBody, request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AnnouncementRequestService(db) trace_id = str(getattr(request.state, "trace_id", "") or "") row = await svc.submit( requester=user, description=body.description, trace_id=trace_id, ) await db.commit() [data] = await _hydrate(db, [row]) return success_response(data=data, request=request) @router.get("") async def list_requests( request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), scope: str = Query("auto", pattern="^(auto|mine|all)$"), status: str | None = Query( None, pattern=( f"^({ANNOUNCEMENT_REQ_STATUS_PENDING}|" f"{ANNOUNCEMENT_REQ_STATUS_APPROVED}|" f"{ANNOUNCEMENT_REQ_STATUS_REJECTED}|" f"{ANNOUNCEMENT_REQ_STATUS_CANCELLED})$" ), ), page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), ): is_admin = has_role(user.role_names, GLOBAL_ADMIN_ROLE) if scope == "all" and not is_admin: scope = "mine" if scope == "auto": scope = "all" if is_admin else "mine" q = select(AnnouncementRequest) if scope == "mine": q = q.where(AnnouncementRequest.person_id == str(user.id)) if status: q = q.where(AnnouncementRequest.status == status) count_q = select(_func.count()).select_from(q.subquery()) total = (await db.execute(count_q)).scalar() or 0 q = ( q.order_by(AnnouncementRequest.created_at.desc()) .offset((page - 1) * page_size) .limit(page_size) ) rows = (await db.execute(q)).scalars().all() return success_response( data=await _hydrate(db, list(rows)), request=request, meta={"total": total, "page": page, "page_size": page_size}, ) @router.post("/{req_id}/cancel") async def cancel( req_id: str, request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): svc = AnnouncementRequestService(db) trace_id = str(getattr(request.state, "trace_id", "") or "") row = await svc.cancel(req_id=req_id, actor=user, trace_id=trace_id) await db.commit() [data] = await _hydrate(db, [row]) # False ⇒ Helix cancelled but the parallel Feishu instance is still # pending (e.g. template withdraw disabled) — frontend warns. data["feishu_cancel_synced"] = svc.last_feishu_cancel_synced return success_response(data=data, request=request)