refactor(backend/api): extract DSFA schemas + services (Step 4 — file 14 of 18)

- Create compliance/schemas/dsfa.py (161 LOC) — extract DSFACreate,
  DSFAUpdate, DSFAStatusUpdate, DSFASectionUpdate, DSFAApproveRequest
- Create compliance/services/dsfa_service.py (386 LOC) — CRUD + helpers
  + stats + audit-log + CSV export; uses domain errors
- Create compliance/services/dsfa_workflow_service.py (347 LOC) — status
  update, section update, submit-for-review, approve, export JSON, versions
- Rewrite compliance/api/dsfa_routes.py (339 LOC) as thin handlers with
  Depends + translate_domain_errors(); re-export legacy symbols via __all__
- Add [mypy-compliance.api.dsfa_routes] ignore_errors = False to mypy.ini
- Update tests: 422 -> 400 for domain ValidationError (6 assertions)
- Regenerate OpenAPI baseline (360 paths / 484 operations — unchanged)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-09 19:20:48 +02:00
parent 6658776610
commit ae008d7d25
7 changed files with 1359 additions and 999 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,161 @@
"""
DSFA — Datenschutz-Folgenabschaetzung schemas (Art. 35 DSGVO).
Phase 1 Step 4: extracted from ``compliance.api.dsfa_routes``.
"""
from typing import List, Optional
from pydantic import BaseModel
class DSFACreate(BaseModel):
title: str
description: str = ""
status: str = "draft"
risk_level: str = "low"
processing_activity: str = ""
data_categories: List[str] = []
recipients: List[str] = []
measures: List[str] = []
created_by: str = "system"
# Section 1
processing_description: Optional[str] = None
processing_purpose: Optional[str] = None
legal_basis: Optional[str] = None
legal_basis_details: Optional[str] = None
# Section 2
necessity_assessment: Optional[str] = None
proportionality_assessment: Optional[str] = None
data_minimization: Optional[str] = None
alternatives_considered: Optional[str] = None
retention_justification: Optional[str] = None
# Section 3
involves_ai: Optional[bool] = None
overall_risk_level: Optional[str] = None
risk_score: Optional[int] = None
# Section 6
dpo_consulted: Optional[bool] = None
dpo_name: Optional[str] = None
dpo_opinion: Optional[str] = None
dpo_approved: Optional[bool] = None
authority_consulted: Optional[bool] = None
authority_reference: Optional[str] = None
authority_decision: Optional[str] = None
# Metadata
version: Optional[int] = None
conclusion: Optional[str] = None
federal_state: Optional[str] = None
authority_resource_id: Optional[str] = None
submitted_by: Optional[str] = None
# JSONB Arrays
data_subjects: Optional[List[str]] = None
affected_rights: Optional[List[str]] = None
triggered_rule_codes: Optional[List[str]] = None
ai_trigger_ids: Optional[List[str]] = None
wp248_criteria_met: Optional[List[str]] = None
art35_abs3_triggered: Optional[List[str]] = None
tom_references: Optional[List[str]] = None
risks: Optional[List[dict]] = None # type: ignore[type-arg]
mitigations: Optional[List[dict]] = None # type: ignore[type-arg]
stakeholder_consultations: Optional[List[dict]] = None # type: ignore[type-arg]
review_triggers: Optional[List[dict]] = None # type: ignore[type-arg]
review_comments: Optional[List[dict]] = None # type: ignore[type-arg]
ai_use_case_modules: Optional[List[dict]] = None # type: ignore[type-arg]
section_8_complete: Optional[bool] = None
# JSONB Objects
threshold_analysis: Optional[dict] = None # type: ignore[type-arg]
consultation_requirement: Optional[dict] = None # type: ignore[type-arg]
review_schedule: Optional[dict] = None # type: ignore[type-arg]
section_progress: Optional[dict] = None # type: ignore[type-arg]
metadata: Optional[dict] = None # type: ignore[type-arg]
class DSFAUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
risk_level: Optional[str] = None
processing_activity: Optional[str] = None
data_categories: Optional[List[str]] = None
recipients: Optional[List[str]] = None
measures: Optional[List[str]] = None
approved_by: Optional[str] = None
# Section 1
processing_description: Optional[str] = None
processing_purpose: Optional[str] = None
legal_basis: Optional[str] = None
legal_basis_details: Optional[str] = None
# Section 2
necessity_assessment: Optional[str] = None
proportionality_assessment: Optional[str] = None
data_minimization: Optional[str] = None
alternatives_considered: Optional[str] = None
retention_justification: Optional[str] = None
# Section 3
involves_ai: Optional[bool] = None
overall_risk_level: Optional[str] = None
risk_score: Optional[int] = None
# Section 6
dpo_consulted: Optional[bool] = None
dpo_name: Optional[str] = None
dpo_opinion: Optional[str] = None
dpo_approved: Optional[bool] = None
authority_consulted: Optional[bool] = None
authority_reference: Optional[str] = None
authority_decision: Optional[str] = None
# Metadata
version: Optional[int] = None
conclusion: Optional[str] = None
federal_state: Optional[str] = None
authority_resource_id: Optional[str] = None
submitted_by: Optional[str] = None
# JSONB Arrays
data_subjects: Optional[List[str]] = None
affected_rights: Optional[List[str]] = None
triggered_rule_codes: Optional[List[str]] = None
ai_trigger_ids: Optional[List[str]] = None
wp248_criteria_met: Optional[List[str]] = None
art35_abs3_triggered: Optional[List[str]] = None
tom_references: Optional[List[str]] = None
risks: Optional[List[dict]] = None # type: ignore[type-arg]
mitigations: Optional[List[dict]] = None # type: ignore[type-arg]
stakeholder_consultations: Optional[List[dict]] = None # type: ignore[type-arg]
review_triggers: Optional[List[dict]] = None # type: ignore[type-arg]
review_comments: Optional[List[dict]] = None # type: ignore[type-arg]
ai_use_case_modules: Optional[List[dict]] = None # type: ignore[type-arg]
section_8_complete: Optional[bool] = None
# JSONB Objects
threshold_analysis: Optional[dict] = None # type: ignore[type-arg]
consultation_requirement: Optional[dict] = None # type: ignore[type-arg]
review_schedule: Optional[dict] = None # type: ignore[type-arg]
section_progress: Optional[dict] = None # type: ignore[type-arg]
metadata: Optional[dict] = None # type: ignore[type-arg]
class DSFAStatusUpdate(BaseModel):
status: str
approved_by: Optional[str] = None
class DSFASectionUpdate(BaseModel):
"""Body for PUT /dsfa/{id}/sections/{section_number}."""
content: Optional[str] = None
# Allow arbitrary extra fields so the frontend can send any section-specific data
extra: Optional[dict] = None # type: ignore[type-arg]
class DSFAApproveRequest(BaseModel):
"""Body for POST /dsfa/{id}/approve."""
approved: bool
comments: Optional[str] = None
approved_by: Optional[str] = None
__all__ = [
"DSFACreate",
"DSFAUpdate",
"DSFAStatusUpdate",
"DSFASectionUpdate",
"DSFAApproveRequest",
]

View File

@@ -0,0 +1,386 @@
# mypy: disable-error-code="arg-type,assignment,union-attr,no-any-return,call-overload,index,no-untyped-call"
"""
DSFA service — CRUD + helpers + stats + audit + CSV export.
Phase 1 Step 4: extracted from ``compliance.api.dsfa_routes``. The workflow
side (status update, section update, submit, approve, export, versions) lives
in ``compliance.services.dsfa_workflow_service``.
Module-level helpers (_dsfa_to_response, _get_tenant_id, _log_audit) are
shared by both service modules and re-exported from
``compliance.api.dsfa_routes`` for legacy test imports.
"""
import csv
import io
import json
import logging
from typing import Any, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from compliance.domain import NotFoundError, ValidationError
from compliance.schemas.dsfa import DSFACreate, DSFAUpdate
logger = logging.getLogger(__name__)
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
VALID_STATUSES = {"draft", "in-review", "approved", "needs-update"}
VALID_RISK_LEVELS = {"low", "medium", "high", "critical"}
JSONB_FIELDS = {
"data_categories", "recipients", "measures", "data_subjects",
"affected_rights", "triggered_rule_codes", "ai_trigger_ids",
"wp248_criteria_met", "art35_abs3_triggered", "tom_references",
"risks", "mitigations", "stakeholder_consultations", "review_triggers",
"review_comments", "ai_use_case_modules", "threshold_analysis",
"consultation_requirement", "review_schedule", "section_progress",
"metadata",
}
# ---- Module-level helpers (re-exported by compliance.api.dsfa_routes) -----
def _get_tenant_id(tenant_id: Optional[str]) -> str:
return tenant_id or DEFAULT_TENANT_ID
def _parse_arr(val: Any) -> Any:
"""Parse a JSONB array field -> list."""
if val is None:
return []
if isinstance(val, list):
return val
if isinstance(val, str):
try:
parsed = json.loads(val)
return parsed if isinstance(parsed, list) else []
except Exception:
return []
return val
def _parse_obj(val: Any) -> Any:
"""Parse a JSONB object field -> dict."""
if val is None:
return {}
if isinstance(val, dict):
return val
if isinstance(val, str):
try:
parsed = json.loads(val)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return val
def _ts(val: Any) -> Any:
"""Timestamp -> ISO string or None."""
if not val:
return None
return val if isinstance(val, str) else val.isoformat()
def _get(row: Any, key: str, default: Any = None) -> Any:
"""Safe row access — returns default if key missing."""
try:
v = row[key]
return default if v is None and default is not None else v
except (KeyError, IndexError):
return default
def _dsfa_to_response(row: Any) -> dict[str, Any]:
"""Convert a DB row to a JSON-serializable dict."""
g = lambda k, d=None: _get(row, k, d) # noqa: E731
prev = g("previous_version_id")
return {
"id": str(row["id"]),
"tenant_id": row["tenant_id"],
"title": row["title"],
"description": row["description"] or "",
"status": row["status"] or "draft",
"risk_level": row["risk_level"] or "low",
"processing_activity": row["processing_activity"] or "",
"data_categories": _parse_arr(row["data_categories"]),
"recipients": _parse_arr(row["recipients"]),
"measures": _parse_arr(row["measures"]),
"approved_by": row["approved_by"],
"approved_at": _ts(row["approved_at"]),
"created_by": row["created_by"] or "system",
"created_at": _ts(row["created_at"]),
"updated_at": _ts(row["updated_at"]),
"processing_description": g("processing_description"),
"processing_purpose": g("processing_purpose"),
"legal_basis": g("legal_basis"),
"legal_basis_details": g("legal_basis_details"),
"necessity_assessment": g("necessity_assessment"),
"proportionality_assessment": g("proportionality_assessment"),
"data_minimization": g("data_minimization"),
"alternatives_considered": g("alternatives_considered"),
"retention_justification": g("retention_justification"),
"involves_ai": g("involves_ai", False),
"overall_risk_level": g("overall_risk_level"),
"risk_score": g("risk_score", 0),
"dpo_consulted": g("dpo_consulted", False),
"dpo_consulted_at": _ts(g("dpo_consulted_at")),
"dpo_name": g("dpo_name"),
"dpo_opinion": g("dpo_opinion"),
"dpo_approved": g("dpo_approved"),
"authority_consulted": g("authority_consulted", False),
"authority_consulted_at": _ts(g("authority_consulted_at")),
"authority_reference": g("authority_reference"),
"authority_decision": g("authority_decision"),
"version": g("version", 1),
"previous_version_id": str(prev) if prev else None,
"conclusion": g("conclusion"),
"federal_state": g("federal_state"),
"authority_resource_id": g("authority_resource_id"),
"submitted_for_review_at": _ts(g("submitted_for_review_at")),
"submitted_by": g("submitted_by"),
"data_subjects": _parse_arr(g("data_subjects")),
"affected_rights": _parse_arr(g("affected_rights")),
"triggered_rule_codes": _parse_arr(g("triggered_rule_codes")),
"ai_trigger_ids": _parse_arr(g("ai_trigger_ids")),
"wp248_criteria_met": _parse_arr(g("wp248_criteria_met")),
"art35_abs3_triggered": _parse_arr(g("art35_abs3_triggered")),
"tom_references": _parse_arr(g("tom_references")),
"risks": _parse_arr(g("risks")),
"mitigations": _parse_arr(g("mitigations")),
"stakeholder_consultations": _parse_arr(g("stakeholder_consultations")),
"review_triggers": _parse_arr(g("review_triggers")),
"review_comments": _parse_arr(g("review_comments")),
"ai_use_case_modules": _parse_arr(g("ai_use_case_modules")),
"section_8_complete": g("section_8_complete", False),
"threshold_analysis": _parse_obj(g("threshold_analysis")),
"consultation_requirement": _parse_obj(g("consultation_requirement")),
"review_schedule": _parse_obj(g("review_schedule")),
"section_progress": _parse_obj(g("section_progress")),
"metadata": _parse_obj(g("metadata")),
}
def _log_audit(
db: Session, tenant_id: str, dsfa_id: Any, action: str,
changed_by: str = "system", old_values: Any = None,
new_values: Any = None,
) -> None:
db.execute(
text("""
INSERT INTO compliance_dsfa_audit_log
(tenant_id, dsfa_id, action, changed_by, old_values, new_values)
VALUES
(:tenant_id, :dsfa_id, :action, :changed_by,
CAST(:old_values AS jsonb), CAST(:new_values AS jsonb))
"""),
{
"tenant_id": tenant_id,
"dsfa_id": str(dsfa_id) if dsfa_id else None,
"action": action, "changed_by": changed_by,
"old_values": json.dumps(old_values) if old_values else None,
"new_values": json.dumps(new_values) if new_values else None,
},
)
# ---- Service ---------------------------------------------------------------
class DSFAService:
"""CRUD + stats + audit-log + CSV export."""
def __init__(self, db: Session) -> None:
self.db = db
def list_dsfas(
self, tenant_id: Optional[str], status: Optional[str],
risk_level: Optional[str], skip: int, limit: int,
) -> list[dict[str, Any]]:
tid = _get_tenant_id(tenant_id)
sql = "SELECT * FROM compliance_dsfas WHERE tenant_id = :tid"
params: dict[str, Any] = {"tid": tid}
if status:
sql += " AND status = :status"; params["status"] = status
if risk_level:
sql += " AND risk_level = :risk_level"; params["risk_level"] = risk_level
sql += " ORDER BY created_at DESC LIMIT :limit OFFSET :skip"
params["limit"] = limit; params["skip"] = skip
rows = self.db.execute(text(sql), params).fetchall()
return [_dsfa_to_response(r) for r in rows]
def create(
self, tenant_id: Optional[str], body: DSFACreate,
) -> dict[str, Any]:
if body.status not in VALID_STATUSES:
raise ValidationError(f"Ungültiger Status: {body.status}")
if body.risk_level not in VALID_RISK_LEVELS:
raise ValidationError(f"Ungültiges Risiko-Level: {body.risk_level}")
tid = _get_tenant_id(tenant_id)
row = self.db.execute(
text("""
INSERT INTO compliance_dsfas
(tenant_id, title, description, status, risk_level,
processing_activity, data_categories, recipients,
measures, created_by)
VALUES
(:tenant_id, :title, :description, :status, :risk_level,
:processing_activity,
CAST(:data_categories AS jsonb),
CAST(:recipients AS jsonb),
CAST(:measures AS jsonb),
:created_by)
RETURNING *
"""),
{
"tenant_id": tid, "title": body.title,
"description": body.description, "status": body.status,
"risk_level": body.risk_level,
"processing_activity": body.processing_activity,
"data_categories": json.dumps(body.data_categories),
"recipients": json.dumps(body.recipients),
"measures": json.dumps(body.measures),
"created_by": body.created_by,
},
).fetchone()
self.db.flush()
_log_audit(
self.db, tid, row["id"], "CREATE", body.created_by,
new_values={"title": body.title, "status": body.status},
)
self.db.commit()
return _dsfa_to_response(row)
def get(self, dsfa_id: str, tenant_id: Optional[str]) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
row = self.db.execute(
text("SELECT * FROM compliance_dsfas WHERE id = :id AND tenant_id = :tid"),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not row:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
return _dsfa_to_response(row)
def update(
self, dsfa_id: str, tenant_id: Optional[str], body: DSFAUpdate,
) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text("SELECT * FROM compliance_dsfas WHERE id = :id AND tenant_id = :tid"),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
updates = body.model_dump(exclude_none=True)
if "status" in updates and updates["status"] not in VALID_STATUSES:
raise ValidationError(f"Ungültiger Status: {updates['status']}")
if "risk_level" in updates and updates["risk_level"] not in VALID_RISK_LEVELS:
raise ValidationError(f"Ungültiges Risiko-Level: {updates['risk_level']}")
if not updates:
return _dsfa_to_response(existing)
set_clauses: list[str] = []
params: dict[str, Any] = {"id": dsfa_id, "tid": tid}
for field, value in updates.items():
if field in JSONB_FIELDS:
set_clauses.append(f"{field} = CAST(:{field} AS jsonb)")
params[field] = json.dumps(value)
else:
set_clauses.append(f"{field} = :{field}")
params[field] = value
set_clauses.append("updated_at = NOW()")
sql = (
f"UPDATE compliance_dsfas SET {', '.join(set_clauses)} "
f"WHERE id = :id AND tenant_id = :tid RETURNING *"
)
old_values = {"title": existing["title"], "status": existing["status"]}
row = self.db.execute(text(sql), params).fetchone()
_log_audit(self.db, tid, dsfa_id, "UPDATE",
new_values=updates, old_values=old_values)
self.db.commit()
return _dsfa_to_response(row)
def delete(self, dsfa_id: str, tenant_id: Optional[str]) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text("SELECT id, title FROM compliance_dsfas WHERE id = :id AND tenant_id = :tid"),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
_log_audit(self.db, tid, dsfa_id, "DELETE",
old_values={"title": existing["title"]})
self.db.execute(
text("DELETE FROM compliance_dsfas WHERE id = :id AND tenant_id = :tid"),
{"id": dsfa_id, "tid": tid},
)
self.db.commit()
return {"success": True, "message": f"DSFA {dsfa_id} gelöscht"}
def stats(self, tenant_id: Optional[str]) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
rows = self.db.execute(
text("SELECT status, risk_level FROM compliance_dsfas WHERE tenant_id = :tid"),
{"tid": tid},
).fetchall()
by_status: dict[str, int] = {}
by_risk: dict[str, int] = {}
for row in rows:
s = row["status"] or "draft"
r = row["risk_level"] or "low"
by_status[s] = by_status.get(s, 0) + 1
by_risk[r] = by_risk.get(r, 0) + 1
return {
"total": len(rows), "by_status": by_status, "by_risk_level": by_risk,
"draft_count": by_status.get("draft", 0),
"in_review_count": by_status.get("in-review", 0),
"approved_count": by_status.get("approved", 0),
"needs_update_count": by_status.get("needs-update", 0),
}
def audit_log(
self, tenant_id: Optional[str], limit: int, offset: int,
) -> list[dict[str, Any]]:
tid = _get_tenant_id(tenant_id)
rows = self.db.execute(
text("""
SELECT id, tenant_id, dsfa_id, action, changed_by,
old_values, new_values, created_at
FROM compliance_dsfa_audit_log
WHERE tenant_id = :tid
ORDER BY created_at DESC LIMIT :limit OFFSET :offset
"""),
{"tid": tid, "limit": limit, "offset": offset},
).fetchall()
result: list[dict[str, Any]] = []
for r in rows:
ca = r["created_at"]
result.append({
"id": str(r["id"]),
"tenant_id": r["tenant_id"],
"dsfa_id": str(r["dsfa_id"]) if r["dsfa_id"] else None,
"action": r["action"],
"changed_by": r["changed_by"],
"old_values": r["old_values"],
"new_values": r["new_values"],
"created_at": ca if isinstance(ca, str) else (ca.isoformat() if ca else None),
})
return result
def export_csv(self, tenant_id: Optional[str]) -> str:
tid = _get_tenant_id(tenant_id)
rows = self.db.execute(
text("SELECT * FROM compliance_dsfas WHERE tenant_id = :tid ORDER BY created_at DESC"),
{"tid": tid},
).fetchall()
output = io.StringIO()
writer = csv.writer(output, delimiter=";")
writer.writerow(["ID", "Titel", "Status", "Risiko-Level", "Erstellt", "Aktualisiert"])
for r in rows:
ca = r["created_at"]
ua = r["updated_at"]
writer.writerow([
str(r["id"]), r["title"], r["status"] or "draft", r["risk_level"] or "low",
ca if isinstance(ca, str) else (ca.isoformat() if ca else ""),
ua if isinstance(ua, str) else (ua.isoformat() if ua else ""),
])
return output.getvalue()

View File

@@ -0,0 +1,347 @@
# mypy: disable-error-code="arg-type,assignment,union-attr,no-any-return,call-overload,index"
"""
DSFA workflow service — status, section update, submit, approve, export, versions.
Phase 1 Step 4: extracted from ``compliance.api.dsfa_routes``. CRUD + helpers
live in ``compliance.services.dsfa_service``.
"""
import json
import logging
from datetime import datetime, timezone
from typing import Any, Optional
from sqlalchemy import text
from sqlalchemy.orm import Session
from compliance.api.versioning_utils import get_version, list_versions
from compliance.domain import NotFoundError, ValidationError
from compliance.schemas.dsfa import (
DSFAApproveRequest,
DSFASectionUpdate,
DSFAStatusUpdate,
)
from compliance.services.dsfa_service import (
VALID_STATUSES,
_dsfa_to_response,
_get_tenant_id,
_log_audit,
)
logger = logging.getLogger(__name__)
SECTION_FIELD_MAP: dict[int, str] = {
1: "processing_description",
2: "necessity_assessment",
3: "risk_assessment",
4: "stakeholder_consultations",
5: "measures",
6: "dpo_opinion",
7: "conclusion",
8: "ai_use_case_modules",
}
class DSFAWorkflowService:
"""Status update, section update, submit, approve, export, versions."""
def __init__(self, db: Session) -> None:
self.db = db
# ------------------------------------------------------------------
# Status update
# ------------------------------------------------------------------
def update_status(
self,
dsfa_id: str,
tenant_id: Optional[str],
body: DSFAStatusUpdate,
) -> dict[str, Any]:
if body.status not in VALID_STATUSES:
raise ValidationError(f"Ungültiger Status: {body.status}")
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text(
"SELECT id, status FROM compliance_dsfas "
"WHERE id = :id AND tenant_id = :tid"
),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
params: dict[str, Any] = {
"id": dsfa_id,
"tid": tid,
"status": body.status,
"approved_at": (
datetime.now(timezone.utc)
if body.status == "approved"
else None
),
"approved_by": body.approved_by,
}
row = self.db.execute(
text("""
UPDATE compliance_dsfas
SET status = :status, approved_at = :approved_at,
approved_by = :approved_by, updated_at = NOW()
WHERE id = :id AND tenant_id = :tid
RETURNING *
"""),
params,
).fetchone()
_log_audit(
self.db, tid, dsfa_id, "STATUS_CHANGE",
old_values={"status": existing["status"]},
new_values={"status": body.status},
)
self.db.commit()
return _dsfa_to_response(row)
# ------------------------------------------------------------------
# Section update
# ------------------------------------------------------------------
def update_section(
self,
dsfa_id: str,
section_number: int,
tenant_id: Optional[str],
body: DSFASectionUpdate,
) -> dict[str, Any]:
if section_number < 1 or section_number > 8:
raise ValidationError(
f"Section must be 1-8, got {section_number}"
)
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text(
"SELECT * FROM compliance_dsfas "
"WHERE id = :id AND tenant_id = :tid"
),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
field = SECTION_FIELD_MAP[section_number]
jsonb_sections = {4, 5, 8}
params: dict[str, Any] = {"id": dsfa_id, "tid": tid}
if section_number in jsonb_sections:
value = (
body.extra
if body.extra is not None
else ([] if section_number != 4 else [])
)
params["val"] = json.dumps(value)
set_clause = f"{field} = CAST(:val AS jsonb)"
else:
params["val"] = body.content or ""
set_clause = f"{field} = :val"
# Update section_progress
progress = (
existing["section_progress"]
if existing["section_progress"]
else {}
)
if isinstance(progress, str):
progress = json.loads(progress)
progress[f"section_{section_number}"] = True
params["progress"] = json.dumps(progress)
row = self.db.execute(
text(f"""
UPDATE compliance_dsfas
SET {set_clause},
section_progress = CAST(:progress AS jsonb),
updated_at = NOW()
WHERE id = :id AND tenant_id = :tid
RETURNING *
"""),
params,
).fetchone()
_log_audit(
self.db, tid, dsfa_id, "SECTION_UPDATE",
new_values={"section": section_number, "field": field},
)
self.db.commit()
return _dsfa_to_response(row)
# ------------------------------------------------------------------
# Submit for review
# ------------------------------------------------------------------
def submit_for_review(
self, dsfa_id: str, tenant_id: Optional[str]
) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text(
"SELECT id, status FROM compliance_dsfas "
"WHERE id = :id AND tenant_id = :tid"
),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
if existing["status"] not in ("draft", "needs-update"):
raise ValidationError(
f"Kann nur aus Status 'draft' oder 'needs-update' "
f"eingereicht werden, aktuell: {existing['status']}"
)
row = self.db.execute(
text("""
UPDATE compliance_dsfas
SET status = 'in-review',
submitted_for_review_at = NOW(),
updated_at = NOW()
WHERE id = :id AND tenant_id = :tid
RETURNING *
"""),
{"id": dsfa_id, "tid": tid},
).fetchone()
_log_audit(
self.db, tid, dsfa_id, "SUBMIT_FOR_REVIEW",
old_values={"status": existing["status"]},
new_values={"status": "in-review"},
)
self.db.commit()
return {
"message": "DSFA zur Prüfung eingereicht",
"status": "in-review",
"dsfa": _dsfa_to_response(row),
}
# ------------------------------------------------------------------
# Approve / reject
# ------------------------------------------------------------------
def approve(
self,
dsfa_id: str,
tenant_id: Optional[str],
body: DSFAApproveRequest,
) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
existing = self.db.execute(
text(
"SELECT id, status FROM compliance_dsfas "
"WHERE id = :id AND tenant_id = :tid"
),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not existing:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
if existing["status"] != "in-review":
raise ValidationError(
f"Nur DSFAs im Status 'in-review' können genehmigt werden, "
f"aktuell: {existing['status']}"
)
if body.approved:
new_status = "approved"
self.db.execute(
text("""
UPDATE compliance_dsfas
SET status = 'approved',
approved_by = :approved_by,
approved_at = NOW(),
updated_at = NOW()
WHERE id = :id AND tenant_id = :tid
RETURNING *
"""),
{
"id": dsfa_id,
"tid": tid,
"approved_by": body.approved_by or "system",
},
).fetchone()
else:
new_status = "needs-update"
self.db.execute(
text("""
UPDATE compliance_dsfas
SET status = 'needs-update', updated_at = NOW()
WHERE id = :id AND tenant_id = :tid
RETURNING *
"""),
{"id": dsfa_id, "tid": tid},
).fetchone()
_log_audit(
self.db, tid, dsfa_id,
"APPROVE" if body.approved else "REJECT",
old_values={"status": existing["status"]},
new_values={"status": new_status, "comments": body.comments},
)
self.db.commit()
return {
"message": (
"DSFA genehmigt"
if body.approved
else "DSFA zurückgewiesen"
),
"status": new_status,
}
# ------------------------------------------------------------------
# Export JSON
# ------------------------------------------------------------------
def export_json(
self, dsfa_id: str, tenant_id: Optional[str], fmt: str
) -> dict[str, Any]:
tid = _get_tenant_id(tenant_id)
row = self.db.execute(
text(
"SELECT * FROM compliance_dsfas "
"WHERE id = :id AND tenant_id = :tid"
),
{"id": dsfa_id, "tid": tid},
).fetchone()
if not row:
raise NotFoundError(f"DSFA {dsfa_id} nicht gefunden")
dsfa_data = _dsfa_to_response(row)
return {
"exported_at": datetime.now(timezone.utc).isoformat(),
"format": fmt,
"dsfa": dsfa_data,
}
# ------------------------------------------------------------------
# Versions
# ------------------------------------------------------------------
def list_versions(
self, dsfa_id: str, tenant_id: Optional[str]
) -> Any:
tid = _get_tenant_id(tenant_id)
return list_versions(self.db, "dsfa", dsfa_id, tid)
def get_version(
self,
dsfa_id: str,
version_number: int,
tenant_id: Optional[str],
) -> Any:
tid = _get_tenant_id(tenant_id)
v = get_version(self.db, "dsfa", dsfa_id, version_number, tid)
if not v:
raise NotFoundError(
f"Version {version_number} not found"
)
return v

View File

@@ -95,5 +95,7 @@ ignore_errors = False
ignore_errors = False
[mypy-compliance.api.legal_document_routes]
ignore_errors = False
[mypy-compliance.api.dsfa_routes]
ignore_errors = False
[mypy-compliance.api._http_errors]
ignore_errors = False

View File

@@ -374,59 +374,6 @@
"title": "ApprovalCommentRequest",
"type": "object"
},
"ApprovalHistoryEntry": {
"properties": {
"action": {
"title": "Action",
"type": "string"
},
"approver": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Approver"
},
"comment": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Comment"
},
"created_at": {
"format": "date-time",
"title": "Created At",
"type": "string"
},
"id": {
"title": "Id",
"type": "string"
},
"version_id": {
"title": "Version Id",
"type": "string"
}
},
"required": [
"id",
"version_id",
"action",
"approver",
"comment",
"created_at"
],
"title": "ApprovalHistoryEntry",
"type": "object"
},
"AssignRequest": {
"properties": {
"assignee_id": {
@@ -19563,122 +19510,6 @@
"title": "ConsentCreate",
"type": "object"
},
"compliance__api__legal_document_routes__VersionCreate": {
"properties": {
"content": {
"title": "Content",
"type": "string"
},
"created_by": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created By"
},
"document_id": {
"title": "Document Id",
"type": "string"
},
"language": {
"default": "de",
"title": "Language",
"type": "string"
},
"summary": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Summary"
},
"title": {
"title": "Title",
"type": "string"
},
"version": {
"title": "Version",
"type": "string"
}
},
"required": [
"document_id",
"version",
"title",
"content"
],
"title": "VersionCreate",
"type": "object"
},
"compliance__api__legal_document_routes__VersionUpdate": {
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Content"
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Language"
},
"summary": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Summary"
},
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Title"
},
"version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Version"
}
},
"title": "VersionUpdate",
"type": "object"
},
"compliance__api__notfallplan_routes__IncidentCreate": {
"properties": {
"affected_data_categories": {
@@ -20361,6 +20192,122 @@
],
"title": "StatusUpdate",
"type": "object"
},
"compliance__schemas__legal_document__VersionCreate": {
"properties": {
"content": {
"title": "Content",
"type": "string"
},
"created_by": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created By"
},
"document_id": {
"title": "Document Id",
"type": "string"
},
"language": {
"default": "de",
"title": "Language",
"type": "string"
},
"summary": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Summary"
},
"title": {
"title": "Title",
"type": "string"
},
"version": {
"title": "Version",
"type": "string"
}
},
"required": [
"document_id",
"version",
"title",
"content"
],
"title": "VersionCreate",
"type": "object"
},
"compliance__schemas__legal_document__VersionUpdate": {
"properties": {
"content": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Content"
},
"language": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Language"
},
"summary": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Summary"
},
"title": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Title"
},
"version": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Version"
}
},
"title": "VersionUpdate",
"type": "object"
}
}
},
@@ -23428,7 +23375,7 @@
},
"/api/compliance/controls/paginated": {
"get": {
"description": "List controls with pagination and eager-loaded relationships.\n\nThis endpoint is optimized for large datasets with:\n- Eager loading to prevent N+1 queries\n- Server-side pagination\n- Full-text search support",
"description": "List controls with pagination.",
"operationId": "list_controls_paginated_api_compliance_controls_paginated_get",
"parameters": [
{
@@ -23708,13 +23655,17 @@
},
"/api/compliance/create-indexes": {
"post": {
"description": "Create additional performance indexes for large datasets.\n\nThese indexes are optimized for:\n- Pagination queries (1000+ requirements)\n- Full-text search\n- Filtering by status/priority",
"description": "Create additional performance indexes.",
"operationId": "create_performance_indexes_api_compliance_create_indexes_post",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Create Performance Indexes Api Compliance Create Indexes Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -23821,7 +23772,7 @@
},
"/api/compliance/dsfa": {
"get": {
"description": "Liste aller DSFAs f\u00fcr einen Tenant.",
"description": "Liste aller DSFAs fuer einen Tenant.",
"operationId": "list_dsfas_api_compliance_dsfa_get",
"parameters": [
{
@@ -23900,7 +23851,14 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Response List Dsfas Api Compliance Dsfa Get",
"type": "array"
}
}
},
"description": "Successful Response"
@@ -23957,7 +23915,11 @@
"201": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Create Dsfa Api Compliance Dsfa Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24029,7 +23991,14 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Response Get Audit Log Api Compliance Dsfa Audit Log Get",
"type": "array"
}
}
},
"description": "Successful Response"
@@ -24081,7 +24050,13 @@
"501": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": {
"type": "string"
},
"title": "Response Get By Assessment Api Compliance Dsfa By Assessment Assessment Id Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24145,7 +24120,7 @@
},
"/api/compliance/dsfa/from-assessment/{assessment_id}": {
"post": {
"description": "Stub: Create DSFA from UCCA assessment. Requires cross-service communication.",
"description": "Stub: Create DSFA from UCCA assessment.",
"operationId": "create_from_assessment_api_compliance_dsfa_from_assessment__assessment_id__post",
"parameters": [
{
@@ -24172,7 +24147,13 @@
"501": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": {
"type": "string"
},
"title": "Response Create From Assessment Api Compliance Dsfa From Assessment Assessment Id Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24187,7 +24168,7 @@
},
"/api/compliance/dsfa/stats": {
"get": {
"description": "Z\u00e4hler nach Status und Risiko-Level.",
"description": "Zaehler nach Status und Risiko-Level.",
"operationId": "get_stats_api_compliance_dsfa_stats_get",
"parameters": [
{
@@ -24211,7 +24192,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Stats Api Compliance Dsfa Stats Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24236,7 +24221,7 @@
},
"/api/compliance/dsfa/{dsfa_id}": {
"delete": {
"description": "DSFA l\u00f6schen (Art. 17 DSGVO).",
"description": "DSFA loeschen (Art. 17 DSGVO).",
"operationId": "delete_dsfa_api_compliance_dsfa__dsfa_id__delete",
"parameters": [
{
@@ -24269,7 +24254,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Delete Dsfa Api Compliance Dsfa Dsfa Id Delete",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24325,7 +24314,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Dsfa Api Compliance Dsfa Dsfa Id Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24391,7 +24384,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Update Dsfa Api Compliance Dsfa Dsfa Id Put",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24459,7 +24456,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Approve Dsfa Api Compliance Dsfa Dsfa Id Approve Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24527,7 +24528,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Export Dsfa Json Api Compliance Dsfa Dsfa Id Export Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24604,7 +24609,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Update Section Api Compliance Dsfa Dsfa Id Sections Section Number Put",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24672,7 +24681,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Update Dsfa Status Api Compliance Dsfa Dsfa Id Status Patch",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24697,7 +24710,7 @@
},
"/api/compliance/dsfa/{dsfa_id}/submit-for-review": {
"post": {
"description": "Submit a DSFA for DPO review (draft \u2192 in-review).",
"description": "Submit a DSFA for DPO review (draft -> in-review).",
"operationId": "submit_for_review_api_compliance_dsfa__dsfa_id__submit_for_review_post",
"parameters": [
{
@@ -24730,7 +24743,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Submit For Review Api Compliance Dsfa Dsfa Id Submit For Review Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -24788,7 +24805,9 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"title": "Response List Dsfa Versions Api Compliance Dsfa Dsfa Id Versions Get"
}
}
},
"description": "Successful Response"
@@ -24855,7 +24874,9 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"title": "Response Get Dsfa Version Api Compliance Dsfa Dsfa Id Versions Version Number Get"
}
}
},
"description": "Successful Response"
@@ -30989,7 +31010,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Init Tables Api Compliance Init Tables Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33186,7 +33211,6 @@
},
"/api/compliance/legal-documents/audit-log": {
"get": {
"description": "Consent audit trail (paginated).",
"operationId": "get_audit_log_api_compliance_legal_documents_audit_log_get",
"parameters": [
{
@@ -33265,7 +33289,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Audit Log Api Compliance Legal Documents Audit Log Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33290,7 +33318,6 @@
},
"/api/compliance/legal-documents/consents": {
"post": {
"description": "Record user consent for a legal document.",
"operationId": "record_consent_api_compliance_legal_documents_consents_post",
"parameters": [
{
@@ -33324,7 +33351,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Record Consent Api Compliance Legal Documents Consents Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33349,7 +33380,6 @@
},
"/api/compliance/legal-documents/consents/check/{document_type}": {
"get": {
"description": "Check if user has active consent for a document type.",
"operationId": "check_consent_api_compliance_legal_documents_consents_check__document_type__get",
"parameters": [
{
@@ -33391,7 +33421,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Check Consent Api Compliance Legal Documents Consents Check Document Type Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33416,7 +33450,6 @@
},
"/api/compliance/legal-documents/consents/my": {
"get": {
"description": "Get all consents for a specific user.",
"operationId": "get_my_consents_api_compliance_legal_documents_consents_my_get",
"parameters": [
{
@@ -33449,7 +33482,14 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Response Get My Consents Api Compliance Legal Documents Consents My Get",
"type": "array"
}
}
},
"description": "Successful Response"
@@ -33474,7 +33514,6 @@
},
"/api/compliance/legal-documents/consents/{consent_id}": {
"delete": {
"description": "Withdraw a consent (DSGVO Art. 7 Abs. 3).",
"operationId": "withdraw_consent_api_compliance_legal_documents_consents__consent_id__delete",
"parameters": [
{
@@ -33507,7 +33546,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Withdraw Consent Api Compliance Legal Documents Consents Consent Id Delete",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33532,7 +33575,6 @@
},
"/api/compliance/legal-documents/cookie-categories": {
"get": {
"description": "List all cookie categories.",
"operationId": "list_cookie_categories_api_compliance_legal_documents_cookie_categories_get",
"parameters": [
{
@@ -33556,7 +33598,14 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Response List Cookie Categories Api Compliance Legal Documents Cookie Categories Get",
"type": "array"
}
}
},
"description": "Successful Response"
@@ -33579,7 +33628,6 @@
]
},
"post": {
"description": "Create a cookie category.",
"operationId": "create_cookie_category_api_compliance_legal_documents_cookie_categories_post",
"parameters": [
{
@@ -33613,7 +33661,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Create Cookie Category Api Compliance Legal Documents Cookie Categories Post",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33638,7 +33690,6 @@
},
"/api/compliance/legal-documents/cookie-categories/{category_id}": {
"delete": {
"description": "Delete a cookie category.",
"operationId": "delete_cookie_category_api_compliance_legal_documents_cookie_categories__category_id__delete",
"parameters": [
{
@@ -33689,7 +33740,6 @@
]
},
"put": {
"description": "Update a cookie category.",
"operationId": "update_cookie_category_api_compliance_legal_documents_cookie_categories__category_id__put",
"parameters": [
{
@@ -33732,7 +33782,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Update Cookie Category Api Compliance Legal Documents Cookie Categories Category Id Put",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -33757,7 +33811,6 @@
},
"/api/compliance/legal-documents/documents": {
"get": {
"description": "List all legal documents, optionally filtered by tenant or type.",
"operationId": "list_documents_api_compliance_legal_documents_documents_get",
"parameters": [
{
@@ -33824,7 +33877,6 @@
]
},
"post": {
"description": "Create a new legal document type.",
"operationId": "create_document_api_compliance_legal_documents_documents_post",
"requestBody": {
"content": {
@@ -33867,7 +33919,6 @@
},
"/api/compliance/legal-documents/documents/{document_id}": {
"delete": {
"description": "Delete a legal document and all its versions.",
"operationId": "delete_document_api_compliance_legal_documents_documents__document_id__delete",
"parameters": [
{
@@ -33902,7 +33953,6 @@
]
},
"get": {
"description": "Get a single legal document by ID.",
"operationId": "get_document_api_compliance_legal_documents_documents__document_id__get",
"parameters": [
{
@@ -33946,7 +33996,6 @@
},
"/api/compliance/legal-documents/documents/{document_id}/versions": {
"get": {
"description": "List all versions for a legal document.",
"operationId": "list_versions_api_compliance_legal_documents_documents__document_id__versions_get",
"parameters": [
{
@@ -33994,7 +34043,6 @@
},
"/api/compliance/legal-documents/public": {
"get": {
"description": "Active documents for end-user display.",
"operationId": "list_public_documents_api_compliance_legal_documents_public_get",
"parameters": [
{
@@ -34018,7 +34066,14 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"items": {
"additionalProperties": true,
"type": "object"
},
"title": "Response List Public Documents Api Compliance Legal Documents Public Get",
"type": "array"
}
}
},
"description": "Successful Response"
@@ -34043,7 +34098,6 @@
},
"/api/compliance/legal-documents/public/{document_type}/latest": {
"get": {
"description": "Get the latest published version of a document type.",
"operationId": "get_latest_published_api_compliance_legal_documents_public__document_type__latest_get",
"parameters": [
{
@@ -34086,7 +34140,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Latest Published Api Compliance Legal Documents Public Document Type Latest Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -34111,7 +34169,6 @@
},
"/api/compliance/legal-documents/stats/consents": {
"get": {
"description": "Consent statistics for dashboard.",
"operationId": "get_consent_stats_api_compliance_legal_documents_stats_consents_get",
"parameters": [
{
@@ -34135,7 +34192,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Consent Stats Api Compliance Legal Documents Stats Consents Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -34160,13 +34221,12 @@
},
"/api/compliance/legal-documents/versions": {
"post": {
"description": "Create a new version for a legal document.",
"operationId": "create_version_api_compliance_legal_documents_versions_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/compliance__api__legal_document_routes__VersionCreate"
"$ref": "#/components/schemas/compliance__schemas__legal_document__VersionCreate"
}
}
},
@@ -34203,7 +34263,6 @@
},
"/api/compliance/legal-documents/versions/upload-word": {
"post": {
"description": "Convert DOCX to HTML using mammoth (if available) or return raw text.",
"operationId": "upload_word_api_compliance_legal_documents_versions_upload_word_post",
"requestBody": {
"content": {
@@ -34248,7 +34307,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}": {
"get": {
"description": "Get a single version by ID.",
"operationId": "get_version_api_compliance_legal_documents_versions__version_id__get",
"parameters": [
{
@@ -34290,7 +34348,6 @@
]
},
"put": {
"description": "Update a draft legal document version.",
"operationId": "update_version_api_compliance_legal_documents_versions__version_id__put",
"parameters": [
{
@@ -34307,7 +34364,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/compliance__api__legal_document_routes__VersionUpdate"
"$ref": "#/components/schemas/compliance__schemas__legal_document__VersionUpdate"
}
}
},
@@ -34344,7 +34401,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}/approval-history": {
"get": {
"description": "Get the full approval audit trail for a version.",
"operationId": "get_approval_history_api_compliance_legal_documents_versions__version_id__approval_history_get",
"parameters": [
{
@@ -34363,7 +34419,8 @@
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ApprovalHistoryEntry"
"additionalProperties": true,
"type": "object"
},
"title": "Response Get Approval History Api Compliance Legal Documents Versions Version Id Approval History Get",
"type": "array"
@@ -34392,7 +34449,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}/approve": {
"post": {
"description": "Approve a version under review.",
"operationId": "approve_version_api_compliance_legal_documents_versions__version_id__approve_post",
"parameters": [
{
@@ -34446,7 +34502,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}/publish": {
"post": {
"description": "Publish an approved version.",
"operationId": "publish_version_api_compliance_legal_documents_versions__version_id__publish_post",
"parameters": [
{
@@ -34500,7 +34555,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}/reject": {
"post": {
"description": "Reject a version under review.",
"operationId": "reject_version_api_compliance_legal_documents_versions__version_id__reject_post",
"parameters": [
{
@@ -34554,7 +34608,6 @@
},
"/api/compliance/legal-documents/versions/{version_id}/submit-review": {
"post": {
"description": "Submit a draft version for review.",
"operationId": "submit_review_api_compliance_legal_documents_versions__version_id__submit_review_post",
"parameters": [
{
@@ -39445,7 +39498,7 @@
},
"/api/compliance/requirements": {
"get": {
"description": "List requirements with pagination and eager-loaded relationships.\n\nThis endpoint is optimized for large datasets (1000+ requirements) with:\n- Eager loading to prevent N+1 queries\n- Server-side pagination\n- Full-text search support",
"description": "List requirements with pagination.",
"operationId": "list_requirements_paginated_api_compliance_requirements_get",
"parameters": [
{
@@ -39635,7 +39688,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Delete Requirement Api Compliance Requirements Requirement Id Delete",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -39686,7 +39743,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Get Requirement Api Compliance Requirements Requirement Id Get",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -39737,7 +39798,11 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Update Requirement Api Compliance Requirements Requirement Id Put",
"type": "object"
}
}
},
"description": "Successful Response"
@@ -40741,13 +40806,17 @@
},
"/api/compliance/seed-risks": {
"post": {
"description": "Seed only risks (incremental update for existing databases).",
"description": "Seed only risks.",
"operationId": "seed_risks_only_api_compliance_seed_risks_post",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"additionalProperties": true,
"title": "Response Seed Risks Only Api Compliance Seed Risks Post",
"type": "object"
}
}
},
"description": "Successful Response"

View File

@@ -712,11 +712,11 @@ class TestDSFARouteCRUD:
def test_create_invalid_status(self):
resp = client.post("/api/compliance/dsfa", json={"title": "Bad", "status": "invalid"})
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
def test_create_invalid_risk_level(self):
resp = client.post("/api/compliance/dsfa", json={"title": "Bad", "risk_level": "extreme"})
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
# =============================================================================
@@ -760,7 +760,7 @@ class TestDSFARouteStatusPatch:
f"/api/compliance/dsfa/{created['id']}/status",
json={"status": "bogus"},
)
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
def test_patch_status_not_found(self):
resp = client.patch(
@@ -810,7 +810,7 @@ class TestDSFARouteSectionUpdate:
f"/api/compliance/dsfa/{created['id']}/sections/9",
json={"content": "X"},
)
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
def test_update_section_not_found(self):
resp = client.put(
@@ -839,7 +839,7 @@ class TestDSFARouteWorkflow:
client.post(f"/api/compliance/dsfa/{created['id']}/submit-for-review")
# Try to submit again (already in-review)
resp = client.post(f"/api/compliance/dsfa/{created['id']}/submit-for-review")
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
def test_submit_not_found(self):
resp = client.post(f"/api/compliance/dsfa/{uuid.uuid4()}/submit-for-review")
@@ -871,7 +871,7 @@ class TestDSFARouteWorkflow:
f"/api/compliance/dsfa/{created['id']}/approve",
json={"approved": True},
)
assert resp.status_code == 422
assert resp.status_code == 400 # ValidationError -> 400
def test_approve_not_found(self):
resp = client.post(