- Replace module-level _current_model global with contextvars.ContextVar
to prevent concurrent requests from overwriting each other's search
strategy (flash vs pro)
- Add GET /api/tickets/summary returning {total, by_status, by_priority}
aggregated from Gongdan API, registered before parameterized ticket routes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131 lines
4.0 KiB
Python
131 lines
4.0 KiB
Python
"""Ticket API endpoints — proxy to Gongdan system.
|
|
|
|
Returns data in a format aligned with the frontend TicketData interface:
|
|
{ id, title, status, priority, createdAt }
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
|
|
import httpx
|
|
from litestar import get
|
|
from litestar.exceptions import NotFoundException
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _gongdan_headers() -> dict[str, str]:
|
|
return {"X-Api-Key": settings.gongdan_api_key}
|
|
|
|
|
|
def _map_status(raw: str) -> str:
|
|
mapping = {
|
|
"OPEN": "pending",
|
|
"ASSIGNED": "processing",
|
|
"IN_PROGRESS": "processing",
|
|
"PENDING_CUSTOMER": "processing",
|
|
"RESOLVED": "resolved",
|
|
"CLOSED": "resolved",
|
|
}
|
|
return mapping.get(raw, "pending")
|
|
|
|
|
|
def _map_priority(raw: str) -> str:
|
|
mapping = {
|
|
"URGENT": "P0",
|
|
"PRIORITY": "P1",
|
|
"NORMAL": "P2",
|
|
"LOW": "P3",
|
|
}
|
|
return mapping.get(raw, "P2")
|
|
|
|
|
|
def _transform_ticket(t: dict) -> dict:
|
|
"""Transform a Gongdan ticket to the frontend TicketData shape."""
|
|
return {
|
|
"id": t.get("id", ""),
|
|
"ticketNumber": t.get("ticketNumber", ""),
|
|
"title": t.get("description", "")[:120] or "No description",
|
|
"status": _map_status(t.get("status", "")),
|
|
"priority": _map_priority(t.get("priority", "")),
|
|
"createdAt": t.get("createdAt", ""),
|
|
}
|
|
|
|
|
|
@get("/api/tickets/summary")
|
|
async def get_tickets_summary() -> dict:
|
|
"""GET /api/tickets/summary -- Aggregate ticket statistics.
|
|
|
|
Returns:
|
|
{
|
|
"total": int,
|
|
"by_status": {"pending": N, "processing": N, "resolved": N},
|
|
"by_priority": {"P0": N, "P1": N, "P2": N, "P3": N}
|
|
}
|
|
"""
|
|
# Fetch a large page to get representative counts.
|
|
# For a production system this would be a dedicated aggregation API;
|
|
# the Gongdan API only exposes list endpoints so we aggregate client-side.
|
|
url = f"{settings.gongdan_api_base}/api/tickets"
|
|
params = {"page": 1, "pageSize": 100}
|
|
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(url, params=params, headers=_gongdan_headers())
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
tickets = data.get("tickets", [])
|
|
|
|
status_counter: Counter[str] = Counter()
|
|
priority_counter: Counter[str] = Counter()
|
|
|
|
for t in tickets:
|
|
status_counter[_map_status(t.get("status", ""))] += 1
|
|
priority_counter[_map_priority(t.get("priority", ""))] += 1
|
|
|
|
return {
|
|
"total": len(tickets),
|
|
"by_status": {
|
|
"pending": status_counter.get("pending", 0),
|
|
"processing": status_counter.get("processing", 0),
|
|
"resolved": status_counter.get("resolved", 0),
|
|
},
|
|
"by_priority": {
|
|
"P0": priority_counter.get("P0", 0),
|
|
"P1": priority_counter.get("P1", 0),
|
|
"P2": priority_counter.get("P2", 0),
|
|
"P3": priority_counter.get("P3", 0),
|
|
},
|
|
}
|
|
|
|
|
|
@get("/api/tickets")
|
|
async def list_tickets(page: int = 1, page_size: int = 20) -> list[dict]:
|
|
"""GET /api/tickets — List tickets from Gongdan, formatted for frontend."""
|
|
url = f"{settings.gongdan_api_base}/api/tickets"
|
|
params = {"page": page, "pageSize": page_size}
|
|
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(url, params=params, headers=_gongdan_headers())
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
|
|
tickets = data.get("tickets", [])
|
|
return [_transform_ticket(t) for t in tickets]
|
|
|
|
|
|
@get("/api/tickets/{ticket_id:str}")
|
|
async def get_ticket(ticket_id: str) -> dict:
|
|
"""GET /api/tickets/:id — Get a single ticket detail."""
|
|
url = f"{settings.gongdan_api_base}/api/tickets/{ticket_id}"
|
|
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
resp = await client.get(url, headers=_gongdan_headers())
|
|
if resp.status_code == 404:
|
|
raise NotFoundException(detail=f"Ticket {ticket_id} not found")
|
|
resp.raise_for_status()
|
|
t = resp.json()
|
|
|
|
return _transform_ticket(t)
|