feat(psa): update search endpoint for pagination, add create/status/resource/priority endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 02:55:49 +00:00
parent a5e9615666
commit e2cdfac1c3

View File

@@ -30,6 +30,14 @@ from app.schemas.psa_connection import (
PSABoardResponse,
)
from app.core.config import settings
from app.schemas.psa_tickets import (
PSAResourceSchema,
PSATicketCreatedSchema,
PSATicketStatusUpdateSchema,
TicketCreatePayloadSchema,
PSAPrioritySchema,
)
import app.services.ticket_service as ticket_svc
from app.services.psa.encryption import (
decrypt_credentials,
encrypt_credentials,
@@ -367,7 +375,7 @@ async def list_boards(
return []
@router.get("/tickets/search", response_model=list[PSATicketSearchResult])
@router.get("/tickets/search")
async def search_tickets(
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
@@ -378,17 +386,18 @@ async def search_tickets(
assigned_to_me: bool = False,
unassigned: bool = False,
board_ids: str = "",
priority: str | None = None,
company_id: int | None = None,
page: int = 1,
page_size: int = 10,
page_size: int = 25,
):
"""Search ConnectWise tickets."""
"""Search ConnectWise tickets — returns paginated TicketListResponse."""
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.registry import get_provider_for_account
from app.services.psa.exceptions import PSAError
# Resolve assigned_to_me → member_identifier (CW login name for resources contains filter)
member_identifier: str | None = None
if assigned_to_me:
conn_result = await db.execute(
@@ -407,23 +416,18 @@ async def search_tickets(
)
mapping = mapping_result.scalar_one_or_none()
if not mapping:
# No mapping for this user — return empty list
return []
from app.services.psa.registry import get_provider_for_account as _get_provider
from app.services.psa.exceptions import PSAError as _PSAError
return {"items": [], "total": 0, "page": page, "page_size": page_size}
try:
_provider = await _get_provider(current_user.account_id, db)
_provider = await get_provider_for_account(current_user.account_id, db)
cw_members = await _provider.list_members()
matched = next((m for m in cw_members if m.id == mapping.external_member_id), None)
if matched:
member_identifier = matched.identifier
else:
return []
except _PSAError:
return []
return {"items": [], "total": 0, "page": page, "page_size": page_size}
except PSAError:
return {"items": [], "total": 0, "page": page, "page_size": page_size}
# Parse comma-separated board_ids
parsed_board_ids: list[int] = []
if board_ids:
try:
@@ -433,7 +437,7 @@ async def search_tickets(
try:
provider = await get_provider_for_account(current_user.account_id, db)
tickets = await provider.search_tickets(
result = await provider.search_tickets(
query,
board_id=board_id,
status_id=status_id,
@@ -441,25 +445,134 @@ async def search_tickets(
member_identifier=member_identifier,
unassigned=unassigned,
board_ids=parsed_board_ids,
company_id=company_id,
page=page,
page_size=page_size,
)
return [
items = [
PSATicketSearchResult(
id=t.id,
summary=t.summary,
company_name=t.company_name,
company_id=t.company_id,
board_name=t.board_name,
board_id=t.board_id,
status_name=t.status_name,
status_id=t.status_id,
priority_name=t.priority_name,
priority_id=t.priority_id,
closed=t.closed,
)
for t in tickets
for t in result.items
]
return {"items": items, "total": result.total, "page": result.page, "page_size": result.page_size}
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.post("/tickets", response_model=PSATicketCreatedSchema, status_code=201)
async def create_ticket(
data: TicketCreatePayloadSchema,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Create a new PSA ticket."""
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.exceptions import PSAError
from app.services.psa.types import TicketCreatePayload
try:
return await ticket_svc.create_ticket(
current_user.account_id,
TicketCreatePayload(**data.model_dump()),
db,
)
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.patch("/tickets/{ticket_id}/status", response_model=PSATicketStatusUpdateSchema)
async def update_ticket_status_endpoint(
ticket_id: int,
status_id: int,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Update a ticket's status."""
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.exceptions import PSAError
try:
return await ticket_svc.update_status(current_user.account_id, ticket_id, status_id, db)
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get("/tickets/{ticket_id}/resources", response_model=list[PSAResourceSchema])
async def list_ticket_resources(
ticket_id: int,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.exceptions import PSAError
try:
return await ticket_svc.list_resources(current_user.account_id, ticket_id, db)
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.post("/tickets/{ticket_id}/resources", response_model=PSAResourceSchema, status_code=201)
async def add_ticket_resource(
ticket_id: int,
member_id: int,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.exceptions import PSAError
try:
return await ticket_svc.add_resource(current_user.account_id, ticket_id, member_id, db)
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.delete("/tickets/{ticket_id}/resources/{member_id}", status_code=204)
async def remove_ticket_resource(
ticket_id: int,
member_id: int,
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.exceptions import PSAError
try:
await ticket_svc.remove_resource(current_user.account_id, ticket_id, member_id, db)
except PSAError as e:
raise HTTPException(status_code=502, detail=str(e))
@router.get("/priorities", response_model=list[PSAPrioritySchema])
async def list_priorities(
current_user: Annotated[User, Depends(require_engineer_or_admin)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""List PSA priority levels for ticket creation form."""
if not current_user.account_id:
raise HTTPException(status_code=400, detail="User has no account")
from app.services.psa.registry import get_provider_for_account
from app.services.psa.exceptions import PSAError
try:
provider = await get_provider_for_account(current_user.account_id, db)
raw = await provider.list_priorities()
return [PSAPrioritySchema(id=p["id"], name=p["name"]) for p in raw if p.get("id")]
except PSAError:
return []
@router.get("/tickets/{ticket_id}/context")
async def get_ticket_context(
ticket_id: int,