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
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:
186
backend-compliance/compliance/api/generation_routes.py
Normal file
186
backend-compliance/compliance/api/generation_routes.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
FastAPI routes for Document Generation from Stammdaten.
|
||||
|
||||
Endpoints:
|
||||
GET /generation/preview/{doc_type} — Markdown preview from Stammdaten
|
||||
POST /generation/apply/{doc_type} — Generate drafts → create Change-Requests
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from classroom_engine.database import get_db
|
||||
from .tenant_utils import get_tenant_id
|
||||
from .document_templates import (
|
||||
generate_dsfa_draft,
|
||||
generate_vvt_drafts,
|
||||
generate_loeschfristen_drafts,
|
||||
generate_tom_drafts,
|
||||
generate_obligation_drafts,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/generation", tags=["generation"])
|
||||
|
||||
VALID_DOC_TYPES = {"dsfa", "vvt", "tom", "loeschfristen", "obligation"}
|
||||
|
||||
|
||||
def _get_template_context(db, tid: str) -> dict:
|
||||
"""Fetch company profile and build template context."""
|
||||
from .company_profile_routes import _BASE_COLUMNS, row_to_response
|
||||
from database import SessionLocal
|
||||
|
||||
# Use a fresh session for company_profile (different DB import pattern)
|
||||
cp_db = SessionLocal()
|
||||
try:
|
||||
result = cp_db.execute(
|
||||
f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid",
|
||||
{"tid": tid},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
resp = row_to_response(row)
|
||||
# Build flat context
|
||||
return {
|
||||
"company_name": resp.company_name,
|
||||
"legal_form": resp.legal_form,
|
||||
"industry": resp.industry,
|
||||
"business_model": resp.business_model,
|
||||
"company_size": resp.company_size,
|
||||
"employee_count": resp.employee_count,
|
||||
"headquarters_country": resp.headquarters_country,
|
||||
"headquarters_city": resp.headquarters_city,
|
||||
"primary_jurisdiction": resp.primary_jurisdiction,
|
||||
"is_data_controller": resp.is_data_controller,
|
||||
"is_data_processor": resp.is_data_processor,
|
||||
"uses_ai": resp.uses_ai,
|
||||
"dpo_name": resp.dpo_name or "",
|
||||
"dpo_email": resp.dpo_email or "",
|
||||
"supervisory_authority": resp.supervisory_authority or "",
|
||||
"review_cycle_months": resp.review_cycle_months,
|
||||
"subject_to_nis2": resp.subject_to_nis2,
|
||||
"subject_to_ai_act": resp.subject_to_ai_act,
|
||||
"subject_to_iso27001": resp.subject_to_iso27001,
|
||||
"offerings": resp.offerings,
|
||||
"target_markets": resp.target_markets,
|
||||
"ai_use_cases": resp.ai_use_cases,
|
||||
"repos": resp.repos,
|
||||
"document_sources": resp.document_sources,
|
||||
"processing_systems": resp.processing_systems,
|
||||
"ai_systems": resp.ai_systems,
|
||||
"technical_contacts": resp.technical_contacts,
|
||||
"has_ai_systems": len(resp.ai_systems) > 0,
|
||||
"processing_system_count": len(resp.processing_systems),
|
||||
"ai_system_count": len(resp.ai_systems),
|
||||
"is_complete": resp.is_complete,
|
||||
}
|
||||
finally:
|
||||
cp_db.close()
|
||||
|
||||
|
||||
def _generate_for_type(doc_type: str, ctx: dict):
|
||||
"""Call the appropriate template generator."""
|
||||
if doc_type == "dsfa":
|
||||
return [generate_dsfa_draft(ctx)]
|
||||
elif doc_type == "vvt":
|
||||
return generate_vvt_drafts(ctx)
|
||||
elif doc_type == "tom":
|
||||
return generate_tom_drafts(ctx)
|
||||
elif doc_type == "loeschfristen":
|
||||
return generate_loeschfristen_drafts(ctx)
|
||||
elif doc_type == "obligation":
|
||||
return generate_obligation_drafts(ctx)
|
||||
else:
|
||||
raise ValueError(f"Unknown doc_type: {doc_type}")
|
||||
|
||||
|
||||
@router.get("/preview/{doc_type}")
|
||||
async def preview_generation(
|
||||
doc_type: str,
|
||||
tid: str = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Preview what documents would be generated (no DB writes)."""
|
||||
if doc_type not in VALID_DOC_TYPES:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}")
|
||||
|
||||
ctx = _get_template_context(db, tid)
|
||||
if not ctx:
|
||||
raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first")
|
||||
|
||||
drafts = _generate_for_type(doc_type, ctx)
|
||||
|
||||
return {
|
||||
"doc_type": doc_type,
|
||||
"count": len(drafts),
|
||||
"drafts": drafts,
|
||||
"company_name": ctx.get("company_name"),
|
||||
"is_preview": True,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apply/{doc_type}")
|
||||
async def apply_generation(
|
||||
doc_type: str,
|
||||
tid: str = Depends(get_tenant_id),
|
||||
db: Session = Depends(get_db),
|
||||
x_user_id: Optional[str] = Header(None, alias="X-User-ID"),
|
||||
):
|
||||
"""Generate drafts and create Change-Requests for each.
|
||||
|
||||
Does NOT create documents directly — all go through the CR inbox.
|
||||
"""
|
||||
if doc_type not in VALID_DOC_TYPES:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid doc_type: {doc_type}. Valid: {VALID_DOC_TYPES}")
|
||||
|
||||
ctx = _get_template_context(db, tid)
|
||||
if not ctx:
|
||||
raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first")
|
||||
|
||||
drafts = _generate_for_type(doc_type, ctx)
|
||||
user = x_user_id or "system"
|
||||
|
||||
cr_ids = []
|
||||
for draft in drafts:
|
||||
title = draft.get("title") or draft.get("name") or draft.get("data_category") or f"Neues {doc_type}-Dokument"
|
||||
try:
|
||||
result = db.execute(
|
||||
text("""
|
||||
INSERT INTO compliance_change_requests
|
||||
(tenant_id, trigger_type, target_document_type,
|
||||
proposal_title, proposal_body, proposed_changes,
|
||||
priority, created_by)
|
||||
VALUES (:tid, 'generation', :doc_type,
|
||||
:title, :body, CAST(:changes AS jsonb),
|
||||
'normal', :user)
|
||||
RETURNING id
|
||||
"""),
|
||||
{
|
||||
"tid": tid,
|
||||
"doc_type": doc_type,
|
||||
"title": f"[Generiert] {title}",
|
||||
"body": f"Automatisch aus Stammdaten generiert für {ctx.get('company_name', '')}",
|
||||
"changes": json.dumps(draft),
|
||||
"user": user,
|
||||
},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
cr_ids.append(str(row[0]))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create CR for draft: {e}")
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"doc_type": doc_type,
|
||||
"drafts_generated": len(drafts),
|
||||
"change_requests_created": len(cr_ids),
|
||||
"change_request_ids": cr_ids,
|
||||
}
|
||||
Reference in New Issue
Block a user