Files
microdao-daarion/services/city-service/routes_incidents.py

288 lines
9.0 KiB
Python

"""
Incidents API Routes для DAARION City Service
/api/v1/incidents/*
"""
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from typing import Optional, List
import logging
import repo_governance as repo
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/incidents", tags=["incidents"])
# =============================================================================
# Pydantic Models
# =============================================================================
from datetime import datetime as dt
class Incident(BaseModel):
id: str
title: str
description: Optional[str] = None
status: str
priority: str
scope_type: Optional[str] = None
scope_id: Optional[str] = None
escalation_level: Optional[str] = None
reporter_id: Optional[str] = None
reporter_name: Optional[str] = None
assigned_to: Optional[str] = None
assignee_name: Optional[str] = None
created_at: Optional[dt] = None
updated_at: Optional[dt] = None
class CreateIncidentRequest(BaseModel):
title: str = Field(..., min_length=3, max_length=200)
description: str = Field(..., min_length=10, max_length=5000)
priority: str = Field(default="medium", pattern="^(low|medium|high|critical)$")
scope_type: Optional[str] = None
scope_id: Optional[str] = None
reporter_id: str = "dais-demo-user"
class AssignIncidentRequest(BaseModel):
assignee_id: str
actor_id: str = "dais-demo-user"
class EscalateRequest(BaseModel):
actor_id: str = "dais-demo-user"
class ResolveRequest(BaseModel):
resolution: str = Field(..., min_length=10, max_length=2000)
actor_id: str = "dais-demo-user"
class CloseRequest(BaseModel):
actor_id: str = "dais-demo-user"
class CommentRequest(BaseModel):
comment: str = Field(..., min_length=1, max_length=2000)
actor_id: str = "dais-demo-user"
class IncidentHistory(BaseModel):
id: str
action: str
details: Optional[dict] = None
comment: Optional[str] = None
created_at: Optional[dt] = None
actor_name: Optional[str] = None
# =============================================================================
# Routes
# =============================================================================
@router.get("", response_model=List[Incident])
async def get_incidents(
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
status: Optional[str] = None,
priority: Optional[str] = None,
scope_type: Optional[str] = None,
scope_id: Optional[str] = None
):
"""
Отримати список інцидентів з фільтрами
Параметри:
- limit: кількість записів (1-500)
- offset: зміщення для пагінації
- status: фільтр по статусу (open, in_progress, resolved, closed)
- priority: фільтр по пріоритету (low, medium, high, critical)
- scope_type: фільтр по типу scope (microdao, district, city)
- scope_id: фільтр по ID scope
"""
try:
incidents = await repo.get_incidents(
limit=limit,
offset=offset,
status=status,
priority=priority,
scope_type=scope_type,
scope_id=scope_id
)
return incidents
except Exception as e:
logger.error(f"Error fetching incidents: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{incident_id}", response_model=Incident)
async def get_incident(incident_id: str):
"""
Отримати інцидент за ID
"""
try:
incident = await repo.get_incident_by_id(incident_id)
if not incident:
raise HTTPException(status_code=404, detail="Incident not found")
return incident
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("", response_model=Incident)
async def create_incident(request: CreateIncidentRequest):
"""
Створити новий інцидент
Пріоритети:
- low: низький (non-urgent)
- medium: середній (normal handling)
- high: високий (quick response needed)
- critical: критичний (immediate action)
"""
try:
incident = await repo.create_incident(
title=request.title,
description=request.description,
reporter_id=request.reporter_id,
priority=request.priority,
scope_type=request.scope_type,
scope_id=request.scope_id
)
return incident
except Exception as e:
logger.error(f"Error creating incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{incident_id}/assign")
async def assign_incident(incident_id: str, request: AssignIncidentRequest):
"""
Призначити інцидент на агента
Автоматично змінює статус на 'in_progress'
"""
try:
result = await repo.assign_incident(
incident_id=incident_id,
assignee_id=request.assignee_id,
actor_id=request.actor_id
)
if not result:
raise HTTPException(status_code=404, detail="Incident not found")
return {"success": True, "incident": result}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error assigning incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{incident_id}/escalate")
async def escalate_incident(incident_id: str, request: EscalateRequest):
"""
Ескалювати інцидент на вищий рівень
Ескалація:
- microdao → district
- district → city
- city → city (max level)
"""
try:
result = await repo.escalate_incident(
incident_id=incident_id,
actor_id=request.actor_id
)
if not result:
raise HTTPException(status_code=404, detail="Incident not found")
return {"success": True, "incident": result}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error escalating incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{incident_id}/resolve")
async def resolve_incident(incident_id: str, request: ResolveRequest):
"""
Вирішити інцидент
Змінює статус на 'resolved' та записує рішення в історію
"""
try:
result = await repo.resolve_incident(
incident_id=incident_id,
actor_id=request.actor_id,
resolution=request.resolution
)
if not result:
raise HTTPException(status_code=404, detail="Incident not found")
return {"success": True, "incident": result}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error resolving incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{incident_id}/close")
async def close_incident(incident_id: str, request: CloseRequest):
"""
Закрити інцидент
Фінальний статус, після якого інцидент переходить в архів
"""
try:
result = await repo.close_incident(
incident_id=incident_id,
actor_id=request.actor_id
)
if not result:
raise HTTPException(status_code=404, detail="Incident not found")
return {"success": True, "incident": result}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error closing incident: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{incident_id}/comment")
async def add_comment(incident_id: str, request: CommentRequest):
"""
Додати коментар до інциденту
"""
try:
result = await repo.add_incident_comment(
incident_id=incident_id,
actor_id=request.actor_id,
comment=request.comment
)
return {"success": True, "history": result}
except Exception as e:
logger.error(f"Error adding comment: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{incident_id}/history", response_model=List[IncidentHistory])
async def get_incident_history(incident_id: str):
"""
Отримати історію інциденту
Показує всі дії: створення, призначення, ескалації, коментарі, закриття
"""
try:
history = await repo.get_incident_history(incident_id)
return history
except Exception as e:
logger.error(f"Error fetching incident history: {e}")
raise HTTPException(status_code=500, detail=str(e))