feat: Template-Spec v1 Phase C — IF-Renderer + HOSTING/FEATURES + 4 neue DE-Templates
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 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s

- contextBridge.ts: HostingCtx + FeaturesCtx (35 Felder), ~50 neue Platzhalter-Aliases
- ruleEngine.ts: buildBoolContext() + applyConditionalBlocks() (IF/IF_NOT/IF_ANY)
- ruleEngine.test.ts: 67 Tests (+18 für Phase C), alle grün
- page.tsx: IF-Renderer in Pipeline, HOSTING+FEATURES Formular-Sections, erweiterter SDK-Prefill
- scripts/apply_templates_023.py: 4 neue DE-Templates (Cookie v2, DSE, AGB, Impressum)
- migrations/023_new_templates_de.sql: Dokumentation + Verifikations-Query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 14:35:56 +01:00
parent 7f3bf93cd6
commit e0f7f2134e
6 changed files with 1208 additions and 10 deletions

View File

@@ -112,6 +112,61 @@ export interface ConsentCtx {
MARKETING_PARTNERS: string | null
}
export interface HostingCtx {
PROVIDER_NAME: string
COUNTRY: string
CONTRACT_TYPE: string // e.g. "AVV nach Art. 28 DSGVO"
}
export interface FeaturesCtx {
// ── Impressum ──────────────────────────────────────────────────────────────
HAS_REGULATED_PROFESSION: boolean
REGULATED_PROFESSION_TEXT: string
HAS_EDITORIAL_RESPONSIBLE: boolean
EDITORIAL_RESPONSIBLE_NAME: string
EDITORIAL_RESPONSIBLE_ADDRESS: string
HAS_DISPUTE_RESOLUTION: boolean
DISPUTE_RESOLUTION_TEXT: string
// ── DSI / Datenschutzerklärung ─────────────────────────────────────────────
HAS_THIRD_COUNTRY: boolean
TRANSFER_GUARDS: string // e.g. "EU-Standardvertragsklauseln (SCC)"
SECURITY_MEASURES_SUMMARY: string // e.g. "TLS, RBAC, Backups, Logging"
DATA_SUBJECT_REQUEST_CHANNEL: string // e.g. "per E-Mail an {{CONTACT_EMAIL}}"
CONSENT_WITHDRAWAL_PATH: string // e.g. "Footer-Link Cookie-Einstellungen"
// ── Service-Features (DSI/AGB) ─────────────────────────────────────────────
HAS_NEWSLETTER: boolean
NEWSLETTER_PROVIDER_DETAIL: string
HAS_ACCOUNT: boolean
HAS_PAYMENTS: boolean
PAYMENT_PROVIDER_DETAIL: string
HAS_SUPPORT: boolean
HAS_SOCIAL_MEDIA: boolean
SOCIAL_MEDIA_DETAIL: string
// ── Tool-Details (DSI + Cookie Banner) ────────────────────────────────────
ANALYTICS_TOOLS_DETAIL: string
MARKETING_TOOLS_DETAIL: string
CMP_NAME: string
CMP_LOGS_CONSENTS: boolean
HAS_FUNCTIONAL_COOKIES: boolean
// ── AGB (SaaS) ────────────────────────────────────────────────────────────
HAS_PAID_PLANS: boolean
PRICES_TEXT: string
PAYMENT_TERMS_TEXT: string
CONTRACT_TERM_TEXT: string
HAS_SLA: boolean
SLA_URL: string
HAS_EXPORT_POLICY: boolean
EXPORT_POLICY_TEXT: string
LIMITATION_CAP_TEXT: string
HAS_WITHDRAWAL: boolean
CONSUMER_WITHDRAWAL_TEXT: string
SUPPORT_CHANNELS_TEXT: string
}
export interface TemplateContext {
PROVIDER: ProviderCtx
CUSTOMER: CustomerCtx
@@ -123,6 +178,8 @@ export interface TemplateContext {
SECURITY: SecurityCtx
NDA: NDACtx
CONSENT: ConsentCtx
HOSTING: HostingCtx
FEATURES: FeaturesCtx
}
export interface ComputedFlags {
@@ -180,6 +237,32 @@ export const EMPTY_CONTEXT: TemplateContext = {
},
NDA: { PURPOSE: '', DURATION_YEARS: 5, PENALTY_AMOUNT_EUR: null },
CONSENT: { WEBSITE_NAME: '', ANALYTICS_TOOLS: null, MARKETING_PARTNERS: null },
HOSTING: {
PROVIDER_NAME: '', COUNTRY: 'Deutschland', CONTRACT_TYPE: 'AVV nach Art. 28 DSGVO',
},
FEATURES: {
// Impressum
HAS_REGULATED_PROFESSION: false, REGULATED_PROFESSION_TEXT: '',
HAS_EDITORIAL_RESPONSIBLE: false, EDITORIAL_RESPONSIBLE_NAME: '', EDITORIAL_RESPONSIBLE_ADDRESS: '',
HAS_DISPUTE_RESOLUTION: false, DISPUTE_RESOLUTION_TEXT: '',
// DSI
HAS_THIRD_COUNTRY: false, TRANSFER_GUARDS: 'EU-Standardvertragsklauseln (SCC)',
SECURITY_MEASURES_SUMMARY: 'TLS 1.3, RBAC, Verschlüsselung at rest, regelmäßige Backups, Logging',
DATA_SUBJECT_REQUEST_CHANNEL: 'per E-Mail', CONSENT_WITHDRAWAL_PATH: 'Footer-Link „Cookie-Einstellungen"',
// Service features
HAS_NEWSLETTER: false, NEWSLETTER_PROVIDER_DETAIL: '',
HAS_ACCOUNT: false, HAS_PAYMENTS: false, PAYMENT_PROVIDER_DETAIL: '',
HAS_SUPPORT: false, HAS_SOCIAL_MEDIA: false, SOCIAL_MEDIA_DETAIL: '',
// Tools
ANALYTICS_TOOLS_DETAIL: '', MARKETING_TOOLS_DETAIL: '',
CMP_NAME: '', CMP_LOGS_CONSENTS: false, HAS_FUNCTIONAL_COOKIES: false,
// AGB
HAS_PAID_PLANS: false, PRICES_TEXT: '', PAYMENT_TERMS_TEXT: '',
CONTRACT_TERM_TEXT: '', HAS_SLA: false, SLA_URL: '',
HAS_EXPORT_POLICY: false, EXPORT_POLICY_TEXT: '',
LIMITATION_CAP_TEXT: '', HAS_WITHDRAWAL: false, CONSUMER_WITHDRAWAL_TEXT: '',
SUPPORT_CHANNELS_TEXT: '',
},
}
// =============================================================================
@@ -213,6 +296,8 @@ export function contextToPlaceholders(ctx: TemplateContext): Record<string, stri
const sec = ctx.SECURITY
const nda = ctx.NDA
const con = ctx.CONSENT
const h = ctx.HOSTING
const f = ctx.FEATURES
const address = providerAddress(p)
@@ -298,6 +383,57 @@ export function contextToPlaceholders(ctx: TemplateContext): Record<string, stri
'{{WEBSITE_NAME}}': con.WEBSITE_NAME || str(p.WEBSITE_URL),
'{{ANALYTICS_TOOLS}}': str(con.ANALYTICS_TOOLS),
'{{MARKETING_PARTNERS}}': str(con.MARKETING_PARTNERS),
// --- ALIASES (canonical names from new templates) ---
'{{COMPANY_LEGAL_NAME}}': str(p.LEGAL_NAME),
'{{COMPANY_LEGAL_FORM}}': str(p.LEGAL_FORM),
'{{COMPANY_ADDRESS_LINE}}': str(p.ADDRESS_LINE),
'{{COMPANY_POSTAL_CODE}}': str(p.POSTAL_CODE),
'{{COMPANY_CITY}}': str(p.CITY),
'{{COMPANY_COUNTRY}}': str(p.COUNTRY),
'{{COMPANY_ADDRESS_FULL}}': [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' '), p.COUNTRY].filter(Boolean).join(', '),
'{{WEBSITE_URL}}': str(p.WEBSITE_URL),
'{{CONTACT_PHONE}}': str(p.PHONE),
'{{REPRESENTED_BY_NAME}}': str(p.CEO_NAME),
'{{REGISTER_COURT}}': str(p.REGISTER_COURT),
'{{REGISTER_NUMBER}}': str(p.REGISTER_NUMBER),
'{{VAT_ID}}': str(p.VAT_ID),
'{{SERVICE_DESCRIPTION_SHORT}}': str(s.DESCRIPTION),
'{{NOTICE_PERIOD_DAYS}}': str(s.TERMINATION_NOTICE_DAYS),
'{{ANALYTICS_TOOLS_LIST}}': str(con.ANALYTICS_TOOLS),
'{{MARKETING_PARTNERS_LIST}}': str(con.MARKETING_PARTNERS),
'{{DATA_PROCESSING_LOCATIONS}}': str(s.DATA_LOCATION),
'{{SUPERVISORY_AUTHORITY_NAME}}': str(prv.SUPERVISORY_AUTHORITY_NAME),
'{{SUPERVISORY_AUTHORITY_ADDRESS}}': str(prv.SUPERVISORY_AUTHORITY_ADDRESS),
// --- HOSTING ---
'{{HOSTING_PROVIDER_NAME}}': str(h.PROVIDER_NAME),
'{{HOSTING_PROVIDER_COUNTRY}}': str(h.COUNTRY),
'{{HOSTING_PROVIDER_CONTRACT_TYPE}}': str(h.CONTRACT_TYPE),
// --- FEATURES (text fields) ---
'{{CONSENT_WITHDRAWAL_PATH}}': str(f.CONSENT_WITHDRAWAL_PATH),
'{{SECURITY_MEASURES_SUMMARY}}': str(f.SECURITY_MEASURES_SUMMARY),
'{{DATA_SUBJECT_REQUEST_CHANNEL}}': str(f.DATA_SUBJECT_REQUEST_CHANNEL),
'{{TRANSFER_GUARDS}}': str(f.TRANSFER_GUARDS),
'{{REGULATED_PROFESSION_TEXT}}': str(f.REGULATED_PROFESSION_TEXT),
'{{EDITORIAL_RESPONSIBLE_NAME}}': str(f.EDITORIAL_RESPONSIBLE_NAME),
'{{EDITORIAL_RESPONSIBLE_ADDRESS}}': str(f.EDITORIAL_RESPONSIBLE_ADDRESS),
'{{DISPUTE_RESOLUTION_TEXT}}': str(f.DISPUTE_RESOLUTION_TEXT),
'{{NEWSLETTER_PROVIDER_DETAIL}}': str(f.NEWSLETTER_PROVIDER_DETAIL),
'{{PAYMENT_PROVIDER_DETAIL}}': str(f.PAYMENT_PROVIDER_DETAIL),
'{{SOCIAL_MEDIA_DETAIL}}': str(f.SOCIAL_MEDIA_DETAIL),
'{{ANALYTICS_TOOLS_DETAIL}}': str(f.ANALYTICS_TOOLS_DETAIL),
'{{MARKETING_TOOLS_DETAIL}}': str(f.MARKETING_TOOLS_DETAIL),
'{{CMP_NAME}}': str(f.CMP_NAME),
'{{PRICES_TEXT}}': str(f.PRICES_TEXT),
'{{PAYMENT_TERMS_TEXT}}': str(f.PAYMENT_TERMS_TEXT),
'{{CONTRACT_TERM_TEXT}}': str(f.CONTRACT_TERM_TEXT),
'{{SLA_URL}}': str(f.SLA_URL),
'{{EXPORT_POLICY_TEXT}}': str(f.EXPORT_POLICY_TEXT),
'{{LIMITATION_CAP_TEXT}}': str(f.LIMITATION_CAP_TEXT),
'{{CONSUMER_WITHDRAWAL_TEXT}}': str(f.CONSUMER_WITHDRAWAL_TEXT),
'{{SUPPORT_CHANNELS_TEXT}}': str(f.SUPPORT_CHANNELS_TEXT),
}
}
@@ -323,16 +459,18 @@ export function computeFlags(ctx: TemplateContext): ComputedFlags {
/** All placeholder patterns covered by each context section */
const SECTION_COVERS: Record<keyof TemplateContext, string[]> = {
PROVIDER: ['{{COMPANY_NAME}}', '{{PROVIDER_NAME}}', '{{DISCLOSING_PARTY}}', '{{SERVICE_PROVIDER}}', '{{COMPANY_ADDRESS}}', '{{PROVIDER_ADDRESS}}', '{{CONTACT_EMAIL}}', '{{PROVIDER_EMAIL}}', '{{ABUSE_EMAIL}}', '{{REPORT_EMAIL}}'],
PROVIDER: ['{{COMPANY_NAME}}', '{{PROVIDER_NAME}}', '{{DISCLOSING_PARTY}}', '{{SERVICE_PROVIDER}}', '{{COMPANY_ADDRESS}}', '{{PROVIDER_ADDRESS}}', '{{CONTACT_EMAIL}}', '{{PROVIDER_EMAIL}}', '{{ABUSE_EMAIL}}', '{{REPORT_EMAIL}}', '{{COMPANY_LEGAL_NAME}}', '{{COMPANY_LEGAL_FORM}}', '{{COMPANY_ADDRESS_LINE}}', '{{COMPANY_POSTAL_CODE}}', '{{COMPANY_CITY}}', '{{COMPANY_COUNTRY}}', '{{COMPANY_ADDRESS_FULL}}', '{{WEBSITE_URL}}', '{{CONTACT_PHONE}}', '{{REPRESENTED_BY_NAME}}', '{{REGISTER_COURT}}', '{{REGISTER_NUMBER}}', '{{VAT_ID}}'],
CUSTOMER: ['{{CUSTOMER_NAME}}', '{{RECEIVING_PARTY}}', '{{CUSTOMER}}'],
SERVICE: ['{{SERVICE_NAME}}', '{{PLATFORM_NAME}}', '{{SERVICE_DESCRIPTION}}', '{{SERVICE_MODEL}}', '{{SERVICE_TIER}}', '{{DATA_LOCATION}}', '{{EXPORT_FORMATS}}', '{{EXPORT_WINDOW_DAYS}}', '{{MIN_TERM_MONTHS}}', '{{TERMINATION_NOTICE_DAYS}}'],
SERVICE: ['{{SERVICE_NAME}}', '{{PLATFORM_NAME}}', '{{SERVICE_DESCRIPTION}}', '{{SERVICE_MODEL}}', '{{SERVICE_TIER}}', '{{DATA_LOCATION}}', '{{EXPORT_FORMATS}}', '{{EXPORT_WINDOW_DAYS}}', '{{MIN_TERM_MONTHS}}', '{{TERMINATION_NOTICE_DAYS}}', '{{SERVICE_DESCRIPTION_SHORT}}', '{{NOTICE_PERIOD_DAYS}}', '{{DATA_PROCESSING_LOCATIONS}}'],
LEGAL: ['{{GOVERNING_LAW}}', '{{JURISDICTION_CITY}}', '{{VERSION_DATE}}', '{{EFFECTIVE_DATE}}', '{{START_DATE}}'],
PRIVACY: ['{{PRIVACY_CONTACT_EMAIL}}', '{{DPO_NAME}}', '{{DPO_EMAIL}}', '{{COPYRIGHT_CONTACT_NAME}}', '{{COPYRIGHT_EMAIL}}', '{{PRIVACY_POLICY_URL}}', '{{COOKIE_POLICY_URL}}', '{{ANALYTICS_RETENTION_MONTHS}}', '{{DATA_TRANSFER_THIRD_COUNTRIES}}'],
PRIVACY: ['{{PRIVACY_CONTACT_EMAIL}}', '{{DPO_NAME}}', '{{DPO_EMAIL}}', '{{COPYRIGHT_CONTACT_NAME}}', '{{COPYRIGHT_EMAIL}}', '{{PRIVACY_POLICY_URL}}', '{{COOKIE_POLICY_URL}}', '{{ANALYTICS_RETENTION_MONTHS}}', '{{DATA_TRANSFER_THIRD_COUNTRIES}}', '{{SUPERVISORY_AUTHORITY_NAME}}', '{{SUPERVISORY_AUTHORITY_ADDRESS}}'],
SLA: ['{{AVAILABILITY_PERCENT}}', '{{MAINTENANCE_NOTICE_HOURS}}', '{{SUPPORT_EMAIL}}', '{{SUPPORT_PHONE}}', '{{SUPPORT_HOURS}}', '{{RESPONSE_CRITICAL_H}}', '{{RESOLUTION_CRITICAL_H}}', '{{RESPONSE_HIGH_H}}', '{{RESOLUTION_HIGH_H}}', '{{RESPONSE_MEDIUM_H}}', '{{RESOLUTION_MEDIUM_H}}', '{{RESPONSE_LOW_H}}'],
PAYMENTS: ['{{MONTHLY_FEE}}', '{{PAYMENT_DUE_DAY}}', '{{PAYMENT_METHOD}}', '{{PAYMENT_DAYS}}'],
SECURITY: ['{{INCIDENT_NOTICE_HOURS}}', '{{LOG_RETENTION_DAYS}}', '{{SECURITY_LOG_RETENTION_DAYS}}'],
NDA: ['{{PURPOSE}}', '{{DURATION_YEARS}}', '{{PENALTY_AMOUNT}}'],
CONSENT: ['{{WEBSITE_NAME}}', '{{ANALYTICS_TOOLS}}', '{{MARKETING_PARTNERS}}'],
CONSENT: ['{{WEBSITE_NAME}}', '{{ANALYTICS_TOOLS}}', '{{MARKETING_PARTNERS}}', '{{ANALYTICS_TOOLS_LIST}}', '{{MARKETING_PARTNERS_LIST}}'],
HOSTING: ['{{HOSTING_PROVIDER_NAME}}', '{{HOSTING_PROVIDER_COUNTRY}}', '{{HOSTING_PROVIDER_CONTRACT_TYPE}}'],
FEATURES: ['{{CONSENT_WITHDRAWAL_PATH}}', '{{SECURITY_MEASURES_SUMMARY}}', '{{DATA_SUBJECT_REQUEST_CHANNEL}}', '{{TRANSFER_GUARDS}}', '{{REGULATED_PROFESSION_TEXT}}', '{{EDITORIAL_RESPONSIBLE_NAME}}', '{{EDITORIAL_RESPONSIBLE_ADDRESS}}', '{{DISPUTE_RESOLUTION_TEXT}}', '{{NEWSLETTER_PROVIDER_DETAIL}}', '{{PAYMENT_PROVIDER_DETAIL}}', '{{SOCIAL_MEDIA_DETAIL}}', '{{ANALYTICS_TOOLS_DETAIL}}', '{{MARKETING_TOOLS_DETAIL}}', '{{CMP_NAME}}', '{{PRICES_TEXT}}', '{{PAYMENT_TERMS_TEXT}}', '{{CONTRACT_TERM_TEXT}}', '{{SLA_URL}}', '{{EXPORT_POLICY_TEXT}}', '{{LIMITATION_CAP_TEXT}}', '{{CONSUMER_WITHDRAWAL_TEXT}}', '{{SUPPORT_CHANNELS_TEXT}}'],
}
/**

View File

@@ -22,6 +22,7 @@ import {
} from './contextBridge'
import {
runRuleset, getDocType, applyBlockRemoval,
buildBoolContext, applyConditionalBlocks,
type RuleInput, type RuleEngineResult,
} from './ruleEngine'
@@ -59,6 +60,8 @@ const SECTION_LABELS: Record<keyof TemplateContext, string> = {
SECURITY: 'Sicherheit & Logs',
NDA: 'Geheimhaltung (NDA)',
CONSENT: 'Cookie / Einwilligung',
HOSTING: 'Hosting-Provider',
FEATURES: 'Dokument-Features & Textbausteine',
}
type FieldType = 'text' | 'email' | 'number' | 'select' | 'textarea' | 'boolean'
@@ -156,6 +159,55 @@ const SECTION_FIELDS: Record<keyof TemplateContext, FieldDef[]> = {
{ key: 'ANALYTICS_TOOLS', label: 'Analytics-Tools (leer = kein Block)', nullable: true },
{ key: 'MARKETING_PARTNERS', label: 'Marketing-Partner (leer = kein Block)', nullable: true },
],
HOSTING: [
{ key: 'PROVIDER_NAME', label: 'Hosting-Anbieter' },
{ key: 'COUNTRY', label: 'Hosting-Land' },
{ key: 'CONTRACT_TYPE', label: 'Vertragstyp (z. B. AVV nach Art. 28 DSGVO)' },
],
FEATURES: [
// ── DSI / Cookie ─────────────────────────────────────────────────────────
{ key: 'CONSENT_WITHDRAWAL_PATH', label: 'Einwilligungs-Widerrufspfad' },
{ key: 'SECURITY_MEASURES_SUMMARY', label: 'Sicherheitsmaßnahmen (kurz)' },
{ key: 'DATA_SUBJECT_REQUEST_CHANNEL', label: 'Kanal für Betroffenenanfragen' },
{ key: 'HAS_THIRD_COUNTRY', label: 'Drittlandübermittlung möglich', type: 'boolean' },
{ key: 'TRANSFER_GUARDS', label: 'Garantien (z. B. SCC)' },
// ── Cookie/Consent ───────────────────────────────────────────────────────
{ key: 'HAS_FUNCTIONAL_COOKIES', label: 'Funktionale Cookies aktiviert', type: 'boolean' },
{ key: 'CMP_NAME', label: 'Consent-Manager-Name (optional)' },
{ key: 'CMP_LOGS_CONSENTS', label: 'Consent-Protokollierung aktiv', type: 'boolean' },
{ key: 'ANALYTICS_TOOLS_DETAIL', label: 'Analyse-Tools (Detailtext)', type: 'textarea', span: true },
{ key: 'MARKETING_TOOLS_DETAIL', label: 'Marketing-Tools (Detailtext)', type: 'textarea', span: true },
// ── Service-Features ─────────────────────────────────────────────────────
{ key: 'HAS_ACCOUNT', label: 'Nutzerkonten vorhanden', type: 'boolean' },
{ key: 'HAS_PAYMENTS', label: 'Zahlungsabwicklung vorhanden', type: 'boolean' },
{ key: 'PAYMENT_PROVIDER_DETAIL', label: 'Zahlungsanbieter (Detailtext)', type: 'textarea', span: true },
{ key: 'HAS_SUPPORT', label: 'Support-Funktion vorhanden', type: 'boolean' },
{ key: 'SUPPORT_CHANNELS_TEXT', label: 'Support-Kanäle / Zeiten' },
{ key: 'HAS_NEWSLETTER', label: 'Newsletter vorhanden', type: 'boolean' },
{ key: 'NEWSLETTER_PROVIDER_DETAIL', label: 'Newsletter-Anbieter (Detailtext)', type: 'textarea', span: true },
{ key: 'HAS_SOCIAL_MEDIA', label: 'Social-Media-Präsenz', type: 'boolean' },
{ key: 'SOCIAL_MEDIA_DETAIL', label: 'Social-Media-Details', type: 'textarea', span: true },
// ── AGB ──────────────────────────────────────────────────────────────────
{ key: 'HAS_PAID_PLANS', label: 'Kostenpflichtige Pläne', type: 'boolean' },
{ key: 'PRICES_TEXT', label: 'Preise (Text/Link)', type: 'textarea', span: true },
{ key: 'PAYMENT_TERMS_TEXT', label: 'Zahlungsbedingungen', type: 'textarea', span: true },
{ key: 'CONTRACT_TERM_TEXT', label: 'Laufzeit & Kündigung', type: 'textarea', span: true },
{ key: 'HAS_SLA', label: 'SLA vorhanden', type: 'boolean' },
{ key: 'SLA_URL', label: 'SLA-URL' },
{ key: 'HAS_EXPORT_POLICY', label: 'Datenexport/Löschung geregelt', type: 'boolean' },
{ key: 'EXPORT_POLICY_TEXT', label: 'Datenexport-Regelung (Text)', type: 'textarea', span: true },
{ key: 'HAS_WITHDRAWAL', label: 'Widerrufsrecht (B2C digital)', type: 'boolean' },
{ key: 'CONSUMER_WITHDRAWAL_TEXT', label: 'Widerrufsbelehrung (Text)', type: 'textarea', span: true },
{ key: 'LIMITATION_CAP_TEXT', label: 'Haftungsdeckel B2B (Text)' },
// ── Impressum ────────────────────────────────────────────────────────────
{ key: 'HAS_REGULATED_PROFESSION', label: 'Reglementierter Beruf', type: 'boolean' },
{ key: 'REGULATED_PROFESSION_TEXT', label: 'Berufsrecht-Text', type: 'textarea', span: true },
{ key: 'HAS_EDITORIAL_RESPONSIBLE', label: 'V.i.S.d.P. (redaktionell)', type: 'boolean' },
{ key: 'EDITORIAL_RESPONSIBLE_NAME', label: 'V.i.S.d.P. Name' },
{ key: 'EDITORIAL_RESPONSIBLE_ADDRESS', label: 'V.i.S.d.P. Adresse' },
{ key: 'HAS_DISPUTE_RESOLUTION', label: 'Streitbeilegungshinweis', type: 'boolean' },
{ key: 'DISPUTE_RESOLUTION_TEXT', label: 'Streitbeilegungstext', type: 'textarea', span: true },
],
}
// =============================================================================
@@ -401,16 +453,25 @@ function GeneratorSection({
...extraPlaceholders,
}), [context, extraPlaceholders, ruleResult])
// Boolean context for {{#IF}} rendering
const boolCtx = useMemo(
() => ruleResult ? buildBoolContext(ruleResult.contextAfterDefaults, ruleResult.computedFlags) : {},
[ruleResult]
)
const renderedContent = useMemo(() => {
// Apply block removal BEFORE placeholder substitution
// 1. Remove ruleset-driven blocks ([BLOCK:ID])
let content = applyBlockRemoval(template.text, ruleResult?.removedBlocks ?? [])
// 2. Evaluate {{#IF}} / {{#IF_NOT}} / {{#IF_ANY}} directives
content = applyConditionalBlocks(content, boolCtx)
// 3. Substitute placeholders
for (const [key, value] of Object.entries(allPlaceholderValues)) {
if (value) {
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value)
}
}
return content
}, [template.text, allPlaceholderValues, ruleResult])
}, [template.text, allPlaceholderValues, ruleResult, boolCtx])
// Compute which modules are relevant (mentioned in violations/warnings)
const relevantModules = useMemo(() => {
@@ -776,22 +837,42 @@ function DocumentGeneratorPageInner() {
})
}, [])
// Pre-fill context from company profile
// Pre-fill context from company profile + SDK state
useEffect(() => {
if (state?.companyProfile) {
const profile = state.companyProfile
const p = profile as Record<string, string>
setContext((prev) => ({
...prev,
PROVIDER: {
...prev.PROVIDER,
LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME,
EMAIL: (profile as Record<string, string>).email || prev.PROVIDER.EMAIL,
LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME,
LEGAL_FORM: p.legalForm || prev.PROVIDER.LEGAL_FORM,
ADDRESS_LINE: p.addressLine || prev.PROVIDER.ADDRESS_LINE,
POSTAL_CODE: p.postalCode || prev.PROVIDER.POSTAL_CODE,
CITY: p.city || prev.PROVIDER.CITY,
COUNTRY: p.country || prev.PROVIDER.COUNTRY,
EMAIL: p.email || prev.PROVIDER.EMAIL,
PHONE: p.phone || prev.PROVIDER.PHONE,
WEBSITE_URL: p.websiteUrl || prev.PROVIDER.WEBSITE_URL,
CEO_NAME: p.representedByName || p.ceoName || prev.PROVIDER.CEO_NAME,
REGISTER_COURT: p.registerCourt || prev.PROVIDER.REGISTER_COURT,
REGISTER_NUMBER: p.registerNumber || prev.PROVIDER.REGISTER_NUMBER,
VAT_ID: p.vatId || prev.PROVIDER.VAT_ID,
},
PRIVACY: {
...prev.PRIVACY,
DPO_NAME: profile.dpoName || prev.PRIVACY.DPO_NAME,
DPO_NAME: profile.dpoName || prev.PRIVACY.DPO_NAME,
DPO_EMAIL: profile.dpoEmail || prev.PRIVACY.DPO_EMAIL,
CONTACT_EMAIL: profile.dpoEmail || prev.PRIVACY.CONTACT_EMAIL,
SUPERVISORY_AUTHORITY_NAME: p.supervisoryAuthorityName || prev.PRIVACY.SUPERVISORY_AUTHORITY_NAME,
SUPERVISORY_AUTHORITY_ADDRESS: p.supervisoryAuthorityAddress || prev.PRIVACY.SUPERVISORY_AUTHORITY_ADDRESS,
},
FEATURES: {
...prev.FEATURES,
DATA_SUBJECT_REQUEST_CHANNEL: p.email
? `per E-Mail an ${p.email}`
: prev.FEATURES.DATA_SUBJECT_REQUEST_CHANNEL,
},
}))
}

View File

@@ -3,6 +3,8 @@ import {
runRuleset,
getDocType,
applyBlockRemoval,
buildBoolContext,
applyConditionalBlocks,
type RuleInput,
} from './ruleEngine'
import { EMPTY_CONTEXT } from './contextBridge'
@@ -497,3 +499,166 @@ describe('integration', () => {
expect(r.computedFlags.HAS_MARKETING).toBe(false)
})
})
// =============================================================================
// buildBoolContext
// =============================================================================
describe('buildBoolContext', () => {
it('maps computed flags directly', () => {
const ctx = withCtx({
CUSTOMER: { ...EMPTY_CONTEXT.CUSTOMER, IS_CONSUMER: true, IS_BUSINESS: false },
SERVICE: { ...EMPTY_CONTEXT.SERVICE, MODEL: 'SaaS' },
})
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.IS_B2C).toBe(true)
expect(boolCtx.IS_B2B).toBe(false)
expect(boolCtx.SERVICE_IS_SAAS).toBe(true)
expect(boolCtx.ANALYTICS_ENABLED).toBe(false)
})
it('HAS_REGISTER derived from PROVIDER.REGISTER_COURT', () => {
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, REGISTER_COURT: 'AG München' } })
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_REGISTER).toBe(true)
})
it('HAS_REGISTER=false when REGISTER_COURT is empty', () => {
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, REGISTER_COURT: '' } })
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_REGISTER).toBe(false)
})
it('HAS_VAT_ID derived from PROVIDER.VAT_ID', () => {
const ctx = withCtx({ PROVIDER: { ...EMPTY_CONTEXT.PROVIDER, VAT_ID: 'DE123456789' } })
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_VAT_ID).toBe(true)
})
it('HAS_DPO derived from PRIVACY.DPO_NAME', () => {
const ctx = withCtx({ PRIVACY: { ...EMPTY_CONTEXT.PRIVACY, DPO_NAME: 'Max Mustermann' } })
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_DPO).toBe(true)
})
it('FEATURES booleans are passed through', () => {
const ctx = withCtx({
FEATURES: {
...EMPTY_CONTEXT.FEATURES,
HAS_NEWSLETTER: true,
HAS_ACCOUNT: true,
HAS_PAID_PLANS: false,
HAS_THIRD_COUNTRY: true,
},
})
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_NEWSLETTER).toBe(true)
expect(boolCtx.HAS_ACCOUNT).toBe(true)
expect(boolCtx.HAS_PAID_PLANS).toBe(false)
expect(boolCtx.THIRD_COUNTRY_POSSIBLE).toBe(true)
})
it('MARKETING_ENABLED is alias for HAS_MARKETING', () => {
const ctx = withCtx({ CONSENT: { ...EMPTY_CONTEXT.CONSENT, MARKETING_PARTNERS: 'Meta Pixel' } })
const r = runRuleset(makeInput({ context: ctx }))
const boolCtx = buildBoolContext(r.contextAfterDefaults, r.computedFlags)
expect(boolCtx.HAS_MARKETING).toBe(true)
expect(boolCtx.MARKETING_ENABLED).toBe(true)
})
})
// =============================================================================
// applyConditionalBlocks
// =============================================================================
describe('applyConditionalBlocks', () => {
it('{{#IF}} shows block when condition is true', () => {
const result = applyConditionalBlocks(
'{{#IF HAS_ANALYTICS}}\nAnalytics section\n{{/IF}}',
{ HAS_ANALYTICS: true }
)
expect(result).toContain('Analytics section')
expect(result).not.toContain('{{#IF')
})
it('{{#IF}} hides block when condition is false', () => {
const result = applyConditionalBlocks(
'{{#IF HAS_ANALYTICS}}\nAnalytics section\n{{/IF}}',
{ HAS_ANALYTICS: false }
)
expect(result).toBe('')
expect(result).not.toContain('Analytics section')
})
it('{{#IF_NOT}} shows block when condition is false', () => {
const result = applyConditionalBlocks(
'{{#IF_NOT HAS_DPO}}\nKein DSB bestellt.\n{{/IF_NOT}}',
{ HAS_DPO: false }
)
expect(result).toContain('Kein DSB bestellt.')
})
it('{{#IF_NOT}} hides block when condition is true', () => {
const result = applyConditionalBlocks(
'{{#IF_NOT HAS_DPO}}\nKein DSB bestellt.\n{{/IF_NOT}}',
{ HAS_DPO: true }
)
expect(result).toBe('')
})
it('{{#IF_ANY}} shows block when any condition is true', () => {
const result = applyConditionalBlocks(
'{{#IF_ANY HAS_ANALYTICS HAS_MARKETING}}\nTracking aktiv.\n{{/IF_ANY}}',
{ HAS_ANALYTICS: false, HAS_MARKETING: true }
)
expect(result).toContain('Tracking aktiv.')
})
it('{{#IF_ANY}} hides block when all conditions are false', () => {
const result = applyConditionalBlocks(
'{{#IF_ANY HAS_ANALYTICS HAS_MARKETING}}\nTracking aktiv.\n{{/IF_ANY}}',
{ HAS_ANALYTICS: false, HAS_MARKETING: false }
)
expect(result).toBe('')
})
it('processes multiple blocks independently', () => {
const content = '{{#IF A}}\nBlock A\n{{/IF}}\n{{#IF_NOT B}}\nBlock B\n{{/IF_NOT}}'
const result = applyConditionalBlocks(content, { A: true, B: false })
expect(result).toContain('Block A')
expect(result).toContain('Block B')
})
it('handles multiline block content', () => {
const content = '{{#IF HAS_DPO}}\nName: {{DPO_NAME}}\nEmail: {{DPO_EMAIL}}\n{{/IF}}'
const result = applyConditionalBlocks(content, { HAS_DPO: true })
expect(result).toContain('Name: {{DPO_NAME}}')
expect(result).toContain('Email: {{DPO_EMAIL}}')
})
it('preserves placeholders inside kept blocks for later substitution', () => {
const content = '{{#IF HAS_ANALYTICS}}\nTool: {{ANALYTICS_TOOLS_LIST}}\n{{/IF}}'
const result = applyConditionalBlocks(content, { HAS_ANALYTICS: true })
expect(result).toContain('{{ANALYTICS_TOOLS_LIST}}')
})
it('is a no-op when content has no conditional directives', () => {
const plain = '# Impressum\n\n{{COMPANY_LEGAL_NAME}}'
expect(applyConditionalBlocks(plain, {})).toBe(plain)
})
it('handles unknown condition as false (not in boolCtx)', () => {
const result = applyConditionalBlocks(
'{{#IF UNKNOWN_COND}}\nHidden\n{{/IF}}',
{}
)
expect(result).toBe('')
expect(result).not.toContain('Hidden')
})
})

View File

@@ -359,6 +359,83 @@ export function getDocType(templateType: string, language: string): string {
return DOC_TYPE_MAP[key] ?? `${templateType}_${language}`
}
// =============================================================================
// buildBoolContext: combines computed flags + derived + FEATURES booleans
// =============================================================================
export function buildBoolContext(ctx: TemplateContext, flags: ComputedFlags): Record<string, boolean> {
const f = ctx.FEATURES
return {
// --- From computed flags ---
IS_B2C: flags.IS_B2C,
IS_B2B: flags.IS_B2B,
SERVICE_IS_SAAS: flags.SERVICE_IS_SAAS,
SERVICE_IS_HYBRID: flags.SERVICE_IS_HYBRID,
HAS_PENALTY: flags.HAS_PENALTY,
HAS_ANALYTICS: flags.HAS_ANALYTICS,
ANALYTICS_ENABLED: flags.HAS_ANALYTICS, // alias used in cookie banner template
HAS_MARKETING: flags.HAS_MARKETING,
MARKETING_ENABLED: flags.HAS_MARKETING, // alias
// --- Derived from PROVIDER ---
HAS_REGISTER: !!(ctx.PROVIDER.REGISTER_COURT),
HAS_VAT_ID: !!(ctx.PROVIDER.VAT_ID),
CONTACT_PHONE: !!(ctx.PROVIDER.PHONE),
// --- Derived from PRIVACY ---
HAS_DPO: !!(ctx.PRIVACY.DPO_NAME),
// --- From FEATURES booleans ---
THIRD_COUNTRY_POSSIBLE: f.HAS_THIRD_COUNTRY,
HAS_THIRD_COUNTRY: f.HAS_THIRD_COUNTRY,
FUNCTIONAL_ENABLED: f.HAS_FUNCTIONAL_COOKIES,
HAS_FUNCTIONAL_COOKIES: f.HAS_FUNCTIONAL_COOKIES,
CMP_LOGS_CONSENTS: f.CMP_LOGS_CONSENTS,
HAS_NEWSLETTER: f.HAS_NEWSLETTER,
HAS_ACCOUNT: f.HAS_ACCOUNT,
HAS_PAYMENTS: f.HAS_PAYMENTS,
HAS_SUPPORT: f.HAS_SUPPORT,
HAS_SOCIAL_MEDIA: f.HAS_SOCIAL_MEDIA,
HAS_PAID_PLANS: f.HAS_PAID_PLANS,
HAS_SLA: f.HAS_SLA,
HAS_EXPORT_POLICY: f.HAS_EXPORT_POLICY,
HAS_WITHDRAWAL: f.HAS_WITHDRAWAL,
HAS_REGULATED_PROFESSION: f.HAS_REGULATED_PROFESSION,
HAS_EDITORIAL_RESPONSIBLE: f.HAS_EDITORIAL_RESPONSIBLE,
HAS_DISPUTE_RESOLUTION: f.HAS_DISPUTE_RESOLUTION,
}
}
// =============================================================================
// applyConditionalBlocks: processes {{#IF}}/{{#IF_NOT}}/{{#IF_ANY}} directives
// Runs BEFORE placeholder substitution.
// =============================================================================
export function applyConditionalBlocks(content: string, boolCtx: Record<string, boolean>): string {
let result = content
// {{#IF_NOT COND}}...{{/IF_NOT}} — process before IF to avoid conflicts
result = result.replace(
/\{\{#IF_NOT ([A-Z_]+)\}\}([\s\S]*?)\{\{\/IF_NOT\}\}/g,
(_, cond: string, body: string) => (boolCtx[cond] ? '' : body)
)
// {{#IF_ANY A B C}}...{{/IF_ANY}}
result = result.replace(
/\{\{#IF_ANY ([A-Z_ ]+)\}\}([\s\S]*?)\{\{\/IF_ANY\}\}/g,
(_, conds: string, body: string) =>
conds.trim().split(/\s+/).some((c) => boolCtx[c]) ? body : ''
)
// {{#IF COND}}...{{/IF}}
result = result.replace(
/\{\{#IF ([A-Z_]+)\}\}([\s\S]*?)\{\{\/IF\}\}/g,
(_, cond: string, body: string) => (boolCtx[cond] ? body : '')
)
return result
}
// =============================================================================
// applyBlockRemoval: removes [BLOCK:ID]…[/BLOCK:ID] markers from content
// =============================================================================

View File

@@ -0,0 +1,54 @@
-- Migration 023: New DE Document Templates with {{#IF}} Block Syntax
-- ============================================================
-- This migration adds 4 new German document templates that use
-- the {{#IF CONDITION}}...{{/IF}} conditional block syntax
-- introduced in Template-Spec v1 Phase C.
--
-- Templates inserted (via Python script: scripts/apply_templates_023.py):
--
-- document_type | language | title
-- ────────────────────┼──────────┼─────────────────────────────────────────────
-- cookie_banner | de | Cookie-Banner Texte v2 (DSGVO/TDDDG, IF-Blöcke)
-- privacy_policy | de | Datenschutzerklärung (DSGVO-konform, IF-Blöcke)
-- agb | de | AGB SaaS/Cloud DE (B2B/B2C, IF-Blöcke)
-- impressum | de | Impressum DE (DDG § 5, IF-Blöcke)
--
-- Conditional syntax supported in templates:
-- {{#IF CONDITION}}...{{/IF}}
-- {{#IF_NOT CONDITION}}...{{/IF_NOT}}
-- {{#IF_ANY COND_A COND_B COND_C}}...{{/IF_ANY}}
--
-- Boolean conditions are evaluated by applyConditionalBlocks() in ruleEngine.ts
-- using the combined boolCtx from buildBoolContext(ctx, computedFlags).
--
-- No schema changes required — templates are inserted into the existing
-- public.compliance_legal_templates table.
--
-- Run the Python migration script:
-- docker cp scripts/apply_templates_023.py bp-compliance-backend:/tmp/
-- docker exec bp-compliance-backend python3 /tmp/apply_templates_023.py
-- ============================================================
-- Verify templates were inserted (run after script):
SELECT
document_type,
language,
title,
version,
is_active,
LENGTH(content) AS content_length,
array_length(placeholders, 1) AS placeholder_count,
created_at
FROM public.compliance_legal_templates
WHERE document_type IN ('cookie_banner', 'privacy_policy', 'agb', 'impressum')
AND language = 'de'
ORDER BY document_type;
-- Count {{#IF}} blocks per template:
SELECT
document_type,
title,
(LENGTH(content) - LENGTH(REPLACE(content, '{{#IF ', ''))) / LENGTH('{{#IF ') AS if_block_count
FROM public.compliance_legal_templates
WHERE document_type IN ('cookie_banner', 'privacy_policy', 'agb', 'impressum')
AND language = 'de';

View File

@@ -0,0 +1,683 @@
#!/usr/bin/env python3
"""
Migration 023: Insert 4 new DE document templates with {{#IF}} block syntax.
Templates:
- cookie_banner_texts_de → Cookie-Banner Texte v2 (DSGVO/TDDDG, mit IF-Blöcken)
- privacy_policy_de → Datenschutzerklärung (DSGVO-konform, mit IF-Blöcken)
- terms_and_conditions_de → AGB SaaS/Cloud DE (B2B/B2C, mit IF-Blöcken)
- imprint_de → Impressum DE (DDG § 5, mit IF-Blöcken)
Usage:
docker cp apply_templates_023.py bp-compliance-backend:/tmp/
docker exec bp-compliance-backend python3 /tmp/apply_templates_023.py
"""
import os
import sys
import uuid
sys.path.insert(0, '/app')
from sqlalchemy import create_engine, text
DATABASE_URL = os.environ.get(
'DATABASE_URL',
'postgresql://compliance_user:compliance_pass@bp-core-postgres:5432/breakpilot_db'
)
engine = create_engine(DATABASE_URL)
# ─────────────────────────────────────────────────────────────────────────────
# Template definitions
# ─────────────────────────────────────────────────────────────────────────────
TEMPLATES = [
{
'document_type': 'cookie_banner',
'language': 'de',
'title': 'Cookie-Banner Texte v2 (DSGVO/TDDDG-konform, IF-Blöcke)',
'description': 'Vollständige Cookie-Banner Texte mit optionalen Analyse-, Marketing- und Funktional-Blöcken. Unterstützt {{#IF}} Syntax.',
'jurisdiction': 'DE',
'license_id': 'mit',
'license_name': 'MIT License',
'is_complete_document': True,
'attribution_required': False,
'source_name': 'BreakPilot Compliance',
'content': """\
# Cookie-Banner Texte
**Website:** {{WEBSITE_NAME}}
**Links:** [Cookie-Richtlinie]({{COOKIE_POLICY_URL}}) | [Datenschutzerklärung]({{PRIVACY_POLICY_URL}})
**Kontakt:** {{COMPANY_LEGAL_NAME}} · {{CONTACT_EMAIL}}
*Rechtsgrundlage: § 25 TDDDG zzgl. DSGVO-Rechtsgrundlage je Zweck.*
---
## 1) Erstlayer (Banner)
**Titel:** Cookie-Einstellungen
**Kurztext:**
Wir verwenden Cookies und ähnliche Technologien, um {{WEBSITE_NAME}} technisch bereitzustellen und Ihnen eine sichere Nutzung zu ermöglichen. Mit Ihrer Einwilligung setzen wir zusätzlich Cookies für Statistik/Analyse und Marketing ein. Sie können Ihre Auswahl jederzeit unter {{CONSENT_WITHDRAWAL_PATH}} ändern.
**Links im Banner:**
Mehr Informationen: [Cookie-Richtlinie]({{COOKIE_POLICY_URL}}) · [Datenschutzerklärung]({{PRIVACY_POLICY_URL}})
**Buttons:**
- „Alle akzeptieren"
- „Nur notwendige"
- „Einstellungen"
*Ihre Einwilligung ist freiwillig und kann jederzeit mit Wirkung für die Zukunft widerrufen werden.*
---
## 2) Zweitlayer (Einstellungen)
**Einleitung:**
Hier können Sie auswählen, welche Cookies wir verwenden dürfen. Notwendige Cookies sind für den Betrieb erforderlich und können nicht deaktiviert werden.
**Buttons:**
- „Auswahl bestätigen"
- „Alle akzeptieren"
- „Ablehnen"
- „Zurück"
---
## 3) Kategorien
### Notwendig (immer aktiv)
Diese Cookies sind für den Betrieb von {{WEBSITE_NAME}} zwingend erforderlich. Sie ermöglichen grundlegende Funktionen wie Seitennavigation, Session-Verwaltung und Sicherheitsfunktionen (z. B. CSRF-Schutz). Ohne diese Cookies kann die Website nicht ordnungsgemäß funktionieren.
**Rechtsgrundlage:** Technische Erforderlichkeit (§ 25 Abs. 2 TDDDG). Details: {{COOKIE_POLICY_URL}}.
### Funktional (optional)
{{#IF FUNCTIONAL_ENABLED}}
Funktionale Cookies ermöglichen erweiterte Funktionen und Personalisierung, z. B. das Merken Ihrer Spracheinstellungen oder Darstellungsvorlieben. Wir setzen diese Cookies nur mit Ihrer Einwilligung ein.
{{/IF}}
### Statistik/Analyse (optional)
{{#IF ANALYTICS_ENABLED}}
Analyse-Cookies helfen uns zu verstehen, wie Besucher {{WEBSITE_NAME}} nutzen. Wir setzen diese Cookies nur mit Ihrer Einwilligung ein.
**Eingesetzte Tools:** {{ANALYTICS_TOOLS_LIST}}
{{#IF THIRD_COUNTRY_POSSIBLE}}
**Hinweis Drittlandübermittlung:** Bei einzelnen Analyse-Tools kann eine Verarbeitung außerhalb EU/EWR nicht ausgeschlossen werden. Geeignete Garantien ({{TRANSFER_GUARDS}}) sind vereinbart. Details: {{PRIVACY_POLICY_URL}}.
{{/IF}}
{{/IF}}
### Marketing (optional)
{{#IF MARKETING_ENABLED}}
Marketing-Cookies ermöglichen personalisierte Werbung und Kampagnenmessung. Wir setzen diese Cookies nur mit Ihrer Einwilligung ein.
**Partner/Tools:** {{MARKETING_PARTNERS_LIST}}
{{#IF THIRD_COUNTRY_POSSIBLE}}
**Hinweis Drittlandübermittlung:** Bei Marketing-Tools kann eine Verarbeitung außerhalb EU/EWR nicht ausgeschlossen werden. Geeignete Garantien ({{TRANSFER_GUARDS}}) werden genutzt. Details: {{PRIVACY_POLICY_URL}}.
{{/IF}}
{{/IF}}
---
## 4) Widerruf / Änderung
Sie können Ihre Einwilligung jederzeit widerrufen oder ändern: {{CONSENT_WITHDRAWAL_PATH}}. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.
---
## 5) Consent-Protokoll
{{#IF CMP_LOGS_CONSENTS}}
Wir protokollieren Ihre Einwilligungsentscheidung (Zeitpunkt, Auswahl{{#IF CMP_NAME}}, verwaltet via {{CMP_NAME}}{{/IF}}), um diese nachweisen zu können. Details: {{PRIVACY_POLICY_URL}}.
{{/IF}}
""",
},
{
'document_type': 'privacy_policy',
'language': 'de',
'title': 'Datenschutzerklärung (DSGVO-konform, IF-Blöcke)',
'description': 'Vollständige Datenschutzerklärung nach DSGVO mit optionalen Abschnitten für Analytics, Marketing, Newsletter, Account, Payments, Support, Social Media und Drittlandübermittlungen.',
'jurisdiction': 'DE',
'license_id': 'mit',
'license_name': 'MIT License',
'is_complete_document': True,
'attribution_required': False,
'source_name': 'BreakPilot Compliance',
'content': """\
# Datenschutzerklärung
**Stand:** {{VERSION_DATE}}
**Gültig für:** {{WEBSITE_URL}}
---
## 1. Verantwortlicher
Verantwortlicher im Sinne der DSGVO:
**{{COMPANY_LEGAL_NAME}} {{COMPANY_LEGAL_FORM}}**
{{COMPANY_ADDRESS_LINE}}
{{COMPANY_POSTAL_CODE}} {{COMPANY_CITY}}
{{COMPANY_COUNTRY}}
E-Mail: {{CONTACT_EMAIL}}
{{#IF CONTACT_PHONE}}Telefon: {{CONTACT_PHONE}}{{/IF}}
---
## 2. Datenschutzbeauftragter
{{#IF HAS_DPO}}
Unser Datenschutzbeauftragter:
**{{DPO_NAME}}** · E-Mail: {{DPO_EMAIL}}
{{/IF}}
{{#IF_NOT HAS_DPO}}
Wir haben keinen Datenschutzbeauftragten bestellt. Bei Fragen zum Datenschutz: {{CONTACT_EMAIL}}
{{/IF_NOT}}
---
## 3. Grundsätze der Verarbeitung
Wir verarbeiten personenbezogene Daten ausschließlich im Einklang mit der DSGVO. Wir beachten Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integrität/Vertraulichkeit und Transparenz.
---
## 4. Datenverarbeitung beim Website-Besuch (Server-Logfiles)
Beim Aufruf unserer Website werden Informationen in Server-Logfiles verarbeitet (IP-Adresse, Datum/Uhrzeit, abgerufene URL, Referrer, Browsertyp, Betriebssystem).
**Zweck:** Technische Bereitstellung, Sicherheit, Fehleranalyse.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
**Speicherdauer:** {{LOG_RETENTION_DAYS}} Tage.
---
## 5. Hosting und Auftragsverarbeitung
{{WEBSITE_URL}} wird gehostet bei: **{{HOSTING_PROVIDER_NAME}}**, {{HOSTING_PROVIDER_COUNTRY}}.
Mit dem Hosting-Provider besteht ein Vertrag zur Auftragsverarbeitung (**{{HOSTING_PROVIDER_CONTRACT_TYPE}}**).
---
## 6. Kontaktaufnahme
Wenn Sie uns kontaktieren (E-Mail, Kontaktformular), verarbeiten wir Ihre Anfragedaten.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b oder f DSGVO.
**Speicherdauer:** Bis zur abschließenden Bearbeitung; ggf. länger bei gesetzlichen Aufbewahrungsfristen.
---
## 7. Nutzerkonto / Registrierung
{{#IF HAS_ACCOUNT}}
Bei der Kontoerstellung verarbeiten wir Stammdaten (Name, E-Mail), Zugangsdaten (Passwort gehasht) und ggf. Abrechnungsdaten.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b DSGVO.
**Speicherdauer:** Dauer der Vertragsbeziehung; anschließend Löschung, soweit keine Aufbewahrungspflichten bestehen.
{{/IF}}
{{#IF_NOT HAS_ACCOUNT}}
Wir bieten kein registrierungspflichtiges Nutzerkonto an.
{{/IF_NOT}}
---
## 8. Vertragsabwicklung und Zahlungen
{{#IF HAS_PAYMENTS}}
Für kostenpflichtige Leistungen verarbeiten wir Vertrags- und Abrechnungsdaten.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b und c DSGVO.
**Details zu Zahlungsanbietern:**
{{PAYMENT_PROVIDER_DETAIL}}
{{/IF}}
---
## 9. Support
{{#IF HAS_SUPPORT}}
Bei Support-Anfragen verarbeiten wir Kontakt- und Inhaltsdaten sowie ggf. technische Diagnosedaten.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. b oder f DSGVO.
{{/IF}}
---
## 10. Cookies und Einwilligungsmanagement
Details zu Cookies, Speicherdauern und Tools: {{COOKIE_POLICY_URL}}.
Einwilligung verwalten/widerrufen: {{CONSENT_WITHDRAWAL_PATH}}.
---
## 11. Webanalyse
{{#IF HAS_ANALYTICS}}
Wir nutzen Webanalyse-Tools zur Verbesserung unserer Website.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).
**Details:**
{{ANALYTICS_TOOLS_DETAIL}}
{{#IF THIRD_COUNTRY_POSSIBLE}}
**Drittlandübermittlungen:** Eine Verarbeitung außerhalb EU/EWR kann nicht ausgeschlossen werden. Geeignete Garantien ({{TRANSFER_GUARDS}}) sind vereinbart.
{{/IF}}
{{/IF}}
{{#IF_NOT HAS_ANALYTICS}}
Wir setzen keine Webanalyse-Tools ein, die eine Einwilligung erfordern.
{{/IF_NOT}}
---
## 12. Marketing und Tracking
{{#IF HAS_MARKETING}}
Wir setzen Marketing-Tools ein, um Kampagnen zu messen und relevante Inhalte anzuzeigen.
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).
**Details:**
{{MARKETING_TOOLS_DETAIL}}
{{#IF THIRD_COUNTRY_POSSIBLE}}
**Drittlandübermittlungen:** Geeignete Garantien ({{TRANSFER_GUARDS}}) werden genutzt.
{{/IF}}
{{/IF}}
{{#IF_NOT HAS_MARKETING}}
Wir setzen keine Marketing-Tools ein, die eine Einwilligung erfordern.
{{/IF_NOT}}
---
## 13. Newsletter
{{#IF HAS_NEWSLETTER}}
Für den Newsletter-Versand verarbeiten wir Ihre E-Mail-Adresse (Double-Opt-In-Verfahren).
**Rechtsgrundlage:** Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).
**Widerruf:** Abmeldelink oder {{CONTACT_EMAIL}}.
**Details:**
{{NEWSLETTER_PROVIDER_DETAIL}}
{{/IF}}
---
## 14. Social Media
{{#IF HAS_SOCIAL_MEDIA}}
Wir unterhalten Präsenzen auf sozialen Netzwerken. Beim Besuch gelten die Datenschutzbestimmungen der jeweiligen Anbieter.
{{SOCIAL_MEDIA_DETAIL}}
{{/IF}}
---
## 15. Empfänger / Auftragsverarbeiter
Wir setzen Dienstleister ein, die Daten in unserem Auftrag verarbeiten und mit denen wir AVV-Verträge schließen. Eine Weitergabe an Dritte erfolgt nur bei Einwilligung, Vertragserfüllung, gesetzlicher Pflicht oder berechtigtem Interesse.
---
## 16. Drittlandübermittlungen
{{#IF THIRD_COUNTRY_POSSIBLE}}
Übermittlungen außerhalb EU/EWR können bei einzelnen Dienstleistern nicht ausgeschlossen werden. Wir stellen ein angemessenes Schutzniveau durch {{TRANSFER_GUARDS}} sicher.
{{/IF}}
{{#IF_NOT THIRD_COUNTRY_POSSIBLE}}
Übermittlungen in Drittländer außerhalb EU/EWR finden nicht statt.
{{/IF_NOT}}
---
## 17. Speicherdauer
Personenbezogene Daten werden nur so lange gespeichert, wie es für die jeweiligen Zwecke oder gesetzliche Aufbewahrungspflichten erforderlich ist.
---
## 18. Sicherheit
Wir setzen technische und organisatorische Maßnahmen ein: {{SECURITY_MEASURES_SUMMARY}}.
---
## 19. Ihre Rechte
- Auskunft (Art. 15 DSGVO)
- Berichtigung (Art. 16 DSGVO)
- Löschung (Art. 17 DSGVO)
- Einschränkung (Art. 18 DSGVO)
- Datenübertragbarkeit (Art. 20 DSGVO)
- Widerspruch (Art. 21 DSGVO)
- Widerruf einer Einwilligung (Art. 7 Abs. 3 DSGVO)
Zur Ausübung Ihrer Rechte: {{DATA_SUBJECT_REQUEST_CHANNEL}}
---
## 20. Beschwerderecht
**{{SUPERVISORY_AUTHORITY_NAME}}**
{{SUPERVISORY_AUTHORITY_ADDRESS}}
---
## 21. Änderungen
Wir können diese Datenschutzerklärung anpassen. Die aktuelle Version finden Sie unter {{WEBSITE_URL}}.
""",
},
{
'document_type': 'agb',
'language': 'de',
'title': 'AGB SaaS/Cloud DE (B2B/B2C steuerbar, IF-Blöcke)',
'description': 'Allgemeine Geschäftsbedingungen für SaaS/Cloud-Dienste. B2B/B2C über Conditions steuerbar. Optionale Abschnitte für Paid Plans, SLA, Widerruf, Datenexport.',
'jurisdiction': 'DE',
'license_id': 'mit',
'license_name': 'MIT License',
'is_complete_document': True,
'attribution_required': False,
'source_name': 'BreakPilot Compliance',
'content': """\
# Allgemeine Geschäftsbedingungen (AGB)
**{{COMPANY_LEGAL_NAME}}**
Stand: {{VERSION_DATE}}
---
## § 1 Geltungsbereich
(1) Diese AGB gelten für alle Verträge zwischen {{COMPANY_LEGAL_NAME}} („Anbieter") und Kunden über die Nutzung von **{{SERVICE_NAME}}**.
(2) Abweichende Kundenbedingungen werden nicht Vertragsbestandteil.
(3) Diese AGB gelten gegenüber Unternehmern und Verbrauchern, soweit nicht ausdrücklich unterschieden.
---
## § 2 Leistungsbeschreibung
(1) Der Anbieter stellt **{{SERVICE_NAME}}** zur Verfügung: {{SERVICE_DESCRIPTION_SHORT}}.
(2) Umfang und Systemvoraussetzungen ergeben sich aus der jeweils aktuellen Leistungsbeschreibung.
(3) Der Anbieter ist berechtigt, den Dienst weiterzuentwickeln, sofern Kernfunktionen im Wesentlichen erhalten bleiben.
---
## § 3 Vertragsschluss
(1) Die Darstellung des Dienstes ist kein verbindliches Angebot.
(2) Der Vertrag kommt durch Bestätigung der Bestellung oder Freischaltung des Dienstes zustande.
---
## § 4 Preise, Abrechnung, Zahlung
{{#IF HAS_PAID_PLANS}}
(1) Es gelten die bei Vertragsschluss vereinbarten Preise: {{PRICES_TEXT}}.
(2) Zahlungsbedingungen: {{PAYMENT_TERMS_TEXT}}.
(3) Preise gegenüber Verbrauchern inkl. gesetzl. USt.; gegenüber Unternehmern zzgl. USt.
(4) Bei Zahlungsverzug: gesetzliche Verzugszinsen; gegenüber Unternehmern 9 Prozentpunkte über Basiszinssatz (§ 288 Abs. 2 BGB).
{{/IF}}
{{#IF_NOT HAS_PAID_PLANS}}
(1) Der Dienst wird derzeit unentgeltlich angeboten. Der Anbieter behält sich vor, kostenpflichtige Leistungsstufen einzuführen.
{{/IF_NOT}}
---
## § 5 Pflichten des Kunden
(1) Der Kunde hat bei Registrierung richtige Angaben zu machen und diese aktuell zu halten.
(2) Zugangsdaten sind geheim zu halten.
(3) Der Dienst darf nur im Rahmen der geltenden Gesetze und dieser AGB genutzt werden.
(4) Der Kunde ist verantwortlich für eingestellte Inhalte/Daten und stellt sicher, dass Rechte Dritter nicht verletzt werden.
---
## § 6 Nutzungsrechte
(1) Der Anbieter räumt dem Kunden ein einfaches, nicht übertragbares, auf die Vertragsdauer beschränktes Nutzungsrecht ein.
(2) Überlassung an Dritte, Unterlizenzierung oder öffentliche Zugänglichmachung außerhalb der vorgesehenen Nutzung sind untersagt.
(3) Reverse Engineering und Umgehung von Sicherheitsmechanismen sind untersagt, soweit gesetzlich zulässig.
---
## § 7 Verfügbarkeit und Support
(1) Der Anbieter bemüht sich um hohe Verfügbarkeit.
{{#IF HAS_SLA}}
(2) Konkrete Verfügbarkeits- und Servicezusagen: {{SLA_URL}}.
{{/IF}}
(3) Support-Kanäle: {{SUPPORT_CHANNELS_TEXT}}.
---
## § 8 Datensicherung und Datenexport
(1) Der Anbieter trifft angemessene Maßnahmen zur Datensicherheit.
(2) Der Kunde ist verantwortlich für die Sicherung seiner Daten, soweit Export-Funktionen verfügbar sind.
{{#IF HAS_EXPORT_POLICY}}
(3) Datenexport/Löschung: {{EXPORT_POLICY_TEXT}}.
{{/IF}}
---
## § 9 Datenschutz
Die Verarbeitung personenbezogener Daten erfolgt gemäß Datenschutzerklärung: {{PRIVACY_POLICY_URL}}.
---
## § 10 Gewährleistung
{{#IF IS_B2B}}
(1) Gegenüber Unternehmern werden Mängel nach Wahl des Anbieters durch Nachbesserung oder Ersatzleistung behoben.
(2) Offensichtliche Mängel sind unverzüglich anzuzeigen.
{{/IF}}
{{#IF IS_B2C}}
(1) Gegenüber Verbrauchern gelten die gesetzlichen Gewährleistungsrechte.
{{/IF}}
---
## § 11 Haftung
(1) Der Anbieter haftet unbeschränkt bei Vorsatz, grober Fahrlässigkeit und Schäden aus Verletzung von Leib, Leben oder Gesundheit.
(2) Bei leicht fahrlässiger Verletzung wesentlicher Vertragspflichten ist die Haftung auf den typischen, vorhersehbaren Schaden begrenzt.
(3) Im Übrigen ist die Haftung ausgeschlossen, soweit gesetzlich zulässig.
(4) Für Datenverlust haftet der Anbieter nur, soweit der Schaden auch bei ordnungsgemäßer Datensicherung eingetreten wäre.
{{#IF IS_B2B}}
(5) Haftungsdeckel (B2B): {{LIMITATION_CAP_TEXT}}.
{{/IF}}
---
## § 12 Vertragslaufzeit und Kündigung
(1) {{CONTRACT_TERM_TEXT}}.
(2) Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.
---
## § 13 Widerrufsrecht (nur Verbraucher)
{{#IF HAS_WITHDRAWAL}}
{{CONSUMER_WITHDRAWAL_TEXT}}
{{/IF}}
{{#IF_NOT HAS_WITHDRAWAL}}
Dieser Abschnitt ist nicht anwendbar (kein Verbrauchervertrag).
{{/IF_NOT}}
---
## § 14 Änderungen von Leistungen und AGB
(1) Der Anbieter kann diese AGB aus sachlichem Grund ändern (z. B. Gesetzesänderung).
(2) Änderungen werden in Textform mit angemessener Frist angekündigt.
(3) Ohne Widerspruch innerhalb der Frist gelten Änderungen als angenommen. Verbraucher werden auf ihr Widerspruchsrecht hingewiesen.
---
## § 15 Schlussbestimmungen
(1) Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts.
{{#IF IS_B2B}}
(2) Gerichtsstand gegenüber Kaufleuten/juristischen Personen: {{JURISDICTION_CITY}}.
{{/IF}}
(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt die Wirksamkeit der übrigen Bestimmungen unberührt.
""",
},
{
'document_type': 'impressum',
'language': 'de',
'title': 'Impressum DE (DDG § 5, IF-Blöcke)',
'description': 'Impressum nach § 5 DDG (Digitale-Dienste-Gesetz) mit optionalen Abschnitten für Registereintrag, USt-ID, reglementierten Beruf, redaktionell Verantwortlichen und Streitbeilegung.',
'jurisdiction': 'DE',
'license_id': 'mit',
'license_name': 'MIT License',
'is_complete_document': True,
'attribution_required': False,
'source_name': 'BreakPilot Compliance',
'content': """\
# Impressum
Angaben gemäß § 5 DDG
---
**Anbieter:**
{{COMPANY_LEGAL_NAME}} ({{COMPANY_LEGAL_FORM}})
{{COMPANY_ADDRESS_LINE}}
{{COMPANY_POSTAL_CODE}} {{COMPANY_CITY}}
{{COMPANY_COUNTRY}}
**Vertreten durch:** {{REPRESENTED_BY_NAME}}
**Kontakt:**
{{#IF CONTACT_PHONE}}Telefon: {{CONTACT_PHONE}}
{{/IF}}E-Mail: {{CONTACT_EMAIL}}
Website: {{WEBSITE_URL}}
---
{{#IF HAS_REGISTER}}
**Registereintrag:**
Registergericht: {{REGISTER_COURT}}
Registernummer: {{REGISTER_NUMBER}}
---
{{/IF}}
{{#IF HAS_VAT_ID}}
**Umsatzsteuer-ID:**
USt-IdNr. gemäß § 27a UStG: {{VAT_ID}}
---
{{/IF}}
{{#IF HAS_REGULATED_PROFESSION}}
**Angaben zur reglementierten Tätigkeit / Berufsrecht:**
{{REGULATED_PROFESSION_TEXT}}
---
{{/IF}}
{{#IF HAS_EDITORIAL_RESPONSIBLE}}
**Verantwortlich für journalistisch-redaktionelle Inhalte (§ 18 Abs. 2 MStV):**
{{EDITORIAL_RESPONSIBLE_NAME}}
{{EDITORIAL_RESPONSIBLE_ADDRESS}}
---
{{/IF}}
{{#IF HAS_DISPUTE_RESOLUTION}}
**Verbraucherstreitbeilegung / Online-Streitbeilegung:**
{{DISPUTE_RESOLUTION_TEXT}}
---
{{/IF}}
## Haftung für Inhalte
Als Diensteanbieter sind wir gemäß den allgemeinen Gesetzen für eigene Inhalte auf diesen Seiten verantwortlich. Wir übernehmen jedoch keine Gewähr für die Richtigkeit, Vollständigkeit und Aktualität der bereitgestellten Inhalte, soweit gesetzlich zulässig.
## Haftung für Links
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte keine Gewähr übernehmen. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
## Urheberrecht
Die durch den Seitenbetreiber erstellten Inhalte auf diesen Seiten unterliegen dem deutschen Urheberrecht. Downloads und Kopien dieser Seite sind nur für den privaten Gebrauch gestattet, sofern nicht ausdrücklich anders angegeben.
""",
},
]
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
def main() -> None:
print('=== Migration 023: 4 neue DE-Templates ===\n')
with engine.begin() as conn:
for t in TEMPLATES:
# Check if title already exists
existing = conn.execute(
text('SELECT id FROM public.compliance_legal_templates WHERE title = :title'),
{'title': t['title']}
).fetchone()
if existing:
print(f'[SKIP] Bereits vorhanden: {t["title"]}')
continue
# Extract placeholders from content
import re
placeholders = sorted(set(re.findall(r'\{\{[A-Z_]+\}\}', t['content'])))
new_id = str(uuid.uuid4())
conn.execute(
text('''
INSERT INTO public.compliance_legal_templates
(id, title, document_type, language, jurisdiction,
content, description, license_id, license_name,
source_name, is_complete_document, attribution_required,
placeholders, created_at, updated_at)
VALUES
(:id, :title, :document_type, :language, :jurisdiction,
:content, :description, :license_id, :license_name,
:source_name, :is_complete_document, :attribution_required,
:placeholders, NOW(), NOW())
'''),
{
'id': new_id,
'title': t['title'],
'document_type': t['document_type'],
'language': t['language'],
'jurisdiction': t['jurisdiction'],
'content': t['content'],
'description': t['description'],
'license_id': t['license_id'],
'license_name': t['license_name'],
'source_name': t['source_name'],
'is_complete_document': t['is_complete_document'],
'attribution_required': t['attribution_required'],
'placeholders': placeholders,
}
)
print(f'[OK] {t["title"]} ({t["document_type"]}/{t["language"]}) — {len(placeholders)} Platzhalter')
print('\n=== Done ===')
if __name__ == '__main__':
main()