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
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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user