feat(incidents): add incident lifecycle api and tests

This commit is contained in:
2026-01-03 10:18:21 +00:00
parent ad94833830
commit f427d191e0
10 changed files with 1456 additions and 2 deletions

103
app/api/v1/incidents.py Normal file
View File

@@ -0,0 +1,103 @@
"""Incident API endpoints."""
from datetime import datetime
from uuid import UUID
from fastapi import APIRouter, Depends, Query, status
from app.api.deps import CurrentUser, get_current_user, require_role
from app.schemas.common import PaginatedResponse
from app.schemas.incident import (
CommentRequest,
IncidentEventResponse,
IncidentResponse,
IncidentStatus,
TransitionRequest,
IncidentCreate,
)
from app.services import IncidentService
router = APIRouter(tags=["incidents"])
incident_service = IncidentService()
@router.get("/incidents", response_model=PaginatedResponse[IncidentResponse])
async def list_incidents(
status: IncidentStatus | None = Query(default=None),
cursor: datetime | None = Query(default=None, description="Cursor (created_at)"),
limit: int = Query(default=20, ge=1, le=100),
current_user: CurrentUser = Depends(get_current_user),
) -> PaginatedResponse[IncidentResponse]:
"""List incidents for the active organization."""
return await incident_service.get_incidents(
current_user,
status=status,
cursor=cursor,
limit=limit,
)
@router.post(
"/services/{service_id}/incidents",
response_model=IncidentResponse,
status_code=status.HTTP_201_CREATED,
)
async def create_incident(
service_id: UUID,
payload: IncidentCreate,
current_user: CurrentUser = Depends(require_role("member")),
) -> IncidentResponse:
"""Create a new incident for the given service (member+)."""
return await incident_service.create_incident(current_user, service_id, payload)
@router.get("/incidents/{incident_id}", response_model=IncidentResponse)
async def get_incident(
incident_id: UUID,
current_user: CurrentUser = Depends(get_current_user),
) -> IncidentResponse:
"""Fetch a single incident by ID."""
return await incident_service.get_incident(current_user, incident_id)
@router.get("/incidents/{incident_id}/events", response_model=list[IncidentEventResponse])
async def get_incident_events(
incident_id: UUID,
current_user: CurrentUser = Depends(get_current_user),
) -> list[IncidentEventResponse]:
"""Get the event timeline for an incident."""
return await incident_service.get_incident_events(current_user, incident_id)
@router.post(
"/incidents/{incident_id}/transition",
response_model=IncidentResponse,
)
async def transition_incident(
incident_id: UUID,
payload: TransitionRequest,
current_user: CurrentUser = Depends(require_role("member")),
) -> IncidentResponse:
"""Transition an incident status (member+)."""
return await incident_service.transition_incident(current_user, incident_id, payload)
@router.post(
"/incidents/{incident_id}/comment",
response_model=IncidentEventResponse,
status_code=status.HTTP_201_CREATED,
)
async def add_comment(
incident_id: UUID,
payload: CommentRequest,
current_user: CurrentUser = Depends(require_role("member")),
) -> IncidentEventResponse:
"""Add a comment to the incident timeline (member+)."""
return await incident_service.add_comment(current_user, incident_id, payload)