""" DSR (Data Subject Request) Routes — Betroffenenanfragen nach DSGVO Art. 15-21. Phase 1 Step 4 refactor: thin handlers delegate to DSRService (CRUD/stats/ export/deadlines) and DSRWorkflowService (status/identity/assign/complete/ reject/communications/exception-checks/templates). """ from typing import Optional from fastapi import APIRouter, Depends, Query, Header from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from classroom_engine.database import get_db from compliance.api._http_errors import translate_domain_errors from compliance.schemas.dsr import ( AssignRequest, CompleteDSR, CreateTemplateVersion, DSRCreate, DSRUpdate, ExtendDeadline, RejectDSR, SendCommunication, StatusChange, UpdateExceptionCheck, VerifyIdentity, ) from compliance.services.dsr_service import ( ART17_EXCEPTIONS, DEFAULT_TENANT, DEADLINE_DAYS, DSRService, VALID_PRIORITIES, VALID_REQUEST_TYPES, VALID_SOURCES, VALID_STATUSES, _dsr_to_dict, _generate_request_number, _get_dsr_or_404, _record_history, ) from compliance.services.dsr_workflow_service import DSRWorkflowService router = APIRouter(prefix="/dsr", tags=["compliance-dsr"]) # --------------------------------------------------------------------------- # DI helpers # --------------------------------------------------------------------------- def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias='X-Tenant-ID')) -> str: return x_tenant_id or DEFAULT_TENANT def _dsr_svc(db: Session = Depends(get_db)) -> DSRService: return DSRService(db) def _wf_svc(db: Session = Depends(get_db)) -> DSRWorkflowService: return DSRWorkflowService(db) # --------------------------------------------------------------------------- # CRUD # --------------------------------------------------------------------------- @router.post("") async def create_dsr( body: DSRCreate, tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.create(body, tenant_id) @router.get("") async def list_dsrs( status: Optional[str] = Query(None), request_type: Optional[str] = Query(None), assigned_to: Optional[str] = Query(None), priority: Optional[str] = Query(None), overdue_only: bool = Query(False), search: Optional[str] = Query(None), from_date: Optional[str] = Query(None), to_date: Optional[str] = Query(None), limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.list( tenant_id, status=status, request_type=request_type, assigned_to=assigned_to, priority=priority, overdue_only=overdue_only, search=search, from_date=from_date, to_date=to_date, limit=limit, offset=offset, ) @router.get("/stats") async def get_dsr_stats( tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.stats(tenant_id) @router.get("/export") async def export_dsrs( format: str = Query("csv", pattern="^(csv|json)$"), tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): result = svc.export(tenant_id, fmt=format) if format == "json": return result return StreamingResponse( result, media_type="text/csv; charset=utf-8", headers={"Content-Disposition": "attachment; filename=dsr_export.csv"}, ) @router.post("/deadlines/process") async def process_deadlines( tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.process_deadlines(tenant_id) # --------------------------------------------------------------------------- # Templates (static paths before /{dsr_id}) # --------------------------------------------------------------------------- @router.get("/templates") async def get_templates( tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_templates(tenant_id) @router.get("/templates/published") async def get_published_templates( request_type: Optional[str] = Query(None), language: str = Query("de"), tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_published_templates( tenant_id, request_type=request_type, language=language, ) @router.get("/templates/{template_id}/versions") async def get_template_versions( template_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_template_versions(template_id, tenant_id) @router.post("/templates/{template_id}/versions") async def create_template_version( template_id: str, body: CreateTemplateVersion, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.create_template_version(template_id, body, tenant_id) @router.put("/template-versions/{version_id}/publish") async def publish_template_version( version_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.publish_template_version(version_id, tenant_id) # --------------------------------------------------------------------------- # Single DSR (parameterized — after static paths) # --------------------------------------------------------------------------- @router.get("/{dsr_id}") async def get_dsr( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.get(dsr_id, tenant_id) @router.put("/{dsr_id}") async def update_dsr( dsr_id: str, body: DSRUpdate, tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.update(dsr_id, body, tenant_id) @router.delete("/{dsr_id}") async def delete_dsr( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRService = Depends(_dsr_svc), ): with translate_domain_errors(): return svc.delete(dsr_id, tenant_id) # --------------------------------------------------------------------------- # Workflow actions # --------------------------------------------------------------------------- @router.post("/{dsr_id}/status") async def change_status( dsr_id: str, body: StatusChange, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.change_status(dsr_id, body, tenant_id) @router.post("/{dsr_id}/verify-identity") async def verify_identity( dsr_id: str, body: VerifyIdentity, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.verify_identity(dsr_id, body, tenant_id) @router.post("/{dsr_id}/assign") async def assign_dsr( dsr_id: str, body: AssignRequest, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.assign(dsr_id, body, tenant_id) @router.post("/{dsr_id}/extend") async def extend_deadline( dsr_id: str, body: ExtendDeadline, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.extend_deadline(dsr_id, body, tenant_id) @router.post("/{dsr_id}/complete") async def complete_dsr( dsr_id: str, body: CompleteDSR, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.complete(dsr_id, body, tenant_id) @router.post("/{dsr_id}/reject") async def reject_dsr( dsr_id: str, body: RejectDSR, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.reject(dsr_id, body, tenant_id) # --------------------------------------------------------------------------- # History & Communications # --------------------------------------------------------------------------- @router.get("/{dsr_id}/history") async def get_history( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_history(dsr_id, tenant_id) @router.get("/{dsr_id}/communications") async def get_communications( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_communications(dsr_id, tenant_id) @router.post("/{dsr_id}/communicate") async def send_communication( dsr_id: str, body: SendCommunication, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.send_communication(dsr_id, body, tenant_id) # --------------------------------------------------------------------------- # Exception Checks (Art. 17) # --------------------------------------------------------------------------- @router.get("/{dsr_id}/exception-checks") async def get_exception_checks( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.get_exception_checks(dsr_id, tenant_id) @router.post("/{dsr_id}/exception-checks/init") async def init_exception_checks( dsr_id: str, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.init_exception_checks(dsr_id, tenant_id) @router.put("/{dsr_id}/exception-checks/{check_id}") async def update_exception_check( dsr_id: str, check_id: str, body: UpdateExceptionCheck, tenant_id: str = Depends(_get_tenant), svc: DSRWorkflowService = Depends(_wf_svc), ): with translate_domain_errors(): return svc.update_exception_check(dsr_id, check_id, body, tenant_id)