Split notfallplan_routes.py (1018 LOC) into clean architecture layers: - compliance/schemas/notfallplan.py (146 LOC): all Pydantic models - compliance/services/notfallplan_service.py (500 LOC): contacts, scenarios, checklists, exercises, stats - compliance/services/notfallplan_workflow_service.py (309 LOC): incidents, templates - compliance/api/notfallplan_routes.py (361 LOC): thin handlers with domain error translation All 250 tests pass. Schemas re-exported via __all__ for legacy test imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
362 lines
11 KiB
Python
362 lines
11 KiB
Python
"""
|
|
FastAPI routes for Notfallplan (Emergency Plan) -- Art. 33/34 DSGVO.
|
|
|
|
Endpoints:
|
|
GET /notfallplan/contacts -- List emergency contacts
|
|
POST /notfallplan/contacts -- Create contact
|
|
PUT /notfallplan/contacts/{id} -- Update contact
|
|
DELETE /notfallplan/contacts/{id} -- Delete contact
|
|
GET /notfallplan/scenarios -- List scenarios
|
|
POST /notfallplan/scenarios -- Create scenario
|
|
PUT /notfallplan/scenarios/{id} -- Update scenario
|
|
DELETE /notfallplan/scenarios/{id} -- Delete scenario
|
|
GET /notfallplan/checklists -- List checklists
|
|
POST /notfallplan/checklists -- Create checklist item
|
|
PUT /notfallplan/checklists/{id} -- Update checklist item
|
|
DELETE /notfallplan/checklists/{id} -- Delete checklist item
|
|
GET /notfallplan/exercises -- List exercises
|
|
POST /notfallplan/exercises -- Create exercise
|
|
GET /notfallplan/stats -- Statistics overview
|
|
GET /notfallplan/incidents -- List incidents
|
|
POST /notfallplan/incidents -- Create incident
|
|
PUT /notfallplan/incidents/{id} -- Update incident
|
|
DELETE /notfallplan/incidents/{id} -- Delete incident
|
|
GET /notfallplan/templates -- List templates
|
|
POST /notfallplan/templates -- Create template
|
|
PUT /notfallplan/templates/{id} -- Update template
|
|
DELETE /notfallplan/templates/{id} -- Delete template
|
|
|
|
Phase 1 Step 4 refactor: handlers delegate to NotfallplanService and
|
|
NotfallplanWorkflowService. Schemas re-exported for legacy test imports.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
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.notfallplan import ( # noqa: F401 -- re-export
|
|
ChecklistCreate,
|
|
ChecklistUpdate,
|
|
ContactCreate,
|
|
ContactUpdate,
|
|
ExerciseCreate,
|
|
IncidentCreate,
|
|
IncidentUpdate,
|
|
ScenarioCreate,
|
|
ScenarioUpdate,
|
|
TemplateCreate,
|
|
TemplateUpdate,
|
|
)
|
|
from compliance.services.notfallplan_service import NotfallplanService
|
|
from compliance.services.notfallplan_workflow_service import (
|
|
NotfallplanWorkflowService,
|
|
)
|
|
|
|
__all__ = [
|
|
"ContactCreate",
|
|
"ContactUpdate",
|
|
"ScenarioCreate",
|
|
"ScenarioUpdate",
|
|
"ChecklistCreate",
|
|
"ChecklistUpdate",
|
|
"ExerciseCreate",
|
|
"IncidentCreate",
|
|
"IncidentUpdate",
|
|
"TemplateCreate",
|
|
"TemplateUpdate",
|
|
"router",
|
|
]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/notfallplan", tags=["notfallplan"])
|
|
|
|
|
|
# ============================================================================
|
|
# Dependencies
|
|
# ============================================================================
|
|
|
|
|
|
def _get_tenant(
|
|
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
|
) -> str:
|
|
return x_tenant_id or "default"
|
|
|
|
|
|
def _get_service(db: Session = Depends(get_db)) -> NotfallplanService:
|
|
return NotfallplanService(db)
|
|
|
|
|
|
def _get_workflow(db: Session = Depends(get_db)) -> NotfallplanWorkflowService:
|
|
return NotfallplanWorkflowService(db)
|
|
|
|
|
|
# ============================================================================
|
|
# Contacts
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/contacts")
|
|
async def list_contacts(
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_contacts(tenant_id)
|
|
|
|
|
|
@router.post("/contacts", status_code=201)
|
|
async def create_contact(
|
|
request: ContactCreate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_contact(tenant_id, request)
|
|
|
|
|
|
@router.put("/contacts/{contact_id}")
|
|
async def update_contact(
|
|
contact_id: str,
|
|
request: ContactUpdate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_contact(tenant_id, contact_id, request)
|
|
|
|
|
|
@router.delete("/contacts/{contact_id}")
|
|
async def delete_contact(
|
|
contact_id: str,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_contact(tenant_id, contact_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Scenarios
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/scenarios")
|
|
async def list_scenarios(
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_scenarios(tenant_id)
|
|
|
|
|
|
@router.post("/scenarios", status_code=201)
|
|
async def create_scenario(
|
|
request: ScenarioCreate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_scenario(tenant_id, request)
|
|
|
|
|
|
@router.put("/scenarios/{scenario_id}")
|
|
async def update_scenario(
|
|
scenario_id: str,
|
|
request: ScenarioUpdate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_scenario(tenant_id, scenario_id, request)
|
|
|
|
|
|
@router.delete("/scenarios/{scenario_id}")
|
|
async def delete_scenario(
|
|
scenario_id: str,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_scenario(tenant_id, scenario_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Checklists
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/checklists")
|
|
async def list_checklists(
|
|
scenario_id: Optional[str] = Query(None),
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_checklists(tenant_id, scenario_id)
|
|
|
|
|
|
@router.post("/checklists", status_code=201)
|
|
async def create_checklist(
|
|
request: ChecklistCreate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_checklist(tenant_id, request)
|
|
|
|
|
|
@router.put("/checklists/{checklist_id}")
|
|
async def update_checklist(
|
|
checklist_id: str,
|
|
request: ChecklistUpdate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.update_checklist(tenant_id, checklist_id, request)
|
|
|
|
|
|
@router.delete("/checklists/{checklist_id}")
|
|
async def delete_checklist(
|
|
checklist_id: str,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.delete_checklist(tenant_id, checklist_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Exercises
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/exercises")
|
|
async def list_exercises(
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.list_exercises(tenant_id)
|
|
|
|
|
|
@router.post("/exercises", status_code=201)
|
|
async def create_exercise(
|
|
request: ExerciseCreate,
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.create_exercise(tenant_id, request)
|
|
|
|
|
|
# ============================================================================
|
|
# Stats
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_stats(
|
|
svc: NotfallplanService = Depends(_get_service),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return svc.get_stats(tenant_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Incidents
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/incidents")
|
|
async def list_incidents(
|
|
status: Optional[str] = None,
|
|
severity: Optional[str] = None,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.list_incidents(tenant_id, status, severity)
|
|
|
|
|
|
@router.post("/incidents", status_code=201)
|
|
async def create_incident(
|
|
request: IncidentCreate,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.create_incident(tenant_id, request)
|
|
|
|
|
|
@router.put("/incidents/{incident_id}")
|
|
async def update_incident(
|
|
incident_id: str,
|
|
request: IncidentUpdate,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.update_incident(tenant_id, incident_id, request)
|
|
|
|
|
|
@router.delete("/incidents/{incident_id}", status_code=204)
|
|
async def delete_incident(
|
|
incident_id: str,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
wf.delete_incident(tenant_id, incident_id)
|
|
|
|
|
|
# ============================================================================
|
|
# Templates
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/templates")
|
|
async def list_templates(
|
|
type: Optional[str] = None,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.list_templates(tenant_id, type)
|
|
|
|
|
|
@router.post("/templates", status_code=201)
|
|
async def create_template(
|
|
request: TemplateCreate,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.create_template(tenant_id, request)
|
|
|
|
|
|
@router.put("/templates/{template_id}")
|
|
async def update_template(
|
|
template_id: str,
|
|
request: TemplateUpdate,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
return wf.update_template(tenant_id, template_id, request)
|
|
|
|
|
|
@router.delete("/templates/{template_id}", status_code=204)
|
|
async def delete_template(
|
|
template_id: str,
|
|
wf: NotfallplanWorkflowService = Depends(_get_workflow),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
with translate_domain_errors():
|
|
wf.delete_template(tenant_id, template_id)
|