feat(sdk): vendor-compliance cross-module integration — VVT, obligations, TOM, loeschfristen

Integrate the vendor-compliance module with four DSGVO modules to eliminate
data silos and resolve the VVT processor tab's ephemeral state problem.

- Reposition vendor-compliance sidebar from seq 4200 to 2500 (after VVT)
- VVT: replace ephemeral ProcessorRecord state with Vendor-API fetch (read-only)
- Obligations: add linked_vendor_ids (JSONB) + compliance check #12 MISSING_VENDOR_LINK
- TOM: add vendor TOM-controls cross-reference table in overview tab
- Loeschfristen: add linked_vendor_ids (JSONB) + vendor picker + document section
- Migrations: 069_obligations_vendor_link.sql, 070_loeschfristen_vendor_link.sql
- Tests: 12 new backend tests (125 total pass)
- Docs: update obligations.md + vendors.md with cross-module integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-19 13:59:43 +01:00
parent 4b1eede45b
commit c3afa628ed
19 changed files with 2852 additions and 421 deletions

View File

@@ -56,6 +56,7 @@ class LoeschfristCreate(BaseModel):
responsible_person: Optional[str] = None
release_process: Optional[str] = None
linked_vvt_activity_ids: Optional[List[Any]] = None
linked_vendor_ids: Optional[List[Any]] = None
status: str = "DRAFT"
last_review_date: Optional[datetime] = None
next_review_date: Optional[datetime] = None
@@ -86,6 +87,7 @@ class LoeschfristUpdate(BaseModel):
responsible_person: Optional[str] = None
release_process: Optional[str] = None
linked_vvt_activity_ids: Optional[List[Any]] = None
linked_vendor_ids: Optional[List[Any]] = None
status: Optional[str] = None
last_review_date: Optional[datetime] = None
next_review_date: Optional[datetime] = None
@@ -100,7 +102,7 @@ class StatusUpdate(BaseModel):
# JSONB fields that need CAST
JSONB_FIELDS = {
"affected_groups", "data_categories", "legal_holds",
"storage_locations", "linked_vvt_activity_ids", "tags"
"storage_locations", "linked_vvt_activity_ids", "linked_vendor_ids", "tags"
}

View File

@@ -42,6 +42,7 @@ class ObligationCreate(BaseModel):
priority: str = "medium"
responsible: Optional[str] = None
linked_systems: Optional[List[str]] = None
linked_vendor_ids: Optional[List[str]] = None
assessment_id: Optional[str] = None
rule_code: Optional[str] = None
notes: Optional[str] = None
@@ -57,6 +58,7 @@ class ObligationUpdate(BaseModel):
priority: Optional[str] = None
responsible: Optional[str] = None
linked_systems: Optional[List[str]] = None
linked_vendor_ids: Optional[List[str]] = None
notes: Optional[str] = None
@@ -173,14 +175,15 @@ async def create_obligation(
import json
linked_systems = json.dumps(payload.linked_systems or [])
linked_vendor_ids = json.dumps(payload.linked_vendor_ids or [])
row = db.execute(text("""
INSERT INTO compliance_obligations
(tenant_id, title, description, source, source_article, deadline,
status, priority, responsible, linked_systems, assessment_id, rule_code, notes)
status, priority, responsible, linked_systems, linked_vendor_ids, assessment_id, rule_code, notes)
VALUES
(:tenant_id, :title, :description, :source, :source_article, :deadline,
:status, :priority, :responsible, CAST(:linked_systems AS jsonb), :assessment_id, :rule_code, :notes)
:status, :priority, :responsible, CAST(:linked_systems AS jsonb), CAST(:linked_vendor_ids AS jsonb), :assessment_id, :rule_code, :notes)
RETURNING *
"""), {
"tenant_id": tenant_id,
@@ -193,6 +196,7 @@ async def create_obligation(
"priority": payload.priority,
"responsible": payload.responsible,
"linked_systems": linked_systems,
"linked_vendor_ids": linked_vendor_ids,
"assessment_id": payload.assessment_id,
"rule_code": payload.rule_code,
"notes": payload.notes,
@@ -235,6 +239,9 @@ async def update_obligation(
if field == "linked_systems":
updates["linked_systems"] = json.dumps(value or [])
set_clauses.append("linked_systems = CAST(:linked_systems AS jsonb)")
elif field == "linked_vendor_ids":
updates["linked_vendor_ids"] = json.dumps(value or [])
set_clauses.append("linked_vendor_ids = CAST(:linked_vendor_ids AS jsonb)")
else:
updates[field] = value
set_clauses.append(f"{field} = :{field}")