Files
breakpilot-compliance/backend-compliance/compliance/api/dsr_routes.py
Sharang Parnerkar 07d470edee refactor(backend/api): extract DSR services (Step 4 — file 15 of 18)
compliance/api/dsr_routes.py (1176 LOC) -> 369 LOC thin routes +
469-line DsrService + 487-line DsrWorkflowService + 101-line schemas.

Two-service split for Data Subject Request (DSGVO Art. 15-22):
  - dsr_service.py: CRUD, list, stats, export, audit log
  - dsr_workflow_service.py: identity verification, processing,
    portability, escalation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:34:48 +02:00

370 lines
10 KiB
Python

"""
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)