""" FastAPI routes for DSFA — Datenschutz-Folgenabschaetzung (Art. 35 DSGVO). Endpoints: GET /v1/dsfa — Liste (tenant_id + status-filter + skip/limit) POST /v1/dsfa — Neu erstellen -> 201 GET /v1/dsfa/stats — Zaehler nach Status GET /v1/dsfa/audit-log — Audit-Log GET /v1/dsfa/export/csv — CSV-Export aller DSFAs POST /v1/dsfa/from-assessment/{id} — Stub: DSFA aus UCCA-Assessment GET /v1/dsfa/by-assessment/{id} — Stub: DSFA nach Assessment-ID GET /v1/dsfa/{id} — Detail PUT /v1/dsfa/{id} — Update DELETE /v1/dsfa/{id} — Loeschen (Art. 17 DSGVO) PATCH /v1/dsfa/{id}/status — Schnell-Statuswechsel PUT /v1/dsfa/{id}/sections/{nr} — Section-Update (1-8) POST /v1/dsfa/{id}/submit-for-review — Workflow: Einreichen POST /v1/dsfa/{id}/approve — Workflow: Genehmigen/Ablehnen GET /v1/dsfa/{id}/export — JSON-Export einer DSFA Phase 1 Step 4 refactor: handlers delegate to DSFAService (CRUD/stats/ audit/csv) and DSFAWorkflowService (status/section/submit/approve/export/ versions). Module-level helpers re-exported for legacy tests. """ import logging from typing import Any, Optional from fastapi import APIRouter, Depends, Query from fastapi.responses import Response from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.schemas.dsfa import ( DSFAApproveRequest, DSFACreate, DSFASectionUpdate, DSFAStatusUpdate, DSFAUpdate, ) from compliance.services.dsfa_service import ( DEFAULT_TENANT_ID, VALID_RISK_LEVELS, VALID_STATUSES, DSFAService, _dsfa_to_response, # re-exported for legacy test imports _get_tenant_id, # re-exported for legacy test imports ) from compliance.services.dsfa_workflow_service import ( SECTION_FIELD_MAP, # noqa: F401 — re-export DSFAWorkflowService, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/dsfa", tags=["compliance-dsfa"]) def get_dsfa_service(db: Session = Depends(get_db)) -> DSFAService: return DSFAService(db) def get_workflow_service( db: Session = Depends(get_db), ) -> DSFAWorkflowService: return DSFAWorkflowService(db) # ============================================================================= # Stats (must be before /{id} to avoid route conflict) # ============================================================================= @router.get("/stats") async def get_stats( tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """Zaehler nach Status und Risiko-Level.""" with translate_domain_errors(): return service.stats(tenant_id) # ============================================================================= # Audit Log (must be before /{id} to avoid route conflict) # ============================================================================= @router.get("/audit-log") async def get_audit_log( tenant_id: Optional[str] = Query(None), limit: int = Query(50, ge=1, le=500), offset: int = Query(0, ge=0), service: DSFAService = Depends(get_dsfa_service), ) -> list[dict[str, Any]]: """DSFA Audit-Trail.""" with translate_domain_errors(): return service.audit_log(tenant_id, limit, offset) # ============================================================================= # CSV Export (must be before /{id} to avoid route conflict) # ============================================================================= @router.get("/export/csv", name="export_dsfas_csv") async def export_dsfas_csv( tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> Response: """Export all DSFAs as CSV.""" with translate_domain_errors(): csv_content = service.export_csv(tenant_id) return Response( content=csv_content, media_type="text/csv", headers={ "Content-Disposition": "attachment; filename=dsfas_export.csv" }, ) # ============================================================================= # UCCA Integration Stubs (must be before /{id} to avoid route conflict) # ============================================================================= @router.post("/from-assessment/{assessment_id}", status_code=501) async def create_from_assessment( assessment_id: str, ) -> dict[str, str]: """Stub: Create DSFA from UCCA assessment.""" return { "detail": ( "Not implemented — requires cross-service " "integration with ai-compliance-sdk" ) } @router.get("/by-assessment/{assessment_id}", status_code=501) async def get_by_assessment( assessment_id: str, ) -> dict[str, str]: """Stub: Get DSFA by linked UCCA assessment ID.""" return { "detail": ( "Not implemented — requires cross-service " "integration with ai-compliance-sdk" ) } # ============================================================================= # List + Create # ============================================================================= @router.get("") async def list_dsfas( tenant_id: Optional[str] = Query(None), status: Optional[str] = Query(None), risk_level: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), service: DSFAService = Depends(get_dsfa_service), ) -> list[dict[str, Any]]: """Liste aller DSFAs fuer einen Tenant.""" with translate_domain_errors(): return service.list_dsfas(tenant_id, status, risk_level, skip, limit) @router.post("", status_code=201) async def create_dsfa( request: DSFACreate, tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """Neue DSFA erstellen.""" with translate_domain_errors(): return service.create(tenant_id, request) # ============================================================================= # Single Item (GET / PUT / DELETE / PATCH status) # ============================================================================= @router.get("/{dsfa_id}") async def get_dsfa( dsfa_id: str, tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """Einzelne DSFA abrufen.""" with translate_domain_errors(): return service.get(dsfa_id, tenant_id) @router.put("/{dsfa_id}") async def update_dsfa( dsfa_id: str, request: DSFAUpdate, tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """DSFA aktualisieren.""" with translate_domain_errors(): return service.update(dsfa_id, tenant_id, request) @router.delete("/{dsfa_id}") async def delete_dsfa( dsfa_id: str, tenant_id: Optional[str] = Query(None), service: DSFAService = Depends(get_dsfa_service), ) -> dict[str, Any]: """DSFA loeschen (Art. 17 DSGVO).""" with translate_domain_errors(): return service.delete(dsfa_id, tenant_id) @router.patch("/{dsfa_id}/status") async def update_dsfa_status( dsfa_id: str, request: DSFAStatusUpdate, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: """Schnell-Statuswechsel.""" with translate_domain_errors(): return wf.update_status(dsfa_id, tenant_id, request) # ============================================================================= # Section Update # ============================================================================= @router.put("/{dsfa_id}/sections/{section_number}") async def update_section( dsfa_id: str, section_number: int, request: DSFASectionUpdate, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: """Update a specific DSFA section (1-8).""" with translate_domain_errors(): return wf.update_section(dsfa_id, section_number, tenant_id, request) # ============================================================================= # Workflow: Submit for Review + Approve # ============================================================================= @router.post("/{dsfa_id}/submit-for-review") async def submit_for_review( dsfa_id: str, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: """Submit a DSFA for DPO review (draft -> in-review).""" with translate_domain_errors(): return wf.submit_for_review(dsfa_id, tenant_id) @router.post("/{dsfa_id}/approve") async def approve_dsfa( dsfa_id: str, request: DSFAApproveRequest, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: """Approve or reject a DSFA (DPO/CISO action).""" with translate_domain_errors(): return wf.approve(dsfa_id, tenant_id, request) # ============================================================================= # Export # ============================================================================= @router.get("/{dsfa_id}/export") async def export_dsfa_json( dsfa_id: str, format: str = Query("json"), tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> dict[str, Any]: """Export a single DSFA as JSON.""" with translate_domain_errors(): return wf.export_json(dsfa_id, tenant_id, format) # ============================================================================= # Versioning # ============================================================================= @router.get("/{dsfa_id}/versions") async def list_dsfa_versions( dsfa_id: str, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> Any: """List all versions for a DSFA.""" with translate_domain_errors(): return wf.list_versions(dsfa_id, tenant_id) @router.get("/{dsfa_id}/versions/{version_number}") async def get_dsfa_version( dsfa_id: str, version_number: int, tenant_id: Optional[str] = Query(None), wf: DSFAWorkflowService = Depends(get_workflow_service), ) -> Any: """Get a specific DSFA version with full snapshot.""" with translate_domain_errors(): return wf.get_version(dsfa_id, version_number, tenant_id) # Legacy re-exports __all__ = [ "router", "DSFACreate", "DSFAUpdate", "DSFAStatusUpdate", "DSFASectionUpdate", "DSFAApproveRequest", "_dsfa_to_response", "_get_tenant_id", "DEFAULT_TENANT_ID", "VALID_STATUSES", "VALID_RISK_LEVELS", ]