feat(sdk): Multi-Tenancy, Versionierung, Change-Requests, Dokumentengenerierung (Phase 1-6)
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s

6-Phasen-Implementation fuer cloud-faehiges, mandantenfaehiges Compliance SDK:

Phase 1: Multi-Tenancy Fix
- Shared tenant_utils.py Dependency (UUID-Validierung, kein "default" mehr)
- VVT tenant_id Column + tenant-scoped Queries
- DSFA/Vendor DEFAULT_TENANT_ID von "default" auf UUID migriert
- Migration 035

Phase 2: Stammdaten-Erweiterung
- Company Profile um JSONB-Felder erweitert (processing_systems, ai_systems, technical_contacts)
- Regulierungs-Flags (NIS2, AI Act, ISO 27001)
- GET /template-context Endpoint
- Migration 036

Phase 3: Dokument-Versionierung
- 5 Versions-Tabellen (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Shared versioning_utils.py Helper
- /{id}/versions Endpoints auf allen 5 Dokumenttypen
- Migration 037

Phase 4: Change-Request System
- Zentrale CR-Inbox mit CRUD + Accept/Reject/Edit Workflow
- Regelbasierte CR-Engine (VVT DPIA → DSFA CR, Datenkategorien → Loeschfristen CR)
- Audit-Trail
- Migration 038

Phase 5: Dokumentengenerierung
- 5 Template-Generatoren (DSFA, VVT, TOM, Loeschfristen, Obligations)
- Preview + Apply Endpoints (erzeugt CRs, keine direkten Dokumente)

Phase 6: Frontend-Integration
- Change-Request Inbox Page mit Stats, Filtern, Modals
- VersionHistory Timeline-Komponente
- SDKSidebar CR-Badge (60s Polling)
- Company Profile: 2 neue Wizard-Steps + "Dokumente generieren" CTA

Docs: 5 neue MkDocs-Seiten, CLAUDE.md aktualisiert
Tests: 97 neue Tests (alle bestanden)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-07 14:12:34 +01:00
parent ef9aed666f
commit 1e84df9769
41 changed files with 4818 additions and 52 deletions

View File

@@ -33,6 +33,7 @@ from .schemas import (
VVTActivityCreate, VVTActivityUpdate, VVTActivityResponse,
VVTStatsResponse, VVTAuditLogEntry,
)
from .tenant_utils import get_tenant_id
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/vvt", tags=["compliance-vvt"])
@@ -40,6 +41,7 @@ router = APIRouter(prefix="/vvt", tags=["compliance-vvt"])
def _log_audit(
db: Session,
tenant_id: str,
action: str,
entity_type: str,
entity_id=None,
@@ -48,6 +50,7 @@ def _log_audit(
new_values=None,
):
entry = VVTAuditLogDB(
tenant_id=tenant_id,
action=action,
entity_type=entity_type,
entity_id=entity_id,
@@ -63,9 +66,17 @@ def _log_audit(
# ============================================================================
@router.get("/organization", response_model=Optional[VVTOrganizationResponse])
async def get_organization(db: Session = Depends(get_db)):
"""Load the VVT organization header (single record)."""
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
async def get_organization(
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Load the VVT organization header for the given tenant."""
org = (
db.query(VVTOrganizationDB)
.filter(VVTOrganizationDB.tenant_id == tid)
.order_by(VVTOrganizationDB.created_at)
.first()
)
if not org:
return None
return VVTOrganizationResponse(
@@ -88,15 +99,22 @@ async def get_organization(db: Session = Depends(get_db)):
@router.put("/organization", response_model=VVTOrganizationResponse)
async def upsert_organization(
request: VVTOrganizationUpdate,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Create or update the VVT organization header."""
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
org = (
db.query(VVTOrganizationDB)
.filter(VVTOrganizationDB.tenant_id == tid)
.order_by(VVTOrganizationDB.created_at)
.first()
)
if not org:
data = request.dict(exclude_none=True)
if 'organization_name' not in data:
data['organization_name'] = 'Meine Organisation'
data['tenant_id'] = tid
org = VVTOrganizationDB(**data)
db.add(org)
else:
@@ -168,10 +186,11 @@ async def list_activities(
business_function: Optional[str] = Query(None),
search: Optional[str] = Query(None),
review_overdue: Optional[bool] = Query(None),
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""List all processing activities with optional filters."""
query = db.query(VVTActivityDB)
query = db.query(VVTActivityDB).filter(VVTActivityDB.tenant_id == tid)
if status:
query = query.filter(VVTActivityDB.status == status)
@@ -199,12 +218,14 @@ async def list_activities(
async def create_activity(
request: VVTActivityCreate,
http_request: Request,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Create a new processing activity."""
# Check for duplicate vvt_id
# Check for duplicate vvt_id within tenant
existing = db.query(VVTActivityDB).filter(
VVTActivityDB.vvt_id == request.vvt_id
VVTActivityDB.tenant_id == tid,
VVTActivityDB.vvt_id == request.vvt_id,
).first()
if existing:
raise HTTPException(
@@ -213,6 +234,7 @@ async def create_activity(
)
data = request.dict()
data['tenant_id'] = tid
# Set created_by from X-User-ID header if not provided in body
if not data.get('created_by'):
data['created_by'] = http_request.headers.get('X-User-ID', 'system')
@@ -223,6 +245,7 @@ async def create_activity(
_log_audit(
db,
tenant_id=tid,
action="CREATE",
entity_type="activity",
entity_id=act.id,
@@ -235,9 +258,16 @@ async def create_activity(
@router.get("/activities/{activity_id}", response_model=VVTActivityResponse)
async def get_activity(activity_id: str, db: Session = Depends(get_db)):
async def get_activity(
activity_id: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Get a single processing activity by ID."""
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
act = db.query(VVTActivityDB).filter(
VVTActivityDB.id == activity_id,
VVTActivityDB.tenant_id == tid,
).first()
if not act:
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
return _activity_to_response(act)
@@ -247,10 +277,14 @@ async def get_activity(activity_id: str, db: Session = Depends(get_db)):
async def update_activity(
activity_id: str,
request: VVTActivityUpdate,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Update a processing activity."""
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
act = db.query(VVTActivityDB).filter(
VVTActivityDB.id == activity_id,
VVTActivityDB.tenant_id == tid,
).first()
if not act:
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
@@ -262,6 +296,7 @@ async def update_activity(
_log_audit(
db,
tenant_id=tid,
action="UPDATE",
entity_type="activity",
entity_id=act.id,
@@ -275,14 +310,22 @@ async def update_activity(
@router.delete("/activities/{activity_id}")
async def delete_activity(activity_id: str, db: Session = Depends(get_db)):
async def delete_activity(
activity_id: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Delete a processing activity."""
act = db.query(VVTActivityDB).filter(VVTActivityDB.id == activity_id).first()
act = db.query(VVTActivityDB).filter(
VVTActivityDB.id == activity_id,
VVTActivityDB.tenant_id == tid,
).first()
if not act:
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
_log_audit(
db,
tenant_id=tid,
action="DELETE",
entity_type="activity",
entity_id=act.id,
@@ -302,11 +345,13 @@ async def delete_activity(activity_id: str, db: Session = Depends(get_db)):
async def get_audit_log(
limit: int = Query(50, ge=1, le=500),
offset: int = Query(0, ge=0),
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Get the VVT audit trail."""
entries = (
db.query(VVTAuditLogDB)
.filter(VVTAuditLogDB.tenant_id == tid)
.order_by(VVTAuditLogDB.created_at.desc())
.offset(offset)
.limit(limit)
@@ -334,14 +379,26 @@ async def get_audit_log(
@router.get("/export")
async def export_activities(
format: str = Query("json", pattern="^(json|csv)$"),
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Export all activities as JSON or CSV (semicolon-separated, DE locale)."""
org = db.query(VVTOrganizationDB).order_by(VVTOrganizationDB.created_at).first()
activities = db.query(VVTActivityDB).order_by(VVTActivityDB.created_at).all()
org = (
db.query(VVTOrganizationDB)
.filter(VVTOrganizationDB.tenant_id == tid)
.order_by(VVTOrganizationDB.created_at)
.first()
)
activities = (
db.query(VVTActivityDB)
.filter(VVTActivityDB.tenant_id == tid)
.order_by(VVTActivityDB.created_at)
.all()
)
_log_audit(
db,
tenant_id=tid,
action="EXPORT",
entity_type="all_activities",
new_values={"count": len(activities), "format": format},
@@ -432,9 +489,12 @@ def _export_csv(activities: list) -> StreamingResponse:
@router.get("/stats", response_model=VVTStatsResponse)
async def get_stats(db: Session = Depends(get_db)):
async def get_stats(
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Get VVT statistics summary."""
activities = db.query(VVTActivityDB).all()
activities = db.query(VVTActivityDB).filter(VVTActivityDB.tenant_id == tid).all()
by_status: dict = {}
by_bf: dict = {}
@@ -459,3 +519,33 @@ async def get_stats(db: Session = Depends(get_db)):
approved_count=by_status.get('APPROVED', 0),
overdue_review_count=overdue_count,
)
# ============================================================================
# Versioning
# ============================================================================
@router.get("/activities/{activity_id}/versions")
async def list_activity_versions(
activity_id: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""List all versions for a VVT activity."""
from .versioning_utils import list_versions
return list_versions(db, "vvt_activity", activity_id, tid)
@router.get("/activities/{activity_id}/versions/{version_number}")
async def get_activity_version(
activity_id: str,
version_number: int,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Get a specific VVT activity version with full snapshot."""
from .versioning_utils import get_version
v = get_version(db, "vvt_activity", activity_id, version_number, tid)
if not v:
raise HTTPException(status_code=404, detail=f"Version {version_number} not found")
return v