Merge remote-tracking branch 'origin/prod-v7' into integration-all

This commit is contained in:
MMD
2026-10-01 14:43:02 +03:30
61 changed files with 6420 additions and 26 deletions
+2
View File
@@ -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,
]
+194
View File
@@ -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)}