feat: Legal Templates — Attribution-Tracking + 6 neue Templates (DE/EN)
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 47s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s

Migration 019: 5 neue Herkunftsspalten (source_url, source_repo,
source_file_path, source_retrieved_at, attribution_text, inspiration_sources)
ermöglichen lückenlosen Nachweis jeder Template-Quelle.

Neue Templates:
  DE: AVV (Art. 28 DSGVO), Widerrufsbelehrung (EGBGB Anlage 1, §5 UrhG),
      Cookie-Richtlinie (TTDSG §25)
  EN: Privacy Policy (GDPR), Terms of Service (EU Directive 2011/83),
      Data Processing Agreement (GDPR Art. 28)

Gesamt: 9 Templates — 5 DE, 4 EN | 6 document_type-Werte

- VALID_DOCUMENT_TYPES um 3 neue Typen erweitert
- Create/Update-Schemas: attribution fields ergänzt
- Status-Endpoint: alle 6 Typen in by_type
- Tests: 34/34 — alle grün

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 09:00:25 +01:00
parent f909182632
commit d454acceff
4 changed files with 906 additions and 17 deletions

View File

@@ -31,7 +31,14 @@ router = APIRouter(prefix="/legal-templates", tags=["legal-templates"])
DEFAULT_TENANT_ID = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e"
VALID_DOCUMENT_TYPES = {"privacy_policy", "terms_of_service", "impressum"}
VALID_DOCUMENT_TYPES = {
"privacy_policy",
"terms_of_service",
"impressum",
"data_processing_agreement",
"withdrawal_policy",
"cookie_policy",
}
VALID_STATUSES = {"published", "draft", "archived"}
@@ -54,6 +61,12 @@ class LegalTemplateCreate(BaseModel):
is_complete_document: bool = True
version: str = "1.0.0"
status: str = "published"
# Attribution / source traceability
source_url: Optional[str] = None
source_repo: Optional[str] = None
source_file_path: Optional[str] = None
attribution_text: Optional[str] = None
inspiration_sources: Optional[List[Any]] = None
class LegalTemplateUpdate(BaseModel):
@@ -71,6 +84,12 @@ class LegalTemplateUpdate(BaseModel):
is_complete_document: Optional[bool] = None
version: Optional[str] = None
status: Optional[str] = None
# Attribution / source traceability
source_url: Optional[str] = None
source_repo: Optional[str] = None
source_file_path: Optional[str] = None
attribution_text: Optional[str] = None
inspiration_sources: Optional[List[Any]] = None
# =============================================================================
@@ -167,13 +186,16 @@ async def get_templates_status(
row = db.execute(text("""
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'published') AS published,
COUNT(*) FILTER (WHERE status = 'draft') AS draft,
COUNT(*) FILTER (WHERE status = 'archived') AS archived,
COUNT(*) FILTER (WHERE document_type = 'privacy_policy') AS privacy_policy,
COUNT(*) FILTER (WHERE document_type = 'terms_of_service') AS terms_of_service,
COUNT(*) FILTER (WHERE document_type = 'impressum') AS impressum
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'published') AS published,
COUNT(*) FILTER (WHERE status = 'draft') AS draft,
COUNT(*) FILTER (WHERE status = 'archived') AS archived,
COUNT(*) FILTER (WHERE document_type = 'privacy_policy') AS privacy_policy,
COUNT(*) FILTER (WHERE document_type = 'terms_of_service') AS terms_of_service,
COUNT(*) FILTER (WHERE document_type = 'impressum') AS impressum,
COUNT(*) FILTER (WHERE document_type = 'data_processing_agreement') AS data_processing_agreement,
COUNT(*) FILTER (WHERE document_type = 'withdrawal_policy') AS withdrawal_policy,
COUNT(*) FILTER (WHERE document_type = 'cookie_policy') AS cookie_policy
FROM compliance_legal_templates
WHERE tenant_id = :tenant_id
"""), {"tenant_id": tenant_id}).fetchone()
@@ -192,6 +214,9 @@ async def get_templates_status(
"privacy_policy": counts["privacy_policy"],
"terms_of_service": counts["terms_of_service"],
"impressum": counts["impressum"],
"data_processing_agreement": counts["data_processing_agreement"],
"withdrawal_policy": counts["withdrawal_policy"],
"cookie_policy": counts["cookie_policy"],
},
}
return {"total": 0, "by_status": {}, "by_type": {}}
@@ -251,18 +276,23 @@ async def create_legal_template(
)
placeholders_json = json.dumps(payload.placeholders or [])
inspiration_json = json.dumps(payload.inspiration_sources or [])
row = db.execute(text("""
INSERT INTO compliance_legal_templates (
tenant_id, document_type, title, description, content,
placeholders, language, jurisdiction,
license_id, license_name, source_name,
attribution_required, is_complete_document, version, status
attribution_required, is_complete_document, version, status,
source_url, source_repo, source_file_path, source_retrieved_at,
attribution_text, inspiration_sources
) VALUES (
:tenant_id, :document_type, :title, :description, :content,
CAST(:placeholders AS jsonb), :language, :jurisdiction,
:license_id, :license_name, :source_name,
:attribution_required, :is_complete_document, :version, :status
:attribution_required, :is_complete_document, :version, :status,
:source_url, :source_repo, :source_file_path, :source_retrieved_at,
:attribution_text, CAST(:inspiration_sources AS jsonb)
) RETURNING *
"""), {
"tenant_id": tenant_id,
@@ -280,6 +310,12 @@ async def create_legal_template(
"is_complete_document": payload.is_complete_document,
"version": payload.version,
"status": payload.status,
"source_url": payload.source_url,
"source_repo": payload.source_repo,
"source_file_path": payload.source_file_path,
"source_retrieved_at": None,
"attribution_text": payload.attribution_text,
"inspiration_sources": inspiration_json,
}).fetchone()
db.commit()
return _row_to_dict(row)
@@ -311,8 +347,9 @@ async def update_legal_template(
"updated_at": datetime.utcnow(),
}
jsonb_fields = {"placeholders", "inspiration_sources"}
for field, value in updates.items():
if field == "placeholders":
if field in jsonb_fields:
params[field] = json.dumps(value if value is not None else [])
set_clauses.append(f"{field} = CAST(:{field} AS jsonb)")
else: