"""Drafts router — exposes bot_drafts to frontend for Wizard pre-population and bi-directional sync.""" from typing import Any from uuid import UUID from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy import func, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.db.engine import get_db from app.models.draft import BotDraft from app.utils.draft_token import verify_draft_token from app.utils.errors import NotFoundError, UnauthorizedError router = APIRouter(prefix="/drafts", tags=["drafts"]) class DraftUpdateIn(BaseModel): payload: dict[str, Any] | None = None status: str | None = None class DraftCommitIn(BaseModel): quote_id: str | None = None def _serialize_draft(draft: BotDraft) -> dict: return { "id": str(draft.id), "chat_id": draft.chat_id, "kind": draft.kind, "status": draft.status, "payload": draft.payload or {}, "missing": draft.missing or [], "source": draft.source, "review_message_id": draft.review_message_id, "last_bale_update_at": draft.last_bale_update_at.isoformat() if draft.last_bale_update_at else None, "created_at": draft.created_at.isoformat() if draft.created_at else None, "updated_at": draft.updated_at.isoformat() if draft.updated_at else None, } def _verify_token_or_raise(token: str) -> dict: token_payload = verify_draft_token(token) if not token_payload or not token_payload.get("draft_id"): raise UnauthorizedError("لینک بازبینی نامعتبر یا منقضی شده است") return token_payload @router.get("/verify") async def verify_and_get_draft(token: str, db: AsyncSession = Depends(get_db)): """Public endpoint for token-based draft review without ERP login.""" token_payload = _verify_token_or_raise(token) draft_id = UUID(token_payload["draft_id"]) stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") return {"data": _serialize_draft(draft), "token_payload": token_payload} @router.patch("/by-token") async def update_draft_by_token(token: str, body: DraftUpdateIn, db: AsyncSession = Depends(get_db)): """Update draft payload using verified token.""" token_payload = _verify_token_or_raise(token) draft_id = UUID(token_payload["draft_id"]) stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") if body.payload is not None: merged = dict(draft.payload or {}) merged.update(body.payload) draft.payload = merged if body.status is not None: draft.status = body.status draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)} @router.post("/by-token/commit") async def commit_draft_by_token(token: str, body: DraftCommitIn, db: AsyncSession = Depends(get_db)): """Commit draft using verified token.""" token_payload = _verify_token_or_raise(token) draft_id = UUID(token_payload["draft_id"]) stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") draft.status = "committed" payload = dict(draft.payload or {}) if body.quote_id: payload["committed_quote_id"] = body.quote_id draft.payload = payload draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)} @router.post("/by-token/cancel") async def cancel_draft_by_token(token: str, db: AsyncSession = Depends(get_db)): """Cancel draft using verified token.""" token_payload = _verify_token_or_raise(token) draft_id = UUID(token_payload["draft_id"]) stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") draft.status = "cancelled" draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)} @router.get("/{draft_id}") async def get_draft(draft_id: UUID, db: AsyncSession = Depends(get_db)): stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") return {"data": _serialize_draft(draft)} @router.patch("/{draft_id}") async def update_draft(draft_id: UUID, body: DraftUpdateIn, db: AsyncSession = Depends(get_db)): stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") if body.payload is not None: merged = dict(draft.payload or {}) merged.update(body.payload) draft.payload = merged if body.status is not None: draft.status = body.status draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)} @router.post("/{draft_id}/commit") async def commit_draft(draft_id: UUID, body: DraftCommitIn, db: AsyncSession = Depends(get_db)): stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") draft.status = "committed" payload = dict(draft.payload or {}) if body.quote_id: payload["committed_quote_id"] = body.quote_id draft.payload = payload draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)} @router.post("/{draft_id}/cancel") async def cancel_draft(draft_id: UUID, db: AsyncSession = Depends(get_db)): stmt = select(BotDraft).where(BotDraft.id == draft_id) draft = (await db.execute(stmt)).scalar_one_or_none() if not draft: raise NotFoundError("پیش‌نویس استعلام یافت نشد") draft.status = "cancelled" draft.updated_at = func.now() await db.commit() await db.refresh(draft) return {"data": _serialize_draft(draft)}