From c76bd54f1a05b157edf8c106334c614e6f41032a Mon Sep 17 00:00:00 2001 From: chihlasm Date: Mon, 16 Mar 2026 01:05:11 -0400 Subject: [PATCH] feat: add GET /integrations/psa/tickets/{id}/context endpoint (Task 9) Returns rich TicketContext for a ticket ID. Handles PSA auth failures (returns structured error), ticket-not-found (404), and general PSA errors (502). Requires active PSA connection for the user's account. Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/endpoints/integrations.py | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/backend/app/api/endpoints/integrations.py b/backend/app/api/endpoints/integrations.py index f4a5cd7c..ba78b93c 100644 --- a/backend/app/api/endpoints/integrations.py +++ b/backend/app/api/endpoints/integrations.py @@ -319,6 +319,61 @@ async def search_tickets( raise HTTPException(status_code=502, detail=str(e)) +@router.get("/tickets/{ticket_id}/context") +async def get_ticket_context( + ticket_id: int, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], +): + """Get rich ticket context (company, contact, configs, notes, related tickets) for AI prompt injection.""" + from app.services.psa.registry import get_provider_for_account + from app.services.psa.exceptions import ( + PSAError, + PSAAuthError, + PSAPermissionError, + PSANotFoundError, + PSAConnectionError, + ) + from app.schemas.psa_context import TicketContext + + if not current_user.account_id: + raise HTTPException(status_code=400, detail="User has no account") + + # Look up the active connection for connection_id + conn_result = await db.execute( + select(PsaConnection).where( + PsaConnection.account_id == current_user.account_id, + PsaConnection.is_active.is_(True), + ) + ) + connection = conn_result.scalar_one_or_none() + if not connection: + raise HTTPException(status_code=404, detail="No active PSA connection configured") + + try: + provider = await get_provider_for_account(current_user.account_id, db) + except PSAConnectionError: + raise HTTPException(status_code=404, detail="No active PSA connection configured") + except PSAError as e: + raise HTTPException(status_code=502, detail=str(e)) + + try: + ctx: TicketContext = await provider.get_ticket_context( + ticket_id=ticket_id, + connection_id=str(connection.id), + ) + return ctx + except (PSAAuthError, PSAPermissionError): + raise HTTPException( + status_code=502, + detail={"error": "psa_auth_failed", "message": "PSA credentials may have expired."}, + ) + except PSANotFoundError: + raise HTTPException(status_code=404, detail="Ticket not found") + except PSAError as e: + raise HTTPException(status_code=502, detail=str(e)) + + @router.get("/tickets/{ticket_id}") async def get_ticket( ticket_id: str,