Merge remote-tracking branch 'origin/prod-v7' into integration-all
This commit is contained in:
@@ -4,6 +4,7 @@ from app.routers import (
|
||||
auth,
|
||||
brand_materials,
|
||||
brands,
|
||||
drafts,
|
||||
files,
|
||||
inventory,
|
||||
item_decisions,
|
||||
@@ -51,4 +52,5 @@ ROUTERS = [
|
||||
inventory.router,
|
||||
pm.router,
|
||||
users.router,
|
||||
drafts.router,
|
||||
]
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""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)}
|
||||
Reference in New Issue
Block a user