""" 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))