288 lines
9.0 KiB
Python
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))
|
|
|