195 lines
6.5 KiB
Python
195 lines
6.5 KiB
Python
"""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)}
|