From e0f7f2134e1e6f003e22d70f4dc1ee906b6a664a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 4 Mar 2026 14:35:56 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Template-Spec=20v1=20Phase=20C=20?= =?UTF-8?q?=E2=80=94=20IF-Renderer=20+=20HOSTING/FEATURES=20+=204=20neue?= =?UTF-8?q?=20DE-Templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../sdk/document-generator/contextBridge.ts | 146 +++- .../app/sdk/document-generator/page.tsx | 93 ++- .../sdk/document-generator/ruleEngine.test.ts | 165 +++++ .../app/sdk/document-generator/ruleEngine.ts | 77 ++ .../migrations/023_new_templates_de.sql | 54 ++ scripts/apply_templates_023.py | 683 ++++++++++++++++++ 6 files changed, 1208 insertions(+), 10 deletions(-) create mode 100644 backend-compliance/migrations/023_new_templates_de.sql create mode 100644 scripts/apply_templates_023.py diff --git a/admin-compliance/app/sdk/document-generator/contextBridge.ts b/admin-compliance/app/sdk/document-generator/contextBridge.ts index 35f0815..e29bf01 100644 --- a/admin-compliance/app/sdk/document-generator/contextBridge.ts +++ b/admin-compliance/app/sdk/document-generator/contextBridge.ts @@ -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 = { - 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}}'], } /** diff --git a/admin-compliance/app/sdk/document-generator/page.tsx b/admin-compliance/app/sdk/document-generator/page.tsx index 94799e4..51f2dfc 100644 --- a/admin-compliance/app/sdk/document-generator/page.tsx +++ b/admin-compliance/app/sdk/document-generator/page.tsx @@ -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 = { 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 = { { 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 setContext((prev) => ({ ...prev, PROVIDER: { ...prev.PROVIDER, - LEGAL_NAME: profile.companyName || prev.PROVIDER.LEGAL_NAME, - EMAIL: (profile as Record).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, }, })) } diff --git a/admin-compliance/app/sdk/document-generator/ruleEngine.test.ts b/admin-compliance/app/sdk/document-generator/ruleEngine.test.ts index 0080d20..4090475 100644 --- a/admin-compliance/app/sdk/document-generator/ruleEngine.test.ts +++ b/admin-compliance/app/sdk/document-generator/ruleEngine.test.ts @@ -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') + }) +}) diff --git a/admin-compliance/app/sdk/document-generator/ruleEngine.ts b/admin-compliance/app/sdk/document-generator/ruleEngine.ts index 9646da3..9817c4e 100644 --- a/admin-compliance/app/sdk/document-generator/ruleEngine.ts +++ b/admin-compliance/app/sdk/document-generator/ruleEngine.ts @@ -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 { + 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 { + 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 // ============================================================================= diff --git a/backend-compliance/migrations/023_new_templates_de.sql b/backend-compliance/migrations/023_new_templates_de.sql new file mode 100644 index 0000000..f039729 --- /dev/null +++ b/backend-compliance/migrations/023_new_templates_de.sql @@ -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'; diff --git a/scripts/apply_templates_023.py b/scripts/apply_templates_023.py new file mode 100644 index 0000000..87d79e8 --- /dev/null +++ b/scripts/apply_templates_023.py @@ -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()