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:
@@ -4,7 +4,9 @@ FastAPI routes for Company Profile CRUD with audit logging.
|
||||
Endpoints:
|
||||
- GET /v1/company-profile: Get company profile for a tenant
|
||||
- POST /v1/company-profile: Create or update company profile
|
||||
- DELETE /v1/company-profile: Delete company profile
|
||||
- GET /v1/company-profile/audit: Get audit log for a tenant
|
||||
- GET /v1/company-profile/template-context: Flat dict for template substitution
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -51,6 +53,17 @@ class CompanyProfileRequest(BaseModel):
|
||||
legal_contact_email: Optional[str] = None
|
||||
machine_builder: Optional[dict] = None
|
||||
is_complete: bool = False
|
||||
# Phase 2 fields
|
||||
repos: list[dict] = []
|
||||
document_sources: list[dict] = []
|
||||
processing_systems: list[dict] = []
|
||||
ai_systems: list[dict] = []
|
||||
technical_contacts: list[dict] = []
|
||||
subject_to_nis2: bool = False
|
||||
subject_to_ai_act: bool = False
|
||||
subject_to_iso27001: bool = False
|
||||
supervisory_authority: Optional[str] = None
|
||||
review_cycle_months: int = 12
|
||||
|
||||
|
||||
class CompanyProfileResponse(BaseModel):
|
||||
@@ -84,6 +97,17 @@ class CompanyProfileResponse(BaseModel):
|
||||
completed_at: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
# Phase 2 fields
|
||||
repos: list[dict] = []
|
||||
document_sources: list[dict] = []
|
||||
processing_systems: list[dict] = []
|
||||
ai_systems: list[dict] = []
|
||||
technical_contacts: list[dict] = []
|
||||
subject_to_nis2: bool = False
|
||||
subject_to_ai_act: bool = False
|
||||
subject_to_iso27001: bool = False
|
||||
supervisory_authority: Optional[str] = None
|
||||
review_cycle_months: int = 12
|
||||
|
||||
|
||||
class AuditEntryResponse(BaseModel):
|
||||
@@ -99,6 +123,22 @@ class AuditListResponse(BaseModel):
|
||||
total: int
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SQL column lists — keep in sync with SELECT/INSERT
|
||||
# =============================================================================
|
||||
|
||||
_BASE_COLUMNS = """id, tenant_id, company_name, legal_form, industry, founded_year,
|
||||
business_model, offerings, company_size, employee_count, annual_revenue,
|
||||
headquarters_country, headquarters_city, has_international_locations,
|
||||
international_countries, target_markets, primary_jurisdiction,
|
||||
is_data_controller, is_data_processor, uses_ai, ai_use_cases,
|
||||
dpo_name, dpo_email, legal_contact_name, legal_contact_email,
|
||||
machine_builder, is_complete, completed_at, created_at, updated_at,
|
||||
repos, document_sources, processing_systems, ai_systems, technical_contacts,
|
||||
subject_to_nis2, subject_to_ai_act, subject_to_iso27001,
|
||||
supervisory_authority, review_cycle_months"""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPERS
|
||||
# =============================================================================
|
||||
@@ -136,6 +176,17 @@ def row_to_response(row) -> CompanyProfileResponse:
|
||||
completed_at=str(row[27]) if row[27] else None,
|
||||
created_at=str(row[28]),
|
||||
updated_at=str(row[29]),
|
||||
# Phase 2 fields (indices 30-39)
|
||||
repos=row[30] if isinstance(row[30], list) else [],
|
||||
document_sources=row[31] if isinstance(row[31], list) else [],
|
||||
processing_systems=row[32] if isinstance(row[32], list) else [],
|
||||
ai_systems=row[33] if isinstance(row[33], list) else [],
|
||||
technical_contacts=row[34] if isinstance(row[34], list) else [],
|
||||
subject_to_nis2=row[35] or False,
|
||||
subject_to_ai_act=row[36] or False,
|
||||
subject_to_iso27001=row[37] or False,
|
||||
supervisory_authority=row[38],
|
||||
review_cycle_months=row[39] or 12,
|
||||
)
|
||||
|
||||
|
||||
@@ -171,14 +222,7 @@ async def get_company_profile(
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = db.execute(
|
||||
"""SELECT id, tenant_id, company_name, legal_form, industry, founded_year,
|
||||
business_model, offerings, company_size, employee_count, annual_revenue,
|
||||
headquarters_country, headquarters_city, has_international_locations,
|
||||
international_countries, target_markets, primary_jurisdiction,
|
||||
is_data_controller, is_data_processor, uses_ai, ai_use_cases,
|
||||
dpo_name, dpo_email, legal_contact_name, legal_contact_email,
|
||||
machine_builder, is_complete, completed_at, created_at, updated_at
|
||||
FROM compliance_company_profiles WHERE tenant_id = :tenant_id""",
|
||||
f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tenant_id",
|
||||
{"tenant_id": tid},
|
||||
)
|
||||
row = result.fetchone()
|
||||
@@ -218,14 +262,21 @@ async def upsert_company_profile(
|
||||
international_countries, target_markets, primary_jurisdiction,
|
||||
is_data_controller, is_data_processor, uses_ai, ai_use_cases,
|
||||
dpo_name, dpo_email, legal_contact_name, legal_contact_email,
|
||||
machine_builder, is_complete)
|
||||
machine_builder, is_complete,
|
||||
repos, document_sources, processing_systems, ai_systems, technical_contacts,
|
||||
subject_to_nis2, subject_to_ai_act, subject_to_iso27001,
|
||||
supervisory_authority, review_cycle_months)
|
||||
VALUES (:tid, :company_name, :legal_form, :industry, :founded_year,
|
||||
:business_model, :offerings::jsonb, :company_size, :employee_count, :annual_revenue,
|
||||
:hq_country, :hq_city, :has_intl, :intl_countries::jsonb,
|
||||
:target_markets::jsonb, :jurisdiction,
|
||||
:is_controller, :is_processor, :uses_ai, :ai_use_cases::jsonb,
|
||||
:dpo_name, :dpo_email, :legal_name, :legal_email,
|
||||
:machine_builder::jsonb, :is_complete)
|
||||
:machine_builder::jsonb, :is_complete,
|
||||
:repos::jsonb, :document_sources::jsonb, :processing_systems::jsonb,
|
||||
:ai_systems::jsonb, :technical_contacts::jsonb,
|
||||
:subject_to_nis2, :subject_to_ai_act, :subject_to_iso27001,
|
||||
:supervisory_authority, :review_cycle_months)
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
company_name = EXCLUDED.company_name,
|
||||
legal_form = EXCLUDED.legal_form,
|
||||
@@ -252,6 +303,16 @@ async def upsert_company_profile(
|
||||
legal_contact_email = EXCLUDED.legal_contact_email,
|
||||
machine_builder = EXCLUDED.machine_builder,
|
||||
is_complete = EXCLUDED.is_complete,
|
||||
repos = EXCLUDED.repos,
|
||||
document_sources = EXCLUDED.document_sources,
|
||||
processing_systems = EXCLUDED.processing_systems,
|
||||
ai_systems = EXCLUDED.ai_systems,
|
||||
technical_contacts = EXCLUDED.technical_contacts,
|
||||
subject_to_nis2 = EXCLUDED.subject_to_nis2,
|
||||
subject_to_ai_act = EXCLUDED.subject_to_ai_act,
|
||||
subject_to_iso27001 = EXCLUDED.subject_to_iso27001,
|
||||
supervisory_authority = EXCLUDED.supervisory_authority,
|
||||
review_cycle_months = EXCLUDED.review_cycle_months,
|
||||
updated_at = NOW()
|
||||
{completed_at_clause}""",
|
||||
{
|
||||
@@ -281,6 +342,16 @@ async def upsert_company_profile(
|
||||
"legal_email": profile.legal_contact_email,
|
||||
"machine_builder": json.dumps(profile.machine_builder) if profile.machine_builder else None,
|
||||
"is_complete": profile.is_complete,
|
||||
"repos": json.dumps(profile.repos),
|
||||
"document_sources": json.dumps(profile.document_sources),
|
||||
"processing_systems": json.dumps(profile.processing_systems),
|
||||
"ai_systems": json.dumps(profile.ai_systems),
|
||||
"technical_contacts": json.dumps(profile.technical_contacts),
|
||||
"subject_to_nis2": profile.subject_to_nis2,
|
||||
"subject_to_ai_act": profile.subject_to_ai_act,
|
||||
"subject_to_iso27001": profile.subject_to_iso27001,
|
||||
"supervisory_authority": profile.supervisory_authority,
|
||||
"review_cycle_months": profile.review_cycle_months,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -291,14 +362,7 @@ async def upsert_company_profile(
|
||||
|
||||
# Fetch and return
|
||||
result = db.execute(
|
||||
"""SELECT id, tenant_id, company_name, legal_form, industry, founded_year,
|
||||
business_model, offerings, company_size, employee_count, annual_revenue,
|
||||
headquarters_country, headquarters_city, has_international_locations,
|
||||
international_countries, target_markets, primary_jurisdiction,
|
||||
is_data_controller, is_data_processor, uses_ai, ai_use_cases,
|
||||
dpo_name, dpo_email, legal_contact_name, legal_contact_email,
|
||||
machine_builder, is_complete, completed_at, created_at, updated_at
|
||||
FROM compliance_company_profiles WHERE tenant_id = :tid""",
|
||||
f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid",
|
||||
{"tid": tid},
|
||||
)
|
||||
row = result.fetchone()
|
||||
@@ -347,6 +411,68 @@ async def delete_company_profile(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/template-context")
|
||||
async def get_template_context(
|
||||
tenant_id: str = "default",
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
):
|
||||
"""Return flat dict for Jinja2 template substitution in document generation."""
|
||||
tid = x_tenant_id or tenant_id
|
||||
db = SessionLocal()
|
||||
try:
|
||||
result = db.execute(
|
||||
f"SELECT {_BASE_COLUMNS} FROM compliance_company_profiles WHERE tenant_id = :tid",
|
||||
{"tid": tid},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Company profile not found — fill Stammdaten first")
|
||||
|
||||
resp = row_to_response(row)
|
||||
# Build flat context dict for templates
|
||||
ctx = {
|
||||
"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 "",
|
||||
"legal_contact_name": resp.legal_contact_name or "",
|
||||
"legal_contact_email": resp.legal_contact_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,
|
||||
# Lists as-is for iteration in templates
|
||||
"offerings": resp.offerings,
|
||||
"target_markets": resp.target_markets,
|
||||
"international_countries": resp.international_countries,
|
||||
"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,
|
||||
# Derived helper values
|
||||
"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,
|
||||
}
|
||||
return ctx
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/audit", response_model=AuditListResponse)
|
||||
async def get_audit_log(
|
||||
tenant_id: str = "default",
|
||||
|
||||
Reference in New Issue
Block a user