""" FastAPI routes for Incidents / Datenpannen-Management (DSGVO Art. 33/34). Endpoints: POST /incidents — create incident GET /incidents — list (filter: status, severity, category) GET /incidents/stats — statistics GET /incidents/{id} — detail + measures + deadline_info PUT /incidents/{id} — update DELETE /incidents/{id} — delete PUT /incidents/{id}/status — quick status change POST /incidents/{id}/assess-risk — risk assessment POST /incidents/{id}/notify-authority — Art. 33 authority notification POST /incidents/{id}/notify-subjects — Art. 34 data subject notification POST /incidents/{id}/measures — add measure PUT /incidents/{id}/measures/{mid} — update measure POST /incidents/{id}/measures/{mid}/complete — complete measure POST /incidents/{id}/timeline — add timeline entry POST /incidents/{id}/close — close incident Phase 1 Step 4 refactor: handlers delegate to IncidentService (CRUD/ stats/status/timeline/close) and IncidentWorkflowService (risk/ notifications/measures). Module-level helpers re-exported for legacy tests. """ import logging from typing import Any, Optional from uuid import UUID from fastapi import APIRouter, Depends, Header, Query from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.schemas.incident import ( AuthorityNotificationRequest, CloseIncidentRequest, DataSubjectNotificationRequest, IncidentCreate, IncidentUpdate, MeasureCreate, MeasureUpdate, RiskAssessmentRequest, StatusUpdate, TimelineEntryRequest, ) from compliance.services.incident_service import ( DEFAULT_TENANT_ID, IncidentService, _calculate_72h_deadline, # re-exported for legacy test imports _calculate_risk_level, # re-exported for legacy test imports _incident_to_response, # re-exported for legacy test imports _is_notification_required, # re-exported for legacy test imports _measure_to_response, # re-exported for legacy test imports _parse_jsonb, # re-exported for legacy test imports ) from compliance.services.incident_workflow_service import IncidentWorkflowService logger = logging.getLogger(__name__) router = APIRouter(prefix="/incidents", tags=["incidents"]) def get_incident_service(db: Session = Depends(get_db)) -> IncidentService: return IncidentService(db) def get_workflow_service(db: Session = Depends(get_db)) -> IncidentWorkflowService: return IncidentWorkflowService(db) # ============================================================================= # CRUD Endpoints # ============================================================================= @router.post("") def create_incident( body: IncidentCreate, x_tenant_id: Optional[str] = Header(None), x_user_id: Optional[str] = Header(None), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.create( x_tenant_id or DEFAULT_TENANT_ID, x_user_id or "system", body, ) @router.get("") def list_incidents( x_tenant_id: Optional[str] = Header(None), status: Optional[str] = Query(None), severity: Optional[str] = Query(None), category: Optional[str] = Query(None), limit: int = Query(50), offset: int = Query(0), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.list_incidents( x_tenant_id or DEFAULT_TENANT_ID, status, severity, category, limit, offset, ) @router.get("/stats") def get_stats( x_tenant_id: Optional[str] = Header(None), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.stats(x_tenant_id or DEFAULT_TENANT_ID) @router.get("/{incident_id}") def get_incident( incident_id: UUID, service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.get(str(incident_id)) @router.put("/{incident_id}") def update_incident( incident_id: UUID, body: IncidentUpdate, service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.update(str(incident_id), body) @router.delete("/{incident_id}") def delete_incident( incident_id: UUID, service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.delete(str(incident_id)) # ============================================================================= # Status Update # ============================================================================= @router.put("/{incident_id}/status") def update_status( incident_id: UUID, body: StatusUpdate, x_user_id: Optional[str] = Header(None), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.update_status(str(incident_id), x_user_id or "system", body) # ============================================================================= # Risk Assessment # ============================================================================= @router.post("/{incident_id}/assess-risk") def assess_risk( incident_id: UUID, body: RiskAssessmentRequest, x_user_id: Optional[str] = Header(None), service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.assess_risk(str(incident_id), x_user_id or "system", body) # ============================================================================= # Authority Notification (Art. 33) # ============================================================================= @router.post("/{incident_id}/notify-authority") def notify_authority( incident_id: UUID, body: AuthorityNotificationRequest, x_user_id: Optional[str] = Header(None), service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.notify_authority(str(incident_id), x_user_id or "system", body) # ============================================================================= # Data Subject Notification (Art. 34) # ============================================================================= @router.post("/{incident_id}/notify-subjects") def notify_subjects( incident_id: UUID, body: DataSubjectNotificationRequest, x_user_id: Optional[str] = Header(None), service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.notify_subjects(str(incident_id), x_user_id or "system", body) # ============================================================================= # Measures # ============================================================================= @router.post("/{incident_id}/measures") def add_measure( incident_id: UUID, body: MeasureCreate, x_user_id: Optional[str] = Header(None), service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.add_measure(str(incident_id), x_user_id or "system", body) @router.put("/{incident_id}/measures/{measure_id}") def update_measure( incident_id: UUID, measure_id: UUID, body: MeasureUpdate, service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.update_measure(str(measure_id), body) @router.post("/{incident_id}/measures/{measure_id}/complete") def complete_measure( incident_id: UUID, measure_id: UUID, service: IncidentWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.complete_measure(str(measure_id)) # ============================================================================= # Timeline # ============================================================================= @router.post("/{incident_id}/timeline") def add_timeline_entry( incident_id: UUID, body: TimelineEntryRequest, x_user_id: Optional[str] = Header(None), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.add_timeline(str(incident_id), x_user_id or "system", body) # ============================================================================= # Close Incident # ============================================================================= @router.post("/{incident_id}/close") def close_incident( incident_id: UUID, body: CloseIncidentRequest, x_user_id: Optional[str] = Header(None), service: IncidentService = Depends(get_incident_service), ) -> dict[str, Any]: with translate_domain_errors(): return service.close(str(incident_id), x_user_id or "system", body) # Legacy re-exports __all__ = [ "router", "DEFAULT_TENANT_ID", "_calculate_risk_level", "_is_notification_required", "_calculate_72h_deadline", "_incident_to_response", "_measure_to_response", "_parse_jsonb", ]