From feedeb052f17df3b94c42093a72fc1b4d88b607c Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:32:08 +0200 Subject: [PATCH] refactor(admin-compliance): split 11 oversized files under 500 LOC hard cap (batch 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Barrel-split pattern: each original becomes a thin re-export barrel; logic moved to sibling files so no consumer imports need updating. Files split: - loeschfristen-profiling.ts → profiling-data.ts + profiling-generator.ts - vendor-compliance/catalog/vendor-templates.ts → vendor-country-profiles.ts - vendor-compliance/catalog/legal-basis.ts → legal-basis-retention.ts - dsfa/eu-legal-frameworks.ts → eu-legal-frameworks-national.ts - compliance-scope-types/document-scope-matrix-core.ts → core-part2.ts - compliance-scope-types/document-scope-matrix-extended.ts → extended-part2.ts - app/sdk/document-generator/contextBridge.ts → contextBridge-helpers.ts - app/api/sdk/drafting-engine/draft/route.ts → draft-helpers.ts + draft-helpers-v2.ts All files ≤ 500 LOC. Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 --- .../drafting-engine/draft/draft-helpers-v2.ts | 364 ++++++++++ .../drafting-engine/draft/draft-helpers.ts | 161 +++++ .../api/sdk/drafting-engine/draft/route.ts | 622 +----------------- .../contextBridge-helpers.ts | 277 ++++++++ .../sdk/document-generator/contextBridge.ts | 271 +------- .../document-scope-matrix-core-part2.ts | 249 +++++++ .../document-scope-matrix-core.ts | 243 +------ .../document-scope-matrix-extended-part2.ts | 286 ++++++++ .../document-scope-matrix-extended.ts | 279 +------- .../sdk/dsfa/eu-legal-frameworks-national.ts | 322 +++++++++ .../lib/sdk/dsfa/eu-legal-frameworks.ts | 400 +---------- .../sdk/loeschfristen-profiling-generator.ts | 290 ++++++++ .../lib/sdk/loeschfristen-profiling.ts | 580 +--------------- .../catalog/legal-basis-retention.ts | 215 ++++++ .../vendor-compliance/catalog/legal-basis.ts | 212 +----- .../catalog/vendor-country-profiles.ts | 127 ++++ .../catalog/vendor-templates.ts | 123 +--- 17 files changed, 2381 insertions(+), 2640 deletions(-) create mode 100644 admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts create mode 100644 admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts create mode 100644 admin-compliance/app/sdk/document-generator/contextBridge-helpers.ts create mode 100644 admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core-part2.ts create mode 100644 admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended-part2.ts create mode 100644 admin-compliance/lib/sdk/dsfa/eu-legal-frameworks-national.ts create mode 100644 admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis-retention.ts create mode 100644 admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-country-profiles.ts diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts new file mode 100644 index 0000000..6f3baec --- /dev/null +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts @@ -0,0 +1,364 @@ +/** + * Drafting Engine - v2 Pipeline Helpers + * + * DOCUMENT_PROSE_BLOCKS, buildV2SystemPrompt, buildBlockSpecificPrompt, + * callOllama, handleV2Draft — split from draft-helpers.ts for the 500 LOC hard cap. + */ + +import { NextResponse } from 'next/server' +import type { DraftContext, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { deriveNarrativeTags, extractScoresFromDraftContext, narrativeTagsToPromptString } from '@/lib/sdk/drafting-engine/narrative-tags' +import type { NarrativeTags } from '@/lib/sdk/drafting-engine/narrative-tags' +import { buildAllowedFactsFromDraftContext, allowedFactsToPromptString, disallowedTopicsToPromptString } from '@/lib/sdk/drafting-engine/allowed-facts-v2' +import { sanitizeAllowedFacts, validateNoRemainingPII, SanitizationError } from '@/lib/sdk/drafting-engine/sanitizer' +import { terminologyToPromptString, styleContractToPromptString } from '@/lib/sdk/drafting-engine/terminology' +import { executeRepairLoop, type ProseBlockOutput, type RepairAudit } from '@/lib/sdk/drafting-engine/prose-validator' +import { computeChecksumSync, type CacheKeyParams } from '@/lib/sdk/drafting-engine/cache' +import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' +import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' +import { + constraintEnforcer, + proseCache, + TEMPLATE_VERSION, + TERMINOLOGY_VERSION, + VALIDATOR_VERSION, + V1_SYSTEM_PROMPT, + buildPromptForDocumentType, +} from './draft-helpers' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// ============================================================================ +// v2 Personalisierte Pipeline +// ============================================================================ + +export const DOCUMENT_PROSE_BLOCKS: Record> = { + tom: [ + { blockId: 'tom-intro', blockType: 'introduction', sectionName: 'Einleitung TOM', targetWords: 120 }, + { blockId: 'tom-transition', blockType: 'transition', sectionName: 'Ueberleitung Massnahmen', targetWords: 40 }, + { blockId: 'tom-conclusion', blockType: 'conclusion', sectionName: 'Fazit TOM', targetWords: 80 }, + ], + dsfa: [ + { blockId: 'dsfa-intro', blockType: 'introduction', sectionName: 'Einleitung DSFA', targetWords: 150 }, + { blockId: 'dsfa-transition', blockType: 'transition', sectionName: 'Ueberleitung Risikobewertung', targetWords: 40 }, + { blockId: 'dsfa-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Massnahmen', targetWords: 60 }, + { blockId: 'dsfa-conclusion', blockType: 'conclusion', sectionName: 'Fazit DSFA', targetWords: 100 }, + ], + vvt: [ + { blockId: 'vvt-intro', blockType: 'introduction', sectionName: 'Einleitung VVT', targetWords: 120 }, + { blockId: 'vvt-conclusion', blockType: 'conclusion', sectionName: 'Fazit VVT', targetWords: 80 }, + ], + dsi: [ + { blockId: 'dsi-intro', blockType: 'introduction', sectionName: 'Einleitung Datenschutzerklaerung', targetWords: 130 }, + { blockId: 'dsi-conclusion', blockType: 'conclusion', sectionName: 'Fazit Datenschutzerklaerung', targetWords: 80 }, + ], + lf: [ + { blockId: 'lf-intro', blockType: 'introduction', sectionName: 'Einleitung Loeschfristen', targetWords: 100 }, + { blockId: 'lf-conclusion', blockType: 'conclusion', sectionName: 'Fazit Loeschfristen', targetWords: 60 }, + ], + av_vertrag: [ + { blockId: 'av-intro', blockType: 'introduction', sectionName: 'Einleitung Auftragsverarbeitung', targetWords: 130 }, + { blockId: 'av-conclusion', blockType: 'conclusion', sectionName: 'Fazit Auftragsverarbeitung', targetWords: 80 }, + ], + betroffenenrechte: [ + { blockId: 'betr-intro', blockType: 'introduction', sectionName: 'Einleitung Betroffenenrechte', targetWords: 120 }, + { blockId: 'betr-conclusion', blockType: 'conclusion', sectionName: 'Fazit Betroffenenrechte', targetWords: 80 }, + ], + risikoanalyse: [ + { blockId: 'risk-intro', blockType: 'introduction', sectionName: 'Einleitung Risikoanalyse', targetWords: 130 }, + { blockId: 'risk-conclusion', blockType: 'conclusion', sectionName: 'Fazit Risikoanalyse', targetWords: 80 }, + ], + notfallplan: [ + { blockId: 'notfall-intro', blockType: 'introduction', sectionName: 'Einleitung Notfallplan', targetWords: 120 }, + { blockId: 'notfall-conclusion', blockType: 'conclusion', sectionName: 'Fazit Notfallplan', targetWords: 80 }, + ], + iace_ce_assessment: [ + { blockId: 'iace-intro', blockType: 'introduction', sectionName: 'Einleitung IACE CE-Bewertung', targetWords: 150 }, + { blockId: 'iace-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Konformitaetsbewertung', targetWords: 60 }, + { blockId: 'iace-conclusion', blockType: 'conclusion', sectionName: 'Fazit IACE CE-Bewertung', targetWords: 100 }, + ], +} + +export function buildV2SystemPrompt( + sanitizedFactsString: string, + narrativeTagsString: string, + terminologyString: string, + styleString: string, + disallowedString: string, + companyName: string, + blockId: string, + blockType: string, + sectionName: string, + documentType: string, + targetWords: number +): string { + return `Du bist ein Compliance-Dokumenten-Redakteur. +Du schreibst einzelne Textabschnitte fuer offizielle Compliance-Dokumente. + +KUNDENPROFIL (ERLAUBTE FAKTEN — nur diese darfst du verwenden): +${sanitizedFactsString} + +BEWERTUNGSERGEBNIS (sprachliche Tags — verwende nur diese Begriffe): +${narrativeTagsString} + +TERMINOLOGIE (verwende ausschliesslich diese Fachbegriffe): +${terminologyString} + +STIL: +${styleString} + +VERBOTENE INHALTE: +${disallowedString} +- Keine konkreten Prozentwerte, Scores oder Zahlen +- Keine Compliance-Level-Bezeichnungen (L1, L2, L3, L4) +- Keine direkte Ansprache ("Sie", "Ihr") +- Kein Denglisch, keine Marketing-Sprache, keine Superlative + +STRIKTE REGELN: +1. Verwende den Firmennamen "${companyName}" — nie "Ihr Unternehmen" +2. Schreibe in der dritten Person ("Die ${companyName}...") +3. Beziehe dich auf die Branche und organisatorische Merkmale +4. Verwende NUR Fakten aus dem Kundenprofil oben +5. Verwende NUR die sprachlichen Tags aus dem Bewertungsergebnis +6. Erfinde KEINE zusaetzlichen Fakten oder Bewertungen +7. Halte dich an die Terminologie-Vorgaben +8. Dein Text wird ZWISCHEN feste Datentabellen eingefuegt + +OUTPUT-FORMAT: Antworte ausschliesslich als JSON: +{ + "blockId": "${blockId}", + "blockType": "${blockType}", + "language": "de", + "text": "...", + "assertions": { + "companyNameUsed": true/false, + "industryReferenced": true/false, + "structureReferenced": true/false, + "itLandscapeReferenced": true/false, + "narrativeTagsUsed": ["riskSummary", ...] + }, + "forbiddenContentDetected": [] +} + +DOKUMENTENTYP: ${documentType} +SEKTION: ${sectionName} +BLOCK-TYP: ${blockType} +ZIEL-LAENGE: ${targetWords} Woerter` +} + +export function buildBlockSpecificPrompt(blockType: string, sectionName: string, documentType: string): string { + switch (blockType) { + case 'introduction': + return `Schreibe eine Einleitung fuer das Dokument "${documentType}" (Sektion: ${sectionName}). +Erklaere, warum dieses Dokument fuer das Unternehmen erstellt wurde. +Gehe auf die spezifische Situation des Unternehmens ein. +Erwaehne die Branche, die Organisationsform und die IT-Strategie.` + case 'transition': + return `Schreibe eine kurze Ueberleitung zur naechsten Sektion "${sectionName}". +Verknuepfe den vorherigen Abschnitt logisch mit dem folgenden.` + case 'conclusion': + return `Schreibe einen abschliessenden Absatz fuer die Sektion "${sectionName}". +Fasse die wesentlichen Punkte zusammen und verweise auf die fortlaufende Pflege.` + case 'appreciation': + return `Schreibe einen wertschaetzenden Satz ueber die bestehenden Massnahmen. +Verwende dabei die sprachlichen Tags aus dem Bewertungsergebnis. +Keine neuen Fakten erfinden — nur das Profil wuerdigen.` + default: + return `Schreibe einen Textabschnitt fuer "${sectionName}".` + } +} + +export async function callOllama(systemPrompt: string, userPrompt: string): Promise { + const response = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + stream: false, + think: false, + options: { temperature: 0.15, num_predict: 4096, num_ctx: 8192 }, + format: 'json', + }), + signal: AbortSignal.timeout(120000), + }) + + if (!response.ok) { + throw new Error(`Ollama error: ${response.status}`) + } + + const result = await response.json() + return result.message?.content || '' +} + +export async function handleV2Draft(body: Record): Promise { + const { documentType, draftContext, instructions } = body as { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + } + + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, constraintCheck, tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + const scores = extractScoresFromDraftContext(draftContext) + const narrativeTags: NarrativeTags = deriveNarrativeTags(scores) + const allowedFacts = buildAllowedFactsFromDraftContext(draftContext, narrativeTags) + + let sanitizationResult + try { + sanitizationResult = sanitizeAllowedFacts(allowedFacts) + } catch (error) { + if (error instanceof SanitizationError) { + return NextResponse.json({ + error: `Sanitization Hard Abort: ${error.message} (Feld: ${error.field})`, + draft: null, constraintCheck, tokensUsed: 0, + }, { status: 422 }) + } + throw error + } + + const sanitizedFacts = sanitizationResult.facts + const piiWarnings = validateNoRemainingPII(sanitizedFacts) + if (piiWarnings.length > 0) console.warn('PII-Warnungen nach Sanitization:', piiWarnings) + + const factsString = allowedFactsToPromptString(sanitizedFacts) + const tagsString = narrativeTagsToPromptString(narrativeTags) + const termsString = terminologyToPromptString() + const styleString = styleContractToPromptString() + const disallowedString = disallowedTopicsToPromptString() + const promptHash = computeChecksumSync({ factsString, tagsString, termsString, styleString, disallowedString }) + + const v2RagCfg = DOCUMENT_RAG_CONFIG[documentType] + const v2RagContext = await queryRAG(v2RagCfg.query, 3, v2RagCfg.collection) + + const proseBlocks = DOCUMENT_PROSE_BLOCKS[documentType] || DOCUMENT_PROSE_BLOCKS.tom + const generatedBlocks: ProseBlockOutput[] = [] + const repairAudits: RepairAudit[] = [] + let totalTokens = 0 + + for (const blockDef of proseBlocks) { + const cacheParams: CacheKeyParams = { + allowedFacts: sanitizedFacts, templateVersion: TEMPLATE_VERSION, + terminologyVersion: TERMINOLOGY_VERSION, narrativeTags, + promptHash, blockType: blockDef.blockType, sectionName: blockDef.sectionName, + } + + const cached = proseCache.getSync(cacheParams) + if (cached) { + generatedBlocks.push(cached) + repairAudits.push({ repairAttempts: 0, validatorFailures: [], repairSuccessful: true, fallbackUsed: false }) + continue + } + + let systemPrompt = buildV2SystemPrompt( + factsString, tagsString, termsString, styleString, disallowedString, + sanitizedFacts.companyName, blockDef.blockId, blockDef.blockType, + blockDef.sectionName, documentType, blockDef.targetWords + ) + if (v2RagContext) systemPrompt += `\n\nRECHTSKONTEXT (als Referenz, nicht woertlich uebernehmen):\n${v2RagContext}` + const userPrompt = buildBlockSpecificPrompt(blockDef.blockType, blockDef.sectionName, documentType) + + (instructions ? `\n\nZusaetzliche Anweisungen: ${instructions}` : '') + + try { + const rawOutput = await callOllama(systemPrompt, userPrompt) + totalTokens += rawOutput.length / 4 + const { block, audit } = await executeRepairLoop( + rawOutput, sanitizedFacts, narrativeTags, blockDef.blockId, blockDef.blockType, + async (repairPrompt) => callOllama(systemPrompt, repairPrompt), documentType + ) + generatedBlocks.push(block) + repairAudits.push(audit) + if (!audit.fallbackUsed) proseCache.setSync(cacheParams, block) + } catch (error) { + const { buildFallbackBlock } = await import('@/lib/sdk/drafting-engine/prose-validator') + generatedBlocks.push(buildFallbackBlock(blockDef.blockId, blockDef.blockType, sanitizedFacts, documentType)) + repairAudits.push({ + repairAttempts: 0, validatorFailures: [[(error as Error).message]], + repairSuccessful: false, fallbackUsed: true, + fallbackReason: `LLM-Fehler: ${(error as Error).message}`, + }) + } + } + + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + + let dataSections: DraftSection[] = [] + try { + const dataResponse = await callOllama(V1_SYSTEM_PROMPT, draftPrompt) + const parsed = JSON.parse(dataResponse) + dataSections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), title: String(s.title || ''), + content: String(s.content || ''), schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + totalTokens += dataResponse.length / 4 + } catch { dataSections = [] } + + const introBlock = generatedBlocks.find(b => b.blockType === 'introduction') + const transitionBlocks = generatedBlocks.filter(b => b.blockType === 'transition') + const appreciationBlocks = generatedBlocks.filter(b => b.blockType === 'appreciation') + const conclusionBlock = generatedBlocks.find(b => b.blockType === 'conclusion') + + const mergedSections: DraftSection[] = [] + if (introBlock) mergedSections.push({ id: introBlock.blockId, title: 'Einleitung', content: introBlock.text }) + for (let i = 0; i < dataSections.length; i++) { + if (i > 0 && transitionBlocks[i - 1]) mergedSections.push({ id: transitionBlocks[i - 1].blockId, title: '', content: transitionBlocks[i - 1].text }) + mergedSections.push(dataSections[i]) + } + for (const block of appreciationBlocks) mergedSections.push({ id: block.blockId, title: 'Wuerdigung', content: block.text }) + if (conclusionBlock) mergedSections.push({ id: conclusionBlock.blockId, title: 'Fazit', content: conclusionBlock.text }) + + const finalSections = mergedSections.length > 0 ? mergedSections : generatedBlocks.map(b => ({ + id: b.blockId, + title: b.blockType === 'introduction' ? 'Einleitung' : b.blockType === 'conclusion' ? 'Fazit' : b.blockType === 'appreciation' ? 'Wuerdigung' : 'Ueberleitung', + content: b.text, + })) + + const draft: DraftRevision = { + id: `draft-v2-${Date.now()}`, + content: finalSections.map(s => s.title ? `## ${s.title}\n\n${s.content}` : s.content).join('\n\n'), + sections: finalSections, createdAt: new Date().toISOString(), instruction: instructions, + } + + const auditTrail = { + documentType, templateVersion: TEMPLATE_VERSION, terminologyVersion: TERMINOLOGY_VERSION, + validatorVersion: VALIDATOR_VERSION, promptHash, llmModel: LLM_MODEL, + llmTemperature: 0.15, llmProvider: 'ollama', narrativeTags, + sanitization: sanitizationResult.audit, repairAudits, + proseBlocks: generatedBlocks.map((b, i) => ({ + blockId: b.blockId, blockType: b.blockType, + wordCount: b.text.split(/\s+/).filter(Boolean).length, + fallbackUsed: repairAudits[i]?.fallbackUsed ?? false, + repairAttempts: repairAudits[i]?.repairAttempts ?? 0, + })), + cacheStats: proseCache.getStats(), + } + + const truthLabel = { generation_mode: 'draft_assistance', truth_status: 'generated', may_be_used_as_evidence: false, generated_by: 'system' } + + try { + const BACKEND_URL = process.env.BACKEND_COMPLIANCE_URL || 'http://backend-compliance:8002' + fetch(`${BACKEND_URL}/api/compliance/llm-audit`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + entity_type: 'document', entity_id: null, generation_mode: 'draft_assistance', + truth_status: 'generated', may_be_used_as_evidence: false, + llm_model: LLM_MODEL, llm_provider: 'ollama', + input_summary: `${documentType} draft generation`, + output_summary: draft?.sections?.length ? `${draft.sections.length} sections generated` : 'draft generated', + }), + }).catch(() => {/* fire-and-forget */}) + } catch { /* LLM audit persistence failure should not block the response */ } + + return NextResponse.json({ draft, constraintCheck, tokensUsed: Math.round(totalTokens), pipelineVersion: 'v2', auditTrail, truthLabel }) +} diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts new file mode 100644 index 0000000..60d88a7 --- /dev/null +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers.ts @@ -0,0 +1,161 @@ +/** + * Drafting Engine - Draft Helper Functions (v1 pipeline + shared constants) + * + * Shared state, v1 legacy pipeline helpers. + * v2 pipeline lives in draft-helpers-v2.ts. + */ + +import { NextResponse } from 'next/server' +import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' +import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' +import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' +import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' +import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' +import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' +import { ProseCacheManager } from '@/lib/sdk/drafting-engine/cache' +import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' +import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +export const constraintEnforcer = new ConstraintEnforcer() +export const proseCache = new ProseCacheManager({ maxEntries: 200, ttlHours: 24 }) + +export const TEMPLATE_VERSION = '2.0.0' +export const TERMINOLOGY_VERSION = '1.0.0' +export const VALIDATOR_VERSION = '1.0.0' + +// ============================================================================ +// v1 Legacy Pipeline +// ============================================================================ + +export const V1_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. +Du MUSST immer im JSON-Format antworten mit einem "sections" Array. +Jede Section hat: id, title, content, schemaField. +Halte die Tiefe strikt am vorgegebenen Level. +Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. +Sprache: Deutsch.` + +export function buildPromptForDocumentType( + documentType: ScopeDocumentType, + context: DraftContext, + instructions?: string +): string { + switch (documentType) { + case 'vvt': + return buildVVTDraftPrompt({ context, instructions }) + case 'tom': + return buildTOMDraftPrompt({ context, instructions }) + case 'dsfa': + return buildDSFADraftPrompt({ context, instructions }) + case 'dsi': + return buildPrivacyPolicyDraftPrompt({ context, instructions }) + case 'lf': + return buildLoeschfristenDraftPrompt({ context, instructions }) + default: + return `## Aufgabe: Entwurf fuer ${documentType} + +### Level: ${context.decisions.level} +### Tiefe: ${context.constraints.depthRequirements.depth} +### Erforderliche Inhalte: +${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Anweisungen: ${instructions}` : ''} + +Antworte als JSON mit "sections" Array.` + } +} + +export async function handleV1Draft(body: Record): Promise { + const { documentType, draftContext, instructions, existingDraft } = body as { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + existingDraft?: DraftRevision + } + + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, + constraintCheck, + tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + const ragCfg = DOCUMENT_RAG_CONFIG[documentType] + const ragContext = await queryRAG(ragCfg.query, 3, ragCfg.collection) + + let v1SystemPrompt = V1_SYSTEM_PROMPT + if (ragContext) { + v1SystemPrompt += `\n\n## Relevanter Rechtskontext\n${ragContext}` + } + + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + const messages = [ + { role: 'system', content: v1SystemPrompt }, + ...(existingDraft ? [{ + role: 'assistant', + content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, + }] : []), + { role: 'user', content: draftPrompt }, + ] + + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: false, + think: false, + options: { temperature: 0.15, num_predict: 16384, num_ctx: 8192 }, + format: 'json', + }), + signal: AbortSignal.timeout(180000), + }) + + if (!ollamaResponse.ok) { + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + const result = await ollamaResponse.json() + const content = result.message?.content || '' + + let sections: DraftSection[] = [] + try { + const parsed = JSON.parse(content) + sections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), + title: String(s.title || ''), + content: String(s.content || ''), + schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + } catch { + sections = [{ id: 'raw', title: 'Entwurf', content }] + } + + const draft: DraftRevision = { + id: `draft-${Date.now()}`, + content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), + sections, + createdAt: new Date().toISOString(), + instruction: instructions as string | undefined, + } + + return NextResponse.json({ + draft, + constraintCheck, + tokensUsed: result.eval_count || 0, + } satisfies DraftResponse) +} + +// Re-export v2 handler for route.ts (backward compat — single import point) +export { handleV2Draft } from './draft-helpers-v2' diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts index 272a015..3b221c0 100644 --- a/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/route.ts @@ -9,627 +9,7 @@ */ import { NextRequest, NextResponse } from 'next/server' - -const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' -const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' - -// v1 imports (Legacy) -import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' -import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' -import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' -import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' -import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' -import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' -import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' -import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' - -// v2 imports (Personalisierte Pipeline) -import { deriveNarrativeTags, extractScoresFromDraftContext, narrativeTagsToPromptString } from '@/lib/sdk/drafting-engine/narrative-tags' -import type { NarrativeTags } from '@/lib/sdk/drafting-engine/narrative-tags' -import { buildAllowedFactsFromDraftContext, allowedFactsToPromptString, disallowedTopicsToPromptString } from '@/lib/sdk/drafting-engine/allowed-facts-v2' -import { sanitizeAllowedFacts, validateNoRemainingPII, SanitizationError } from '@/lib/sdk/drafting-engine/sanitizer' -import { terminologyToPromptString, styleContractToPromptString } from '@/lib/sdk/drafting-engine/terminology' -import { executeRepairLoop, type ProseBlockOutput, type RepairAudit } from '@/lib/sdk/drafting-engine/prose-validator' -import { ProseCacheManager, computeChecksumSync, type CacheKeyParams } from '@/lib/sdk/drafting-engine/cache' -import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query' -import { DOCUMENT_RAG_CONFIG } from '@/lib/sdk/drafting-engine/rag-config' - -// ============================================================================ -// Shared State -// ============================================================================ - -const constraintEnforcer = new ConstraintEnforcer() -const proseCache = new ProseCacheManager({ maxEntries: 200, ttlHours: 24 }) - -// Template/Terminology Versionen (fuer Cache-Key) -const TEMPLATE_VERSION = '2.0.0' -const TERMINOLOGY_VERSION = '1.0.0' -const VALIDATOR_VERSION = '1.0.0' - -// ============================================================================ -// v1 Legacy Pipeline -// ============================================================================ - -const V1_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. -Du MUSST immer im JSON-Format antworten mit einem "sections" Array. -Jede Section hat: id, title, content, schemaField. -Halte die Tiefe strikt am vorgegebenen Level. -Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung]. -Sprache: Deutsch.` - -function buildPromptForDocumentType( - documentType: ScopeDocumentType, - context: DraftContext, - instructions?: string -): string { - switch (documentType) { - case 'vvt': - return buildVVTDraftPrompt({ context, instructions }) - case 'tom': - return buildTOMDraftPrompt({ context, instructions }) - case 'dsfa': - return buildDSFADraftPrompt({ context, instructions }) - case 'dsi': - return buildPrivacyPolicyDraftPrompt({ context, instructions }) - case 'lf': - return buildLoeschfristenDraftPrompt({ context, instructions }) - default: - return `## Aufgabe: Entwurf fuer ${documentType} - -### Level: ${context.decisions.level} -### Tiefe: ${context.constraints.depthRequirements.depth} -### Erforderliche Inhalte: -${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} - -${instructions ? `### Anweisungen: ${instructions}` : ''} - -Antworte als JSON mit "sections" Array.` - } -} - -async function handleV1Draft(body: Record): Promise { - const { documentType, draftContext, instructions, existingDraft } = body as { - documentType: ScopeDocumentType - draftContext: DraftContext - instructions?: string - existingDraft?: DraftRevision - } - - const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) - if (!constraintCheck.allowed) { - return NextResponse.json({ - draft: null, - constraintCheck, - tokensUsed: 0, - error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), - }, { status: 403 }) - } - - // RAG: Fetch relevant legal context (config-based) - const ragCfg = DOCUMENT_RAG_CONFIG[documentType] - const ragContext = await queryRAG(ragCfg.query, 3, ragCfg.collection) - - let v1SystemPrompt = V1_SYSTEM_PROMPT - if (ragContext) { - v1SystemPrompt += `\n\n## Relevanter Rechtskontext\n${ragContext}` - } - - const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) - const messages = [ - { role: 'system', content: v1SystemPrompt }, - ...(existingDraft ? [{ - role: 'assistant', - content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, - }] : []), - { role: 'user', content: draftPrompt }, - ] - - const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: LLM_MODEL, - messages, - stream: false, - think: false, - options: { temperature: 0.15, num_predict: 16384, num_ctx: 8192 }, - format: 'json', - }), - signal: AbortSignal.timeout(180000), - }) - - if (!ollamaResponse.ok) { - return NextResponse.json( - { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, - { status: 502 } - ) - } - - const result = await ollamaResponse.json() - const content = result.message?.content || '' - - let sections: DraftSection[] = [] - try { - const parsed = JSON.parse(content) - sections = (parsed.sections || []).map((s: Record, i: number) => ({ - id: String(s.id || `section-${i}`), - title: String(s.title || ''), - content: String(s.content || ''), - schemaField: s.schemaField ? String(s.schemaField) : undefined, - })) - } catch { - sections = [{ id: 'raw', title: 'Entwurf', content }] - } - - const draft: DraftRevision = { - id: `draft-${Date.now()}`, - content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), - sections, - createdAt: new Date().toISOString(), - instruction: instructions as string | undefined, - } - - return NextResponse.json({ - draft, - constraintCheck, - tokensUsed: result.eval_count || 0, - } satisfies DraftResponse) -} - -// ============================================================================ -// v2 Personalisierte Pipeline -// ============================================================================ - -/** Prose block definitions per document type */ -const DOCUMENT_PROSE_BLOCKS: Record> = { - tom: [ - { blockId: 'tom-intro', blockType: 'introduction', sectionName: 'Einleitung TOM', targetWords: 120 }, - { blockId: 'tom-transition', blockType: 'transition', sectionName: 'Ueberleitung Massnahmen', targetWords: 40 }, - { blockId: 'tom-conclusion', blockType: 'conclusion', sectionName: 'Fazit TOM', targetWords: 80 }, - ], - dsfa: [ - { blockId: 'dsfa-intro', blockType: 'introduction', sectionName: 'Einleitung DSFA', targetWords: 150 }, - { blockId: 'dsfa-transition', blockType: 'transition', sectionName: 'Ueberleitung Risikobewertung', targetWords: 40 }, - { blockId: 'dsfa-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Massnahmen', targetWords: 60 }, - { blockId: 'dsfa-conclusion', blockType: 'conclusion', sectionName: 'Fazit DSFA', targetWords: 100 }, - ], - vvt: [ - { blockId: 'vvt-intro', blockType: 'introduction', sectionName: 'Einleitung VVT', targetWords: 120 }, - { blockId: 'vvt-conclusion', blockType: 'conclusion', sectionName: 'Fazit VVT', targetWords: 80 }, - ], - dsi: [ - { blockId: 'dsi-intro', blockType: 'introduction', sectionName: 'Einleitung Datenschutzerklaerung', targetWords: 130 }, - { blockId: 'dsi-conclusion', blockType: 'conclusion', sectionName: 'Fazit Datenschutzerklaerung', targetWords: 80 }, - ], - lf: [ - { blockId: 'lf-intro', blockType: 'introduction', sectionName: 'Einleitung Loeschfristen', targetWords: 100 }, - { blockId: 'lf-conclusion', blockType: 'conclusion', sectionName: 'Fazit Loeschfristen', targetWords: 60 }, - ], - av_vertrag: [ - { blockId: 'av-intro', blockType: 'introduction', sectionName: 'Einleitung Auftragsverarbeitung', targetWords: 130 }, - { blockId: 'av-conclusion', blockType: 'conclusion', sectionName: 'Fazit Auftragsverarbeitung', targetWords: 80 }, - ], - betroffenenrechte: [ - { blockId: 'betr-intro', blockType: 'introduction', sectionName: 'Einleitung Betroffenenrechte', targetWords: 120 }, - { blockId: 'betr-conclusion', blockType: 'conclusion', sectionName: 'Fazit Betroffenenrechte', targetWords: 80 }, - ], - risikoanalyse: [ - { blockId: 'risk-intro', blockType: 'introduction', sectionName: 'Einleitung Risikoanalyse', targetWords: 130 }, - { blockId: 'risk-conclusion', blockType: 'conclusion', sectionName: 'Fazit Risikoanalyse', targetWords: 80 }, - ], - notfallplan: [ - { blockId: 'notfall-intro', blockType: 'introduction', sectionName: 'Einleitung Notfallplan', targetWords: 120 }, - { blockId: 'notfall-conclusion', blockType: 'conclusion', sectionName: 'Fazit Notfallplan', targetWords: 80 }, - ], - iace_ce_assessment: [ - { blockId: 'iace-intro', blockType: 'introduction', sectionName: 'Einleitung IACE CE-Bewertung', targetWords: 150 }, - { blockId: 'iace-appreciation', blockType: 'appreciation', sectionName: 'Wuerdigung bestehender Konformitaetsbewertung', targetWords: 60 }, - { blockId: 'iace-conclusion', blockType: 'conclusion', sectionName: 'Fazit IACE CE-Bewertung', targetWords: 100 }, - ], -} - -function buildV2SystemPrompt( - sanitizedFactsString: string, - narrativeTagsString: string, - terminologyString: string, - styleString: string, - disallowedString: string, - companyName: string, - blockId: string, - blockType: string, - sectionName: string, - documentType: string, - targetWords: number -): string { - return `Du bist ein Compliance-Dokumenten-Redakteur. -Du schreibst einzelne Textabschnitte fuer offizielle Compliance-Dokumente. - -KUNDENPROFIL (ERLAUBTE FAKTEN — nur diese darfst du verwenden): -${sanitizedFactsString} - -BEWERTUNGSERGEBNIS (sprachliche Tags — verwende nur diese Begriffe): -${narrativeTagsString} - -TERMINOLOGIE (verwende ausschliesslich diese Fachbegriffe): -${terminologyString} - -STIL: -${styleString} - -VERBOTENE INHALTE: -${disallowedString} -- Keine konkreten Prozentwerte, Scores oder Zahlen -- Keine Compliance-Level-Bezeichnungen (L1, L2, L3, L4) -- Keine direkte Ansprache ("Sie", "Ihr") -- Kein Denglisch, keine Marketing-Sprache, keine Superlative - -STRIKTE REGELN: -1. Verwende den Firmennamen "${companyName}" — nie "Ihr Unternehmen" -2. Schreibe in der dritten Person ("Die ${companyName}...") -3. Beziehe dich auf die Branche und organisatorische Merkmale -4. Verwende NUR Fakten aus dem Kundenprofil oben -5. Verwende NUR die sprachlichen Tags aus dem Bewertungsergebnis -6. Erfinde KEINE zusaetzlichen Fakten oder Bewertungen -7. Halte dich an die Terminologie-Vorgaben -8. Dein Text wird ZWISCHEN feste Datentabellen eingefuegt - -OUTPUT-FORMAT: Antworte ausschliesslich als JSON: -{ - "blockId": "${blockId}", - "blockType": "${blockType}", - "language": "de", - "text": "...", - "assertions": { - "companyNameUsed": true/false, - "industryReferenced": true/false, - "structureReferenced": true/false, - "itLandscapeReferenced": true/false, - "narrativeTagsUsed": ["riskSummary", ...] - }, - "forbiddenContentDetected": [] -} - -DOKUMENTENTYP: ${documentType} -SEKTION: ${sectionName} -BLOCK-TYP: ${blockType} -ZIEL-LAENGE: ${targetWords} Woerter` -} - -function buildBlockSpecificPrompt(blockType: string, sectionName: string, documentType: string): string { - switch (blockType) { - case 'introduction': - return `Schreibe eine Einleitung fuer das Dokument "${documentType}" (Sektion: ${sectionName}). -Erklaere, warum dieses Dokument fuer das Unternehmen erstellt wurde. -Gehe auf die spezifische Situation des Unternehmens ein. -Erwaehne die Branche, die Organisationsform und die IT-Strategie.` - case 'transition': - return `Schreibe eine kurze Ueberleitung zur naechsten Sektion "${sectionName}". -Verknuepfe den vorherigen Abschnitt logisch mit dem folgenden.` - case 'conclusion': - return `Schreibe einen abschliessenden Absatz fuer die Sektion "${sectionName}". -Fasse die wesentlichen Punkte zusammen und verweise auf die fortlaufende Pflege.` - case 'appreciation': - return `Schreibe einen wertschaetzenden Satz ueber die bestehenden Massnahmen. -Verwende dabei die sprachlichen Tags aus dem Bewertungsergebnis. -Keine neuen Fakten erfinden — nur das Profil wuerdigen.` - default: - return `Schreibe einen Textabschnitt fuer "${sectionName}".` - } -} - -async function callOllama(systemPrompt: string, userPrompt: string): Promise { - const response = await fetch(`${OLLAMA_URL}/api/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: LLM_MODEL, - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: userPrompt }, - ], - stream: false, - think: false, - options: { temperature: 0.15, num_predict: 4096, num_ctx: 8192 }, - format: 'json', - }), - signal: AbortSignal.timeout(120000), - }) - - if (!response.ok) { - throw new Error(`Ollama error: ${response.status}`) - } - - const result = await response.json() - return result.message?.content || '' -} - -async function handleV2Draft(body: Record): Promise { - const { documentType, draftContext, instructions } = body as { - documentType: ScopeDocumentType - draftContext: DraftContext - instructions?: string - } - - // Step 1: Constraint Check (Hard Gate) - const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) - if (!constraintCheck.allowed) { - return NextResponse.json({ - draft: null, - constraintCheck, - tokensUsed: 0, - error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), - }, { status: 403 }) - } - - // Step 2: Derive Narrative Tags (deterministisch) - const scores = extractScoresFromDraftContext(draftContext) - const narrativeTags: NarrativeTags = deriveNarrativeTags(scores) - - // Step 3: Build Allowed Facts - const allowedFacts = buildAllowedFactsFromDraftContext(draftContext, narrativeTags) - - // Step 4: PII Sanitization - let sanitizationResult - try { - sanitizationResult = sanitizeAllowedFacts(allowedFacts) - } catch (error) { - if (error instanceof SanitizationError) { - return NextResponse.json({ - error: `Sanitization Hard Abort: ${error.message} (Feld: ${error.field})`, - draft: null, - constraintCheck, - tokensUsed: 0, - }, { status: 422 }) - } - throw error - } - - const sanitizedFacts = sanitizationResult.facts - - // Verify no remaining PII - const piiWarnings = validateNoRemainingPII(sanitizedFacts) - if (piiWarnings.length > 0) { - console.warn('PII-Warnungen nach Sanitization:', piiWarnings) - } - - // Step 5: Build prompt components - const factsString = allowedFactsToPromptString(sanitizedFacts) - const tagsString = narrativeTagsToPromptString(narrativeTags) - const termsString = terminologyToPromptString() - const styleString = styleContractToPromptString() - const disallowedString = disallowedTopicsToPromptString() - - // Compute prompt hash for audit - const promptHash = computeChecksumSync({ factsString, tagsString, termsString, styleString, disallowedString }) - - // Step 5b: RAG Legal Context (config-based) - const v2RagCfg = DOCUMENT_RAG_CONFIG[documentType] - const v2RagContext = await queryRAG(v2RagCfg.query, 3, v2RagCfg.collection) - - // Step 6: Generate Prose Blocks (with cache + repair loop) - const proseBlocks = DOCUMENT_PROSE_BLOCKS[documentType] || DOCUMENT_PROSE_BLOCKS.tom - const generatedBlocks: ProseBlockOutput[] = [] - const repairAudits: RepairAudit[] = [] - let totalTokens = 0 - - for (const blockDef of proseBlocks) { - // Check cache - const cacheParams: CacheKeyParams = { - allowedFacts: sanitizedFacts, - templateVersion: TEMPLATE_VERSION, - terminologyVersion: TERMINOLOGY_VERSION, - narrativeTags, - promptHash, - blockType: blockDef.blockType, - sectionName: blockDef.sectionName, - } - - const cached = proseCache.getSync(cacheParams) - if (cached) { - generatedBlocks.push(cached) - repairAudits.push({ - repairAttempts: 0, - validatorFailures: [], - repairSuccessful: true, - fallbackUsed: false, - }) - continue - } - - // Build prompts - let systemPrompt = buildV2SystemPrompt( - factsString, tagsString, termsString, styleString, disallowedString, - sanitizedFacts.companyName, - blockDef.blockId, blockDef.blockType, blockDef.sectionName, - documentType, blockDef.targetWords - ) - if (v2RagContext) { - systemPrompt += `\n\nRECHTSKONTEXT (als Referenz, nicht woertlich uebernehmen):\n${v2RagContext}` - } - const userPrompt = buildBlockSpecificPrompt( - blockDef.blockType, blockDef.sectionName, documentType - ) + (instructions ? `\n\nZusaetzliche Anweisungen: ${instructions}` : '') - - // Call LLM + Repair Loop - try { - const rawOutput = await callOllama(systemPrompt, userPrompt) - totalTokens += rawOutput.length / 4 // Rough token estimate - - const { block, audit } = await executeRepairLoop( - rawOutput, - sanitizedFacts, - narrativeTags, - blockDef.blockId, - blockDef.blockType, - async (repairPrompt) => callOllama(systemPrompt, repairPrompt), - documentType - ) - - generatedBlocks.push(block) - repairAudits.push(audit) - - // Cache successful blocks (not fallbacks) - if (!audit.fallbackUsed) { - proseCache.setSync(cacheParams, block) - } - } catch (error) { - // LLM unreachable → Fallback - const { buildFallbackBlock } = await import('@/lib/sdk/drafting-engine/prose-validator') - generatedBlocks.push( - buildFallbackBlock(blockDef.blockId, blockDef.blockType, sanitizedFacts, documentType) - ) - repairAudits.push({ - repairAttempts: 0, - validatorFailures: [[(error as Error).message]], - repairSuccessful: false, - fallbackUsed: true, - fallbackReason: `LLM-Fehler: ${(error as Error).message}`, - }) - } - } - - // Step 7: Build v1-compatible draft sections from prose blocks + original prompt - const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) - - // Also generate data sections via legacy pipeline - let dataSections: DraftSection[] = [] - try { - const dataResponse = await callOllama(V1_SYSTEM_PROMPT, draftPrompt) - const parsed = JSON.parse(dataResponse) - dataSections = (parsed.sections || []).map((s: Record, i: number) => ({ - id: String(s.id || `section-${i}`), - title: String(s.title || ''), - content: String(s.content || ''), - schemaField: s.schemaField ? String(s.schemaField) : undefined, - })) - totalTokens += dataResponse.length / 4 - } catch { - dataSections = [] - } - - // Merge: Prose intro → Data sections → Prose transitions/conclusion - const introBlock = generatedBlocks.find(b => b.blockType === 'introduction') - const transitionBlocks = generatedBlocks.filter(b => b.blockType === 'transition') - const appreciationBlocks = generatedBlocks.filter(b => b.blockType === 'appreciation') - const conclusionBlock = generatedBlocks.find(b => b.blockType === 'conclusion') - - const mergedSections: DraftSection[] = [] - - if (introBlock) { - mergedSections.push({ - id: introBlock.blockId, - title: 'Einleitung', - content: introBlock.text, - }) - } - - for (let i = 0; i < dataSections.length; i++) { - // Insert transition before data section (if available) - if (i > 0 && transitionBlocks[i - 1]) { - mergedSections.push({ - id: transitionBlocks[i - 1].blockId, - title: '', - content: transitionBlocks[i - 1].text, - }) - } - mergedSections.push(dataSections[i]) - } - - for (const block of appreciationBlocks) { - mergedSections.push({ - id: block.blockId, - title: 'Wuerdigung', - content: block.text, - }) - } - - if (conclusionBlock) { - mergedSections.push({ - id: conclusionBlock.blockId, - title: 'Fazit', - content: conclusionBlock.text, - }) - } - - // If no data sections generated, use prose blocks as sections - const finalSections = mergedSections.length > 0 ? mergedSections : generatedBlocks.map(b => ({ - id: b.blockId, - title: b.blockType === 'introduction' ? 'Einleitung' : - b.blockType === 'conclusion' ? 'Fazit' : - b.blockType === 'appreciation' ? 'Wuerdigung' : 'Ueberleitung', - content: b.text, - })) - - const draft: DraftRevision = { - id: `draft-v2-${Date.now()}`, - content: finalSections.map(s => s.title ? `## ${s.title}\n\n${s.content}` : s.content).join('\n\n'), - sections: finalSections, - createdAt: new Date().toISOString(), - instruction: instructions, - } - - // Step 8: Build Audit Trail - const auditTrail = { - documentType, - templateVersion: TEMPLATE_VERSION, - terminologyVersion: TERMINOLOGY_VERSION, - validatorVersion: VALIDATOR_VERSION, - promptHash, - llmModel: LLM_MODEL, - llmTemperature: 0.15, - llmProvider: 'ollama', - narrativeTags, - sanitization: sanitizationResult.audit, - repairAudits, - proseBlocks: generatedBlocks.map((b, i) => ({ - blockId: b.blockId, - blockType: b.blockType, - wordCount: b.text.split(/\s+/).filter(Boolean).length, - fallbackUsed: repairAudits[i]?.fallbackUsed ?? false, - repairAttempts: repairAudits[i]?.repairAttempts ?? 0, - })), - cacheStats: proseCache.getStats(), - } - - // Anti-Fake-Evidence: Truth label for all LLM-generated content - const truthLabel = { - generation_mode: 'draft_assistance', - truth_status: 'generated', - may_be_used_as_evidence: false, - generated_by: 'system', - } - - // Fire-and-forget: persist LLM audit trail to backend - try { - const BACKEND_URL = process.env.BACKEND_COMPLIANCE_URL || 'http://backend-compliance:8002' - fetch(`${BACKEND_URL}/api/compliance/llm-audit`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - entity_type: 'document', - entity_id: null, - generation_mode: 'draft_assistance', - truth_status: 'generated', - may_be_used_as_evidence: false, - llm_model: LLM_MODEL, - llm_provider: 'ollama', - input_summary: `${documentType} draft generation`, - output_summary: draft?.sections?.length ? `${draft.sections.length} sections generated` : 'draft generated', - }), - }).catch(() => {/* fire-and-forget */}) - } catch { - // LLM audit persistence failure should not block the response - } - - return NextResponse.json({ - draft, - constraintCheck, - tokensUsed: Math.round(totalTokens), - pipelineVersion: 'v2', - auditTrail, - truthLabel, - }) -} +import { handleV1Draft, handleV2Draft } from './draft-helpers' // ============================================================================ // Route Handler diff --git a/admin-compliance/app/sdk/document-generator/contextBridge-helpers.ts b/admin-compliance/app/sdk/document-generator/contextBridge-helpers.ts new file mode 100644 index 0000000..5a098e8 --- /dev/null +++ b/admin-compliance/app/sdk/document-generator/contextBridge-helpers.ts @@ -0,0 +1,277 @@ +/** + * Template-Spec v1 — Context Bridge Helpers + * + * Runtime functions: contextToPlaceholders, computeFlags, section relevance, + * dot-path accessors. Split from contextBridge.ts for the 500 LOC hard cap. + */ + +import type { + TemplateContext, + ProviderCtx, + ComputedFlags, +} from './contextBridge' + +// ============================================================================= +// Bridge: context → flat placeholder map +// ============================================================================= + +function str(v: unknown): string { + if (v === null || v === undefined || v === '') return '' + if (Array.isArray(v)) return v.join(', ') + return String(v) +} + +/** Compute compound address from PROVIDER fields */ +function providerAddress(p: ProviderCtx): string { + const parts = [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' ')].filter(Boolean) + return parts.join(', ') +} + +/** + * Maps a TemplateContext to a flat Record of placeholder values. + * Keys are the {{PLACEHOLDER}} strings used in templates. + */ +export function contextToPlaceholders(ctx: TemplateContext): Record { + const p = ctx.PROVIDER + const c = ctx.CUSTOMER + const s = ctx.SERVICE + const l = ctx.LEGAL + const prv = ctx.PRIVACY + const sla = ctx.SLA + const pay = ctx.PAYMENTS + const sec = ctx.SECURITY + const nda = ctx.NDA + const con = ctx.CONSENT + const h = ctx.HOSTING + const f = ctx.FEATURES + + const address = providerAddress(p) + + return { + // --- PROVIDER --- + '{{COMPANY_NAME}}': str(p.LEGAL_NAME), + '{{PROVIDER_NAME}}': str(p.LEGAL_NAME), + '{{DISCLOSING_PARTY}}': str(p.LEGAL_NAME), // NDA: provider is disclosing party + '{{SERVICE_PROVIDER}}': str(p.LEGAL_NAME), // SLA + '{{COMPANY_ADDRESS}}': address, + '{{PROVIDER_ADDRESS}}': address, + '{{CONTACT_EMAIL}}': str(p.EMAIL), + '{{PROVIDER_EMAIL}}': str(p.EMAIL), + '{{ABUSE_EMAIL}}': str(p.EMAIL), // AUP abuse contact + '{{REPORT_EMAIL}}': prv.CONTACT_EMAIL ? str(prv.CONTACT_EMAIL) : str(p.EMAIL), // community moderation + + // --- CUSTOMER --- + '{{CUSTOMER_NAME}}': str(c.LEGAL_NAME), + '{{RECEIVING_PARTY}}': str(c.LEGAL_NAME), // NDA + '{{CUSTOMER}}': str(c.LEGAL_NAME), // SLA + + // --- SERVICE --- + '{{SERVICE_NAME}}': str(s.NAME), + '{{PLATFORM_NAME}}': str(s.NAME), // community / copyright + '{{SERVICE_DESCRIPTION}}': str(s.DESCRIPTION), + '{{SERVICE_MODEL}}': str(s.MODEL), + '{{SERVICE_TIER}}': str(s.TIER), + '{{DATA_LOCATION}}': str(s.DATA_LOCATION), + '{{EXPORT_FORMATS}}': Array.isArray(s.EXPORT_FORMATS) ? s.EXPORT_FORMATS.join(', ') : '', + '{{EXPORT_WINDOW_DAYS}}': str(s.EXPORT_WINDOW_DAYS), + '{{MIN_TERM_MONTHS}}': str(s.MIN_TERM_MONTHS), + '{{TERMINATION_NOTICE_DAYS}}': str(s.TERMINATION_NOTICE_DAYS), + + // --- LEGAL --- + '{{GOVERNING_LAW}}': str(l.GOVERNING_LAW), + '{{JURISDICTION_CITY}}': str(l.JURISDICTION_CITY), + '{{VERSION_DATE}}': str(l.VERSION_DATE), + '{{EFFECTIVE_DATE}}': str(l.EFFECTIVE_DATE), + '{{START_DATE}}': str(l.EFFECTIVE_DATE), // cloud contract start date + + // --- PRIVACY --- + '{{PRIVACY_CONTACT_EMAIL}}': str(prv.CONTACT_EMAIL), + '{{DPO_NAME}}': str(prv.DPO_NAME), + '{{DPO_EMAIL}}': str(prv.DPO_EMAIL), + '{{COPYRIGHT_CONTACT_NAME}}': str(prv.DPO_NAME), // copyright policy + '{{COPYRIGHT_EMAIL}}': str(prv.DPO_EMAIL), + '{{PRIVACY_POLICY_URL}}': str(prv.PRIVACY_POLICY_URL), + '{{COOKIE_POLICY_URL}}': str(prv.COOKIE_POLICY_URL), + '{{ANALYTICS_RETENTION_MONTHS}}': str(prv.ANALYTICS_RETENTION_MONTHS), + '{{DATA_TRANSFER_THIRD_COUNTRIES}}': str(prv.DATA_TRANSFER_THIRD_COUNTRIES), + + // --- SLA --- + '{{AVAILABILITY_PERCENT}}': str(sla.AVAILABILITY_PERCENT), + '{{MAINTENANCE_NOTICE_HOURS}}': str(sla.MAINTENANCE_NOTICE_HOURS), + '{{SUPPORT_EMAIL}}': str(sla.SUPPORT_EMAIL), + '{{SUPPORT_PHONE}}': str(sla.SUPPORT_PHONE), + '{{SUPPORT_HOURS}}': str(sla.SUPPORT_HOURS), + '{{RESPONSE_CRITICAL_H}}': str(sla.RESPONSE_CRITICAL_H), + '{{RESOLUTION_CRITICAL_H}}': str(sla.RESOLUTION_CRITICAL_H), + '{{RESPONSE_HIGH_H}}': str(sla.RESPONSE_HIGH_H), + '{{RESOLUTION_HIGH_H}}': str(sla.RESOLUTION_HIGH_H), + '{{RESPONSE_MEDIUM_H}}': str(sla.RESPONSE_MEDIUM_H), + '{{RESOLUTION_MEDIUM_H}}': str(sla.RESOLUTION_MEDIUM_H), + '{{RESPONSE_LOW_H}}': str(sla.RESPONSE_LOW_H), + + // --- PAYMENTS --- + '{{MONTHLY_FEE}}': str(pay.MONTHLY_FEE_EUR), + '{{PAYMENT_DUE_DAY}}': str(pay.PAYMENT_DUE_DAY), + '{{PAYMENT_METHOD}}': str(pay.PAYMENT_METHOD), + '{{PAYMENT_DAYS}}': str(pay.PAYMENT_DAYS), + + // --- SECURITY --- + '{{INCIDENT_NOTICE_HOURS}}': str(sec.INCIDENT_NOTICE_HOURS), + '{{LOG_RETENTION_DAYS}}': str(sec.LOG_RETENTION_DAYS), + '{{SECURITY_LOG_RETENTION_DAYS}}': str(sec.SECURITY_LOG_RETENTION_DAYS), + + // --- NDA --- + '{{PURPOSE}}': str(nda.PURPOSE), + '{{DURATION_YEARS}}': str(nda.DURATION_YEARS), + '{{PENALTY_AMOUNT}}': nda.PENALTY_AMOUNT_EUR !== null ? str(nda.PENALTY_AMOUNT_EUR) : '', + + // --- CONSENT --- + '{{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), + } +} + +// ============================================================================= +// Computed flags +// ============================================================================= + +export function computeFlags(ctx: TemplateContext): ComputedFlags { + return { + IS_B2C: ctx.CUSTOMER.IS_CONSUMER, + IS_B2B: ctx.CUSTOMER.IS_BUSINESS, + SERVICE_IS_SAAS: ctx.SERVICE.MODEL === 'SaaS', + SERVICE_IS_HYBRID: ctx.SERVICE.MODEL === 'Hybrid', + HAS_PENALTY: ctx.NDA.PENALTY_AMOUNT_EUR !== null && ctx.NDA.PENALTY_AMOUNT_EUR !== '', + HAS_ANALYTICS: !!ctx.CONSENT.ANALYTICS_TOOLS, + HAS_MARKETING: !!ctx.CONSENT.MARKETING_PARTNERS, + } +} + +// ============================================================================= +// Section relevance — which sections are useful for a given template +// ============================================================================= + +/** All placeholder patterns covered by each context section */ +const SECTION_COVERS: Record = { + 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_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}}', '{{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}}', '{{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}}'], +} + +/** + * Returns which context sections are relevant for a given list of template placeholders. + */ +export function getRelevantSections(placeholders: string[]): (keyof TemplateContext)[] { + const pSet = new Set(placeholders) + return (Object.keys(SECTION_COVERS) as (keyof TemplateContext)[]).filter( + (section) => SECTION_COVERS[section].some((ph) => pSet.has(ph)) + ) +} + +/** + * Returns a list of placeholder keys that are NOT covered by the context bridge. + * These need to be filled in manually. + */ +export function getUncoveredPlaceholders(placeholders: string[], ctx: TemplateContext): string[] { + const bridged = contextToPlaceholders(ctx) + return placeholders.filter((ph) => !(ph in bridged)) +} + +/** + * Strict validation: returns placeholder keys that are required but empty. + * "Required" means: placeholder appears in template AND bridge covers it AND bridge produces empty string. + */ +export function getMissingRequired(placeholders: string[], ctx: TemplateContext): string[] { + const bridged = contextToPlaceholders(ctx) + return placeholders.filter((ph) => ph in bridged && !bridged[ph]) +} + +// ============================================================================= +// Helpers to set nested context values by dot-path +// ============================================================================= + +/** + * Returns a new TemplateContext with the value at dotPath set. + * e.g. setContextPath(ctx, 'PROVIDER.LEGAL_NAME', 'ACME GmbH') + */ +export function setContextPath(ctx: TemplateContext, dotPath: string, value: unknown): TemplateContext { + const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]] + const key = rest.join('.') + return { + ...ctx, + [section]: { + ...(ctx[section] as unknown as Record), + [key]: value, + }, + } +} + +/** + * Get a nested context value by dot-path. + */ +export function getContextPath(ctx: TemplateContext, dotPath: string): unknown { + const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]] + const sectionObj = ctx[section] as unknown as Record + return sectionObj?.[rest.join('.')] +} diff --git a/admin-compliance/app/sdk/document-generator/contextBridge.ts b/admin-compliance/app/sdk/document-generator/contextBridge.ts index a0416ae..9085fe4 100644 --- a/admin-compliance/app/sdk/document-generator/contextBridge.ts +++ b/admin-compliance/app/sdk/document-generator/contextBridge.ts @@ -266,266 +266,15 @@ export const EMPTY_CONTEXT: TemplateContext = { } // ============================================================================= -// Bridge: context → flat placeholder map +// Re-exports from helpers (barrel — no consumer imports need updating) // ============================================================================= -function str(v: unknown): string { - if (v === null || v === undefined || v === '') return '' - if (Array.isArray(v)) return v.join(', ') - return String(v) -} - -/** Compute compound address from PROVIDER fields */ -function providerAddress(p: ProviderCtx): string { - const parts = [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' ')].filter(Boolean) - return parts.join(', ') -} - -/** - * Maps a TemplateContext to a flat Record of placeholder values. - * Keys are the {{PLACEHOLDER}} strings used in templates. - */ -export function contextToPlaceholders(ctx: TemplateContext): Record { - const p = ctx.PROVIDER - const c = ctx.CUSTOMER - const s = ctx.SERVICE - const l = ctx.LEGAL - const prv = ctx.PRIVACY - const sla = ctx.SLA - const pay = ctx.PAYMENTS - const sec = ctx.SECURITY - const nda = ctx.NDA - const con = ctx.CONSENT - const h = ctx.HOSTING - const f = ctx.FEATURES - - const address = providerAddress(p) - - return { - // --- PROVIDER --- - '{{COMPANY_NAME}}': str(p.LEGAL_NAME), - '{{PROVIDER_NAME}}': str(p.LEGAL_NAME), - '{{DISCLOSING_PARTY}}': str(p.LEGAL_NAME), // NDA: provider is disclosing party - '{{SERVICE_PROVIDER}}': str(p.LEGAL_NAME), // SLA - '{{COMPANY_ADDRESS}}': address, - '{{PROVIDER_ADDRESS}}': address, - '{{CONTACT_EMAIL}}': str(p.EMAIL), - '{{PROVIDER_EMAIL}}': str(p.EMAIL), - '{{ABUSE_EMAIL}}': str(p.EMAIL), // AUP abuse contact - '{{REPORT_EMAIL}}': prv.CONTACT_EMAIL ? str(prv.CONTACT_EMAIL) : str(p.EMAIL), // community moderation - - // --- CUSTOMER --- - '{{CUSTOMER_NAME}}': str(c.LEGAL_NAME), - '{{RECEIVING_PARTY}}': str(c.LEGAL_NAME), // NDA - '{{CUSTOMER}}': str(c.LEGAL_NAME), // SLA - - // --- SERVICE --- - '{{SERVICE_NAME}}': str(s.NAME), - '{{PLATFORM_NAME}}': str(s.NAME), // community / copyright - '{{SERVICE_DESCRIPTION}}': str(s.DESCRIPTION), - '{{SERVICE_MODEL}}': str(s.MODEL), - '{{SERVICE_TIER}}': str(s.TIER), - '{{DATA_LOCATION}}': str(s.DATA_LOCATION), - '{{EXPORT_FORMATS}}': Array.isArray(s.EXPORT_FORMATS) ? s.EXPORT_FORMATS.join(', ') : '', - '{{EXPORT_WINDOW_DAYS}}': str(s.EXPORT_WINDOW_DAYS), - '{{MIN_TERM_MONTHS}}': str(s.MIN_TERM_MONTHS), - '{{TERMINATION_NOTICE_DAYS}}': str(s.TERMINATION_NOTICE_DAYS), - - // --- LEGAL --- - '{{GOVERNING_LAW}}': str(l.GOVERNING_LAW), - '{{JURISDICTION_CITY}}': str(l.JURISDICTION_CITY), - '{{VERSION_DATE}}': str(l.VERSION_DATE), - '{{EFFECTIVE_DATE}}': str(l.EFFECTIVE_DATE), - '{{START_DATE}}': str(l.EFFECTIVE_DATE), // cloud contract start date - - // --- PRIVACY --- - '{{PRIVACY_CONTACT_EMAIL}}': str(prv.CONTACT_EMAIL), - '{{DPO_NAME}}': str(prv.DPO_NAME), - '{{DPO_EMAIL}}': str(prv.DPO_EMAIL), - '{{COPYRIGHT_CONTACT_NAME}}': str(prv.DPO_NAME), // copyright policy - '{{COPYRIGHT_EMAIL}}': str(prv.DPO_EMAIL), - '{{PRIVACY_POLICY_URL}}': str(prv.PRIVACY_POLICY_URL), - '{{COOKIE_POLICY_URL}}': str(prv.COOKIE_POLICY_URL), - '{{ANALYTICS_RETENTION_MONTHS}}': str(prv.ANALYTICS_RETENTION_MONTHS), - '{{DATA_TRANSFER_THIRD_COUNTRIES}}': str(prv.DATA_TRANSFER_THIRD_COUNTRIES), - - // --- SLA --- - '{{AVAILABILITY_PERCENT}}': str(sla.AVAILABILITY_PERCENT), - '{{MAINTENANCE_NOTICE_HOURS}}': str(sla.MAINTENANCE_NOTICE_HOURS), - '{{SUPPORT_EMAIL}}': str(sla.SUPPORT_EMAIL), - '{{SUPPORT_PHONE}}': str(sla.SUPPORT_PHONE), - '{{SUPPORT_HOURS}}': str(sla.SUPPORT_HOURS), - '{{RESPONSE_CRITICAL_H}}': str(sla.RESPONSE_CRITICAL_H), - '{{RESOLUTION_CRITICAL_H}}': str(sla.RESOLUTION_CRITICAL_H), - '{{RESPONSE_HIGH_H}}': str(sla.RESPONSE_HIGH_H), - '{{RESOLUTION_HIGH_H}}': str(sla.RESOLUTION_HIGH_H), - '{{RESPONSE_MEDIUM_H}}': str(sla.RESPONSE_MEDIUM_H), - '{{RESOLUTION_MEDIUM_H}}': str(sla.RESOLUTION_MEDIUM_H), - '{{RESPONSE_LOW_H}}': str(sla.RESPONSE_LOW_H), - - // --- PAYMENTS --- - '{{MONTHLY_FEE}}': str(pay.MONTHLY_FEE_EUR), - '{{PAYMENT_DUE_DAY}}': str(pay.PAYMENT_DUE_DAY), - '{{PAYMENT_METHOD}}': str(pay.PAYMENT_METHOD), - '{{PAYMENT_DAYS}}': str(pay.PAYMENT_DAYS), - - // --- SECURITY --- - '{{INCIDENT_NOTICE_HOURS}}': str(sec.INCIDENT_NOTICE_HOURS), - '{{LOG_RETENTION_DAYS}}': str(sec.LOG_RETENTION_DAYS), - '{{SECURITY_LOG_RETENTION_DAYS}}': str(sec.SECURITY_LOG_RETENTION_DAYS), - - // --- NDA --- - '{{PURPOSE}}': str(nda.PURPOSE), - '{{DURATION_YEARS}}': str(nda.DURATION_YEARS), - '{{PENALTY_AMOUNT}}': nda.PENALTY_AMOUNT_EUR !== null ? str(nda.PENALTY_AMOUNT_EUR) : '', - - // --- CONSENT --- - '{{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), - } -} - -// ============================================================================= -// Computed flags -// ============================================================================= - -export function computeFlags(ctx: TemplateContext): ComputedFlags { - return { - IS_B2C: ctx.CUSTOMER.IS_CONSUMER, - IS_B2B: ctx.CUSTOMER.IS_BUSINESS, - SERVICE_IS_SAAS: ctx.SERVICE.MODEL === 'SaaS', - SERVICE_IS_HYBRID: ctx.SERVICE.MODEL === 'Hybrid', - HAS_PENALTY: ctx.NDA.PENALTY_AMOUNT_EUR !== null && ctx.NDA.PENALTY_AMOUNT_EUR !== '', - HAS_ANALYTICS: !!ctx.CONSENT.ANALYTICS_TOOLS, - HAS_MARKETING: !!ctx.CONSENT.MARKETING_PARTNERS, - } -} - -// ============================================================================= -// Section relevance — which sections are useful for a given template -// ============================================================================= - -/** All placeholder patterns covered by each context section */ -const SECTION_COVERS: Record = { - 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_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}}', '{{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}}', '{{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}}'], -} - -/** - * Returns which context sections are relevant for a given list of template placeholders. - */ -export function getRelevantSections(placeholders: string[]): (keyof TemplateContext)[] { - const pSet = new Set(placeholders) - return (Object.keys(SECTION_COVERS) as (keyof TemplateContext)[]).filter( - (section) => SECTION_COVERS[section].some((ph) => pSet.has(ph)) - ) -} - -/** - * Returns a list of placeholder keys that are NOT covered by the context bridge. - * These need to be filled in manually. - */ -export function getUncoveredPlaceholders(placeholders: string[], ctx: TemplateContext): string[] { - const bridged = contextToPlaceholders(ctx) - return placeholders.filter((ph) => !(ph in bridged)) -} - -/** - * Strict validation: returns placeholder keys that are required but empty. - * "Required" means: placeholder appears in template AND bridge covers it AND bridge produces empty string. - */ -export function getMissingRequired(placeholders: string[], ctx: TemplateContext): string[] { - const bridged = contextToPlaceholders(ctx) - return placeholders.filter((ph) => ph in bridged && !bridged[ph]) -} - -// ============================================================================= -// Helpers to set nested context values by dot-path -// ============================================================================= - -/** - * Returns a new TemplateContext with the value at dotPath set. - * e.g. setContextPath(ctx, 'PROVIDER.LEGAL_NAME', 'ACME GmbH') - */ -export function setContextPath(ctx: TemplateContext, dotPath: string, value: unknown): TemplateContext { - const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]] - const key = rest.join('.') - return { - ...ctx, - [section]: { - ...(ctx[section] as unknown as Record), - [key]: value, - }, - } -} - -/** - * Get a nested context value by dot-path. - */ -export function getContextPath(ctx: TemplateContext, dotPath: string): unknown { - const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]] - const sectionObj = ctx[section] as unknown as Record - return sectionObj?.[rest.join('.')] -} +export { + contextToPlaceholders, + computeFlags, + getRelevantSections, + getUncoveredPlaceholders, + getMissingRequired, + setContextPath, + getContextPath, +} from './contextBridge-helpers' diff --git a/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core-part2.ts b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core-part2.ts new file mode 100644 index 0000000..b01969f --- /dev/null +++ b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core-part2.ts @@ -0,0 +1,249 @@ +/** + * Compliance Scope Engine - Document Scope Matrix Core (Part 2) + * + * Continued from document-scope-matrix-core.ts for the 500 LOC hard cap. + * Contains: dsfa, daten_transfer, datenpannen, einwilligung, vertragsmanagement + */ + +import type { ScopeDocumentType } from './documents' +import type { DocumentScopeRequirement } from './documents' + +export const DOCUMENT_SCOPE_MATRIX_CORE_PART2: Partial> = { + dsfa: { + L1: { + required: false, + depth: 'Nicht erforderlich', + detailItems: ['Nur bei Hard Trigger erforderlich'], + estimatedEffort: 'N/A', + }, + L2: { + required: false, + depth: 'Bei Bedarf', + detailItems: [ + 'DSFA-Schwellwertanalyse durchführen', + 'Bei Erforderlichkeit: Basis-DSFA', + 'Risiken identifiziert und bewertet', + 'Maßnahmen zur Risikominimierung', + ], + estimatedEffort: '4-8 Stunden pro DSFA', + }, + L3: { + required: false, + depth: 'Standard', + detailItems: [ + 'Alle L2-Anforderungen', + 'Detaillierte Risikobewertung', + 'Konsultation der Betroffenen wo sinnvoll', + 'Dokumentation der Entscheidungsprozesse', + 'Regelmäßige Überprüfung', + ], + estimatedEffort: '8-16 Stunden pro DSFA', + }, + L4: { + required: true, + depth: 'Vollständig', + detailItems: [ + 'Alle L3-Anforderungen', + 'Strukturierter DSFA-Prozess etabliert', + 'Vorabkonsultation der Aufsichtsbehörde wo erforderlich', + 'Vollständige Dokumentation aller Schritte', + 'Integration in Projektmanagement', + ], + estimatedEffort: '16-24 Stunden pro DSFA', + }, + }, + daten_transfer: { + L1: { + required: false, + depth: 'Basis', + detailItems: [ + 'Liste aller Drittlandtransfers', + 'Grundlegende Rechtsgrundlage identifiziert', + 'Standard-Vertragsklauseln wo nötig', + ], + estimatedEffort: '1-2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Alle L1-Anforderungen', + 'Detaillierte Dokumentation aller Transfers', + 'Angemessenheitsbeschlüsse oder geeignete Garantien', + 'Informationen an Betroffene bereitgestellt', + 'Register geführt', + ], + estimatedEffort: '3-6 Stunden', + }, + L3: { + required: true, + depth: 'Detailliert', + detailItems: [ + 'Alle L2-Anforderungen', + 'Transfer Impact Assessment (TIA) durchgeführt', + 'Zusätzliche Schutzmaßnahmen dokumentiert', + 'Regelmäßige Überprüfung der Rechtsgrundlagen', + 'Risikobewertung für jedes Zielland', + ], + estimatedEffort: '6-12 Stunden', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Alle L3-Anforderungen', + 'Vollständige TIA-Dokumentation', + 'Regelmäßige Reviews dokumentiert', + 'Rechtliche Expertise nachgewiesen', + 'Compliance-Nachweise für alle Transfers', + ], + estimatedEffort: '12-20 Stunden', + }, + }, + datenpannen: { + L1: { + required: true, + depth: 'Basis', + detailItems: [ + 'Grundlegender Prozess für Datenpannen', + 'Kontakt zur Aufsichtsbehörde bekannt', + 'Verantwortlichkeiten grob definiert', + 'Einfache Checkliste', + ], + estimatedEffort: '1-2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Alle L1-Anforderungen', + 'Detaillierter Incident-Response-Plan', + 'Bewertungskriterien für Meldepflicht', + 'Vorlagen für Meldungen (Behörde & Betroffene)', + 'Dokumentationspflichten klar definiert', + ], + estimatedEffort: '3-6 Stunden', + }, + L3: { + required: true, + depth: 'Detailliert', + detailItems: [ + 'Alle L2-Anforderungen', + 'Incident-Management-System etabliert', + 'Regelmäßige Übungen durchgeführt', + 'Eskalationsprozesse dokumentiert', + 'Post-Incident-Review-Prozess', + 'Lessons Learned dokumentiert', + ], + estimatedEffort: '6-10 Stunden', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Alle L3-Anforderungen', + 'Vollständiges Breach-Log geführt', + 'Integration mit IT-Security-Incident-Response', + 'Regelmäßige Audits des Prozesses', + 'Compliance-Nachweise für alle Vorfälle', + ], + estimatedEffort: '10-16 Stunden', + }, + }, + einwilligung: { + L1: { + required: false, + depth: 'Basis', + detailItems: [ + 'Einwilligungsformulare DSGVO-konform', + 'Opt-in statt Opt-out', + 'Widerrufsmöglichkeit bereitgestellt', + ], + estimatedEffort: '1-2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Alle L1-Anforderungen', + 'Granulare Einwilligungen', + 'Nachweisbarkeit der Einwilligung', + 'Dokumentation des Einwilligungsprozesses', + 'Regelmäßige Überprüfung', + ], + estimatedEffort: '3-6 Stunden', + }, + L3: { + required: true, + depth: 'Detailliert', + detailItems: [ + 'Alle L2-Anforderungen', + 'Consent-Management-System implementiert', + 'Vollständiger Audit-Trail', + 'A/B-Testing dokumentiert', + 'Integration mit allen Datenverarbeitungen', + 'Regelmäßige Revalidierung', + ], + estimatedEffort: '6-12 Stunden', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Alle L3-Anforderungen', + 'Enterprise Consent Management Platform', + 'Vollständige Nachweiskette für alle Einwilligungen', + 'Compliance-Dashboard', + 'Regelmäßige externe Audits', + ], + estimatedEffort: '12-20 Stunden', + }, + }, + vertragsmanagement: { + L1: { + required: false, + depth: 'Basis', + detailItems: [ + 'Einfaches Register wichtiger Verträge', + 'Ablage datenschutzrelevanter Verträge', + ], + estimatedEffort: '1-2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Alle L1-Anforderungen', + 'Vollständiges Vertragsregister', + 'Datenschutzklauseln in Standardverträgen', + 'Überprüfungsprozess für neue Verträge', + 'Ablaufdaten und Kündigungsfristen getrackt', + ], + estimatedEffort: '3-6 Stunden Setup', + }, + L3: { + required: true, + depth: 'Detailliert', + detailItems: [ + 'Alle L2-Anforderungen', + 'Vertragsmanagement-System implementiert', + 'Automatische Erinnerungen für Reviews', + 'Risikobewertung für Vertragspartner', + 'Compliance-Checks vor Vertragsabschluss', + ], + estimatedEffort: '6-12 Stunden Setup', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Alle L3-Anforderungen', + 'Enterprise Contract Management System', + 'Vollständiger Audit-Trail', + 'Integration mit Procurement', + 'Regelmäßige Compliance-Audits', + ], + estimatedEffort: '12-20 Stunden Setup', + }, + }, +} diff --git a/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts index 0ceaec3..964bee1 100644 --- a/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts +++ b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-core.ts @@ -8,6 +8,7 @@ import type { ScopeDocumentType } from './documents' import type { DocumentScopeRequirement } from './documents' +import { DOCUMENT_SCOPE_MATRIX_CORE_PART2 } from './document-scope-matrix-core-part2' /** * Scope-Matrix fuer Kern-DSGVO-Dokumente @@ -311,241 +312,7 @@ export const DOCUMENT_SCOPE_MATRIX_CORE: Partial> = { + iace_ce_assessment: { + L1: { + required: false, + depth: 'Minimal', + detailItems: [ + 'Regulatorischer Quick-Check fuer SW/FW/KI', + 'Grundlegende Identifikation relevanter Vorschriften', + ], + estimatedEffort: '2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'CE-Risikobeurteilung fuer SW/FW-Komponenten', + 'Hazard Log mit S×E×P Bewertung', + 'CRA-Konformitaetspruefung', + 'Grundlegende Massnahmendokumentation', + ], + estimatedEffort: '8 Stunden', + }, + L3: { + required: true, + depth: 'Detailliert', + detailItems: [ + 'Alle L2-Anforderungen', + 'Vollstaendige CE-Akte inkl. KI-Dossier', + 'AI Act High-Risk Konformitaetsbewertung', + 'Maschinenverordnung Anhang III Nachweis', + 'Verifikationsplan mit Akzeptanzkriterien', + 'Evidence-Management fuer Testnachweise', + ], + estimatedEffort: '16 Stunden', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Alle L3-Anforderungen', + 'Zertifizierungsfertige CE-Dokumentation', + 'Benannte-Stelle-tauglicher Nachweis', + 'Revisionssichere Audit Trails', + 'Post-Market Monitoring Plan', + 'Continuous Compliance Framework', + ], + estimatedEffort: '24 Stunden', + }, + }, + widerrufsbelehrung: { + L1: { + required: false, + depth: 'Nicht relevant', + detailItems: ['Nur bei B2C-Fernabsatz erforderlich'], + estimatedEffort: '0', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Muster-Widerrufsbelehrung nach EGBGB Anlage 1', + 'Muster-Widerrufsformular nach EGBGB Anlage 2', + 'Integration in Bestellprozess', + '14-Tage Widerrufsfrist korrekt dargestellt', + ], + estimatedEffort: '2-4 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + digitale Inhalte (§ 356 Abs. 5 BGB)', + 'Ausnahmen dokumentiert (§ 312g Abs. 2 BGB)', + ], + estimatedEffort: '4-6 Stunden', + }, + L4: { + required: true, + depth: 'Vollstaendig', + detailItems: [ + 'Wie L3 + automatisierte Pruefung', + 'Mehrsprachig bei EU-Verkauf', + ], + estimatedEffort: '6-8 Stunden', + }, + }, + preisangaben: { + L1: { + required: false, + depth: 'Nicht relevant', + detailItems: ['Nur bei B2C-Preisauszeichnung erforderlich'], + estimatedEffort: '0', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Gesamtpreisangabe inkl. MwSt (§ 1 PAngV)', + 'Grundpreisangabe bei Mengenware (§ 4 PAngV)', + 'Versandkosten deutlich angegeben', + ], + estimatedEffort: '2-3 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + Preishistorie bei Rabattaktionen (Omnibus-RL)', + 'Streichpreise korrekt dargestellt', + ], + estimatedEffort: '3-5 Stunden', + }, + L4: { + required: true, + depth: 'Vollstaendig', + detailItems: [ + 'Wie L3 + automatisierte Pruefung', + 'Mehrwaehrungsunterstuetzung', + ], + estimatedEffort: '5-8 Stunden', + }, + }, + fernabsatz_info: { + L1: { + required: false, + depth: 'Nicht relevant', + detailItems: ['Nur bei Fernabsatzvertraegen erforderlich'], + estimatedEffort: '0', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Pflichtinformationen nach § 312d BGB i.V.m. Art. 246a EGBGB', + 'Wesentliche Eigenschaften der Ware/Dienstleistung', + 'Identitaet und Anschrift des Unternehmers', + 'Zahlungs-, Liefer- und Leistungsbedingungen', + ], + estimatedEffort: '3-5 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + Informationen zu digitalen Inhalten/Diensten', + 'Funktionalitaet und Interoperabilitaet (§ 327 BGB)', + ], + estimatedEffort: '5-8 Stunden', + }, + L4: { + required: true, + depth: 'Vollstaendig', + detailItems: [ + 'Wie L3 + mehrsprachige Informationspflichten', + 'Automatisierte Vollstaendigkeitspruefung', + ], + estimatedEffort: '8-12 Stunden', + }, + }, + streitbeilegung: { + L1: { + required: false, + depth: 'Nicht relevant', + detailItems: ['Nur bei B2C-Handel erforderlich'], + estimatedEffort: '0', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Hinweis auf OS-Plattform der EU-Kommission (Art. 14 ODR-VO)', + 'Erklaerung zur Teilnahmebereitschaft an Streitbeilegung (§ 36 VSBG)', + 'Link zur OS-Plattform im Impressum/AGB', + ], + estimatedEffort: '1-2 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + Benennung zustaendiger Verbraucherschlichtungsstelle', + 'Prozess fuer Streitbeilegungsanfragen dokumentiert', + ], + estimatedEffort: '2-3 Stunden', + }, + L4: { + required: true, + depth: 'Vollstaendig', + detailItems: [ + 'Wie L3 + Eskalationsprozess dokumentiert', + 'Regelmaessige Auswertung von Beschwerden', + ], + estimatedEffort: '3-4 Stunden', + }, + }, + produktsicherheit: { + L1: { + required: false, + depth: 'Minimal', + detailItems: ['Grundlegende Produktkennzeichnung pruefen'], + estimatedEffort: '1 Stunde', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Produktsicherheitsbewertung nach GPSR (EU 2023/988)', + 'CE-Kennzeichnung und Konformitaetserklaerung', + 'Wirtschaftsakteur-Angaben auf Produkt/Verpackung', + 'Technische Dokumentation fuer Marktaufsicht', + ], + estimatedEffort: '8-12 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + Risikoanalyse fuer alle Produktvarianten', + 'Rueckrufplan und Marktbeobachtungspflichten', + 'Supply-Chain-Dokumentation', + ], + estimatedEffort: '16-24 Stunden', + }, + L4: { + required: true, + depth: 'Vollstaendig', + detailItems: [ + 'Wie L3 + vollstaendige GPSR-Konformitaetsakte', + 'Post-Market-Surveillance System', + 'Audit-Trail fuer alle Sicherheitsbewertungen', + ], + estimatedEffort: '24-40 Stunden', + }, + }, + ai_act_doku: { + L1: { + required: false, + depth: 'Minimal', + detailItems: ['KI-Risikokategorisierung (Art. 6 AI Act)'], + estimatedEffort: '2 Stunden', + }, + L2: { + required: true, + depth: 'Standard', + detailItems: [ + 'Technische Dokumentation nach Art. 11 AI Act', + 'Transparenzpflichten (Art. 52 AI Act)', + 'Risikomanagement-Grundlagen (Art. 9 AI Act)', + 'Menschliche Aufsicht dokumentiert (Art. 14 AI Act)', + ], + estimatedEffort: '8-12 Stunden', + }, + L3: { + required: true, + depth: 'Erweitert', + detailItems: [ + 'Wie L2 + Datenqualitaetsmanagement (Art. 10 AI Act)', + 'Genauigkeits- und Robustheitstests (Art. 15 AI Act)', + 'Vollstaendige Konformitaetsbewertung fuer Hochrisiko-KI', + ], + estimatedEffort: '16-24 Stunden', + }, + L4: { + required: true, + depth: 'Audit-Ready', + detailItems: [ + 'Wie L3 + Zertifizierungsfertige AI Act Dokumentation', + 'EU-Datenbank-Registrierung (Art. 60 AI Act)', + 'Post-Market Monitoring fuer KI-Systeme', + 'Continuous Compliance Framework fuer KI', + ], + estimatedEffort: '24-40 Stunden', + }, + }, +} diff --git a/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts index 9da8464..05a7c8e 100644 --- a/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts +++ b/admin-compliance/lib/sdk/compliance-scope-types/document-scope-matrix-extended.ts @@ -10,6 +10,7 @@ import type { ScopeDocumentType } from './documents' import type { DocumentScopeRequirement } from './documents' +import { DOCUMENT_SCOPE_MATRIX_EXTENDED_PART2 } from './document-scope-matrix-extended-part2' /** * Scope-Matrix fuer erweiterte Dokumente @@ -289,277 +290,7 @@ export const DOCUMENT_SCOPE_MATRIX_EXTENDED: Partial a.abbreviation === sa.abbreviation)) { + authorities.push(sa) + } + } + } + } + return authorities +} + +export function getSupervisoryAuthority( + country: CountryCode, + allFrameworks: LegalFramework[] +): SupervisoryAuthority[] { + return getAllSupervisoryAuthorities(allFrameworks).filter(sa => sa.country === country) +} + +export function getCountrySpecificDocTypes( + country: CountryCode, + matrix: DocumentTypeMatrix[] +): DocumentTypeMatrix[] { + return matrix.filter( + d => d.uniformity === 'country_specific' || + (d.uniformity === 'needs_national_supplement' && country !== 'EU') + ) +} + +export function getEUUniformDocTypes(matrix: DocumentTypeMatrix[]): DocumentTypeMatrix[] { + return matrix.filter(d => d.uniformity === 'eu_uniform') +} + +export function isGDPRCountry(country: CountryCode): boolean { + const gdprCountries: CountryCode[] = ['EU', 'DE', 'AT', 'FR', 'ES', 'IT', 'NL', 'NO', 'IS'] + return gdprCountries.includes(country) +} + +export function hasSeparateLegalFramework(country: CountryCode): boolean { + return country === 'CH' || country === 'GB' +} + +export function getRAGSourcesForPhase(phase: 1 | 2 | 3, allFrameworks: LegalFramework[]): LegalFramework[] { + return allFrameworks.filter(f => f.ragPhase === phase) +} + +export function getRequiredFrameworkSummary( + country: CountryCode, + allFrameworks: LegalFramework[] +): { + baseLaw: string + nationalLaw: string | null + supervisoryAuthority: string | null + separateFramework: boolean +} { + const isGDPR = isGDPRCountry(country) + const national = NATIONAL_FRAMEWORKS.filter(f => f.countryCode === country) + const authorities = getSupervisoryAuthority(country, allFrameworks) + + return { + baseLaw: isGDPR ? 'DSGVO (EU 2016/679)' : (country === 'CH' ? 'revDSG (CH)' : 'UK GDPR'), + nationalLaw: national.length > 0 ? national.map(n => n.abbreviation).join(', ') : null, + supervisoryAuthority: authorities.length > 0 ? authorities.map(a => a.abbreviation).join(', ') : null, + separateFramework: hasSeparateLegalFramework(country), + } +} + +// Re-export RAGLayer type for barrel consumers +export type { RAGLayer } diff --git a/admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts b/admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts index 3412034..cd1e75f 100644 --- a/admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts +++ b/admin-compliance/lib/sdk/dsfa/eu-legal-frameworks.ts @@ -150,294 +150,19 @@ export const EU_BASE_FRAMEWORKS: LegalFramework[] = [ }, ] -// ============================================================================= -// Nationale Ergaenzungsgesetze (Phase 2 — modular pro Land) -// ============================================================================= - -export const NATIONAL_FRAMEWORKS: LegalFramework[] = [ - // --- Deutschland --- - { - id: 'DE-BDSG', - countryCode: 'DE', - name: 'BDSG', - fullName: 'Bundesdatenschutzgesetz (2018)', - abbreviation: 'BDSG', - type: 'national_law', - description: - 'Nationales Begleitgesetz zur DSGVO. Ergaenzt u.a. Beschaeftigtendatenschutz (§26), ' + - 'Videoueberwachung (§4), Forschung/Statistik, Bussgeldpraxis.', - sourceUrl: 'https://www.gesetze-im-internet.de/bdsg_2018/', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)', - gdprOpeningClauses: ['Art. 6 Abs. 2', 'Art. 9 Abs. 4', 'Art. 23', 'Art. 85', 'Art. 88'], - specialProvisions: [ - '§26 BDSG — Beschaeftigtendatenschutz', - '§4 BDSG — Videoueberwachung oeffentlich zugaenglicher Raeume', - '§22 BDSG — Verarbeitung besonderer Kategorien', - '§41-43 BDSG — Straf- und Bussgeldvorschriften', - ], - supervisoryAuthorities: [ - { name: 'Bundesbeauftragter fuer den Datenschutz', abbreviation: 'BfDI', url: 'https://www.bfdi.bund.de', country: 'DE' }, - ], - ragPhase: 2, - }, - { - id: 'DE-TTDSG', - countryCode: 'DE', - name: 'TTDSG', - fullName: 'Telekommunikation-Telemedien-Datenschutz-Gesetz', - abbreviation: 'TTDSG', - type: 'national_law', - description: - 'Deutsche Umsetzung der ePrivacy-Richtlinie. Regelt insbesondere Cookie-Consent (§25 TTDSG), ' + - 'Endgeraetezugriff und Telekommunikations-Datenschutz.', - sourceUrl: 'https://www.gesetze-im-internet.de/ttdsg/', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)', - specialProvisions: [ - '§25 TTDSG — Einwilligung fuer Cookies/Tracking', - '§26 TTDSG — Anerkannte Dienste zur Einwilligungsverwaltung', - ], - ragPhase: 2, - }, - { - id: 'DE-TMG', - countryCode: 'DE', - name: 'TMG / DDG', - fullName: 'Telemediengesetz / Digitale-Dienste-Gesetz', - abbreviation: 'TMG', - type: 'national_law', - description: - 'Impressumspflicht (§5 TMG/DDG) und Anbieterkennzeichnung fuer Online-Dienste in Deutschland.', - sourceUrl: 'https://www.gesetze-im-internet.de/tmg/', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Werk, gemeinfrei (§5 UrhG)', - specialProvisions: [ - '§5 TMG — Impressumspflicht (Anbieterkennzeichnung)', - '§7-10 TMG — Verantwortlichkeit von Diensteanbietern', - ], - ragPhase: 3, - }, - - // --- Oesterreich --- - { - id: 'AT-DSG', - countryCode: 'AT', - name: 'DSG (AT)', - fullName: 'Datenschutzgesetz (Oesterreich, 2018)', - abbreviation: 'DSG', - type: 'national_law', - description: - 'Oesterreichisches Begleitgesetz zur DSGVO. Enthält Besonderheiten fuer Behoerden, ' + - 'Strafverfolgung und teilweise andere Auslegungspraxis als Deutschland.', - sourceUrl: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001597', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Werk, Rechtsinformationssystem des Bundes (RIS)', - supervisoryAuthorities: [ - { name: 'Oesterreichische Datenschutzbehoerde', abbreviation: 'DSB', url: 'https://www.dsb.gv.at', country: 'AT' }, - ], - ragPhase: 2, - }, - - // --- Schweiz (NICHT EU — eigenes Recht) --- - { - id: 'CH-DSG', - countryCode: 'CH', - name: 'revDSG (CH)', - fullName: 'Bundesgesetz ueber den Datenschutz (revidiertes DSG, seit 01.09.2023)', - abbreviation: 'revDSG', - type: 'national_law', - description: - 'Die Schweiz ist nicht EU-Mitglied. Das revidierte DSG (2023) ist inhaltlich aehnlich der DSGVO, ' + - 'aber nicht identisch. Unterschiede: andere Sanktionslogik (Busse bis 250.000 CHF gegen ' + - 'natuerliche Personen), teils andere Begriffe, kein One-Stop-Shop.', - sourceUrl: 'https://www.fedlex.admin.ch/eli/cc/2022/491/de', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Bundesrecht, Fedlex (Schweiz)', - specialProvisions: [ - 'Art. 60-66 revDSG — Strafbestimmungen (gegen natuerliche Personen)', - 'Art. 16-18 revDSG — Drittlandtransfer (eigene Laenderliste)', - 'Art. 22 revDSG — Datenschutz-Folgenabschaetzung', - 'Art. 12 revDSG — Verzeichnis der Bearbeitungstaetigkeiten', - ], - supervisoryAuthorities: [ - { name: 'Eidgenoessischer Datenschutzbeauftragter', abbreviation: 'EDOEB', url: 'https://www.edoeb.admin.ch', country: 'CH' }, - ], - ragPhase: 2, - }, - - // --- Frankreich --- - { - id: 'FR-LIL', - countryCode: 'FR', - name: 'Loi Informatique et Libertés', - fullName: 'Loi n° 78-17 du 6 janvier 1978 relative à l\'informatique, aux fichiers et aux libertés', - abbreviation: 'LIL', - type: 'national_law', - description: - 'Franzoesisches Begleitgesetz zur DSGVO (aktualisiert 2018). Spezialregelungen u.a. ' + - 'zur Einwilligung Minderjaehriger (ab 15 Jahren), Forschungsdaten und Gesundheitsdaten.', - sourceUrl: 'https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000886460', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Gesetz, Légifrance (gemeinfrei)', - supervisoryAuthorities: [ - { name: 'Commission Nationale de l\'Informatique et des Libertés', abbreviation: 'CNIL', url: 'https://www.cnil.fr', country: 'FR' }, - ], - ragPhase: 2, - }, - - // --- Spanien --- - { - id: 'ES-LOPDGDD', - countryCode: 'ES', - name: 'LOPDGDD', - fullName: 'Ley Orgánica 3/2018 de Protección de Datos Personales y garantía de los derechos digitales', - abbreviation: 'LOPDGDD', - type: 'national_law', - description: - 'Spanisches Datenschutzgesetz. Ergaenzt DSGVO u.a. mit Regelungen zu ' + - 'Kindereinwilligung, digitalem Testament und Rechten Verstorbener.', - sourceUrl: 'https://www.boe.es/diario_boe/txt.php?id=BOE-A-2018-16673', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Gesetz, Boletín Oficial del Estado (gemeinfrei)', - supervisoryAuthorities: [ - { name: 'Agencia Española de Protección de Datos', abbreviation: 'AEPD', url: 'https://www.aepd.es', country: 'ES' }, - ], - ragPhase: 2, - }, - - // --- Italien --- - { - id: 'IT-CODICE', - countryCode: 'IT', - name: 'Codice Privacy', - fullName: 'Decreto Legislativo 30 giugno 2003, n. 196 (Codice in materia di protezione dei dati personali)', - abbreviation: 'Codice Privacy', - type: 'national_law', - description: - 'Italienischer Datenschutzkodex, angepasst an die DSGVO (D.Lgs. 101/2018). ' + - 'Enthaelt Spezialregelungen fuer Gesundheitsdaten, Forschung und Journalismus.', - sourceUrl: 'https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:decreto.legislativo:2003-06-30;196!vig=', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Gesetz, Normattiva (gemeinfrei)', - supervisoryAuthorities: [ - { name: 'Garante per la protezione dei dati personali', abbreviation: 'Garante', url: 'https://www.garanteprivacy.it', country: 'IT' }, - ], - ragPhase: 2, - }, - - // --- Niederlande --- - { - id: 'NL-AVG', - countryCode: 'NL', - name: 'AVG / UAVG', - fullName: 'Uitvoeringswet Algemene verordening gegevensbescherming (UAVG)', - abbreviation: 'UAVG', - type: 'national_law', - description: - 'Niederlaendisches Ausfuehrungsgesetz zur DSGVO.', - sourceUrl: 'https://wetten.overheid.nl/BWBR0040948/', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Gesetz, wetten.overheid.nl (gemeinfrei)', - supervisoryAuthorities: [ - { name: 'Autoriteit Persoonsgegevens', abbreviation: 'AP', url: 'https://www.autoriteitpersoonsgegevens.nl', country: 'NL' }, - ], - ragPhase: 2, - }, - - // --- Grossbritannien (post-Brexit) --- - { - id: 'GB-DPA', - countryCode: 'GB', - name: 'UK DPA 2018 / UK GDPR', - fullName: 'Data Protection Act 2018 + UK GDPR (retained EU law)', - abbreviation: 'DPA 2018', - type: 'national_law', - description: - 'Nach Brexit: UK GDPR (inhaltlich weitgehend identisch mit EU-DSGVO) plus Data Protection Act 2018 ' + - 'als nationales Begleitgesetz. ICO als Aufsichtsbehoerde.', - sourceUrl: 'https://www.legislation.gov.uk/ukpga/2018/12/contents', - license: 'OGL-3.0', - licenseNote: 'UK legislation, Open Government Licence v3.0', - supervisoryAuthorities: [ - { name: 'Information Commissioner\'s Office', abbreviation: 'ICO', url: 'https://ico.org.uk', country: 'GB' }, - ], - ragPhase: 2, - }, - - // --- Norwegen (EWR) --- - { - id: 'NO-PERSONOPPL', - countryCode: 'NO', - name: 'Personopplysningsloven', - fullName: 'Lov om behandling av personopplysninger (personopplysningsloven)', - abbreviation: 'POL', - type: 'national_law', - description: - 'Norwegisches DSGVO-Ausfuehrungsgesetz (EWR-Mitglied, DSGVO gilt ueber EWR-Abkommen).', - sourceUrl: 'https://lovdata.no/dokument/NL/lov/2018-06-15-38', - license: 'PUBLIC_DOMAIN', - licenseNote: 'Amtliches Gesetz, Lovdata (gemeinfrei)', - supervisoryAuthorities: [ - { name: 'Datatilsynet', abbreviation: 'DT', url: 'https://www.datatilsynet.no', country: 'NO' }, - ], - ragPhase: 2, - }, -] - // ============================================================================= // Dokumenttyp-Matrix: EU-einheitlich vs. laenderspezifisch // ============================================================================= export const DOCUMENT_TYPE_MATRIX: DocumentTypeMatrix[] = [ - { - documentType: 'privacy_policy', - label: 'Datenschutzerklaerung', - uniformity: 'needs_national_supplement', - description: 'DSGVO-Kern EU-weit gleich. Nationale Ergaenzungen fuer ePrivacy-Umsetzung, Behoerden-Praxis.', - }, - { - documentType: 'ropa', - label: 'Verarbeitungsverzeichnis (VVT)', - uniformity: 'eu_uniform', - description: 'Art. 30 DSGVO — EU-weit identische Anforderungen.', - }, - { - documentType: 'tom', - label: 'Technisch-Organisatorische Massnahmen', - uniformity: 'eu_uniform', - description: 'Art. 32 DSGVO — EU-weit identische Anforderungen.', - }, - { - documentType: 'dpia', - label: 'Datenschutz-Folgenabschaetzung (DSFA)', - uniformity: 'eu_uniform', - description: 'Art. 35 DSGVO — EU-weit identisch. Muss-Listen variieren je Aufsichtsbehoerde.', - }, - { - documentType: 'dpa', - label: 'Auftragsverarbeitungsvertrag (AVV)', - uniformity: 'eu_uniform', - description: 'Art. 28 DSGVO — EU-weit identische Anforderungen.', - }, - { - documentType: 'deletion_concept', - label: 'Loeschkonzept', - uniformity: 'eu_uniform', - description: 'Art. 5(1)(e), Art. 17 DSGVO — EU-weit einheitlich.', - }, - { - documentType: 'breach_process', - label: 'Data Breach / Incident Response', - uniformity: 'eu_uniform', - description: 'Art. 33-34 DSGVO — EU-weit identische 72-Stunden-Frist.', - }, - { - documentType: 'dsar_process', - label: 'Betroffenenrechte-Prozess (DSAR)', - uniformity: 'eu_uniform', - description: 'Art. 12-22 DSGVO — EU-weit identische Rechte und Fristen.', - }, + { documentType: 'privacy_policy', label: 'Datenschutzerklaerung', uniformity: 'needs_national_supplement', description: 'DSGVO-Kern EU-weit gleich. Nationale Ergaenzungen fuer ePrivacy-Umsetzung, Behoerden-Praxis.' }, + { documentType: 'ropa', label: 'Verarbeitungsverzeichnis (VVT)', uniformity: 'eu_uniform', description: 'Art. 30 DSGVO — EU-weit identische Anforderungen.' }, + { documentType: 'tom', label: 'Technisch-Organisatorische Massnahmen', uniformity: 'eu_uniform', description: 'Art. 32 DSGVO — EU-weit identische Anforderungen.' }, + { documentType: 'dpia', label: 'Datenschutz-Folgenabschaetzung (DSFA)', uniformity: 'eu_uniform', description: 'Art. 35 DSGVO — EU-weit identisch. Muss-Listen variieren je Aufsichtsbehoerde.' }, + { documentType: 'dpa', label: 'Auftragsverarbeitungsvertrag (AVV)', uniformity: 'eu_uniform', description: 'Art. 28 DSGVO — EU-weit identische Anforderungen.' }, + { documentType: 'deletion_concept', label: 'Loeschkonzept', uniformity: 'eu_uniform', description: 'Art. 5(1)(e), Art. 17 DSGVO — EU-weit einheitlich.' }, + { documentType: 'breach_process', label: 'Data Breach / Incident Response', uniformity: 'eu_uniform', description: 'Art. 33-34 DSGVO — EU-weit identische 72-Stunden-Frist.' }, + { documentType: 'dsar_process', label: 'Betroffenenrechte-Prozess (DSAR)', uniformity: 'eu_uniform', description: 'Art. 12-22 DSGVO — EU-weit identische Rechte und Fristen.' }, { documentType: 'imprint', label: 'Impressum', @@ -457,24 +182,9 @@ export const DOCUMENT_TYPE_MATRIX: DocumentTypeMatrix[] = [ 'IS': 'Rafraeðislög', }, }, - { - documentType: 'terms_of_service', - label: 'AGB / Nutzungsbedingungen', - uniformity: 'country_specific', - description: 'Nationales Vertragsrecht (BGB, ABGB, OR). Verbraucherrecht teils EU-harmonisiert, aber national umgesetzt.', - }, - { - documentType: 'withdrawal_notice', - label: 'Widerrufsbelehrung', - uniformity: 'country_specific', - description: 'EU-Verbraucherrechterichtlinie national umgesetzt. DE/AT: Muster-Widerrufsbelehrung. CH: eigene Logik.', - }, - { - documentType: 'cookie_banner', - label: 'Cookie-Banner / Consent', - uniformity: 'needs_national_supplement', - description: 'ePrivacy + DSGVO EU-weit aehnlich, aber Aufsichtspraxis variiert (CNIL vs. DSK vs. DPC etc.).', - }, + { documentType: 'terms_of_service', label: 'AGB / Nutzungsbedingungen', uniformity: 'country_specific', description: 'Nationales Vertragsrecht (BGB, ABGB, OR). Verbraucherrecht teils EU-harmonisiert, aber national umgesetzt.' }, + { documentType: 'withdrawal_notice', label: 'Widerrufsbelehrung', uniformity: 'country_specific', description: 'EU-Verbraucherrechterichtlinie national umgesetzt. DE/AT: Muster-Widerrufsbelehrung. CH: eigene Logik.' }, + { documentType: 'cookie_banner', label: 'Cookie-Banner / Consent', uniformity: 'needs_national_supplement', description: 'ePrivacy + DSGVO EU-weit aehnlich, aber Aufsichtspraxis variiert (CNIL vs. DSK vs. DPC etc.).' }, ] // ============================================================================= @@ -533,9 +243,27 @@ export const RAG_LAYERS: RAGLayer[] = [ ] // ============================================================================= -// Helper Functions +// Re-exports from national sibling (backward compat — no consumer import changes) // ============================================================================= +export { + NATIONAL_FRAMEWORKS, + getAllSupervisoryAuthorities, + getSupervisoryAuthority, + getCountrySpecificDocTypes, + getEUUniformDocTypes, + isGDPRCountry, + hasSeparateLegalFramework, + getRAGSourcesForPhase, + getRequiredFrameworkSummary, +} from './eu-legal-frameworks-national' + +// ============================================================================= +// Helper Functions (EU-level) +// ============================================================================= + +import { NATIONAL_FRAMEWORKS } from './eu-legal-frameworks-national' + /** Alle Rechtsgrundlagen zusammen (EU-Basis + National) */ export function getAllFrameworks(): LegalFramework[] { return [...EU_BASE_FRAMEWORKS, ...NATIONAL_FRAMEWORKS] @@ -552,71 +280,3 @@ export function getFrameworksForCountry(country: CountryCode): LegalFramework[] export function getNationalFrameworks(country: CountryCode): LegalFramework[] { return NATIONAL_FRAMEWORKS.filter(f => f.countryCode === country) } - -/** Alle Aufsichtsbehoerden */ -export function getAllSupervisoryAuthorities(): SupervisoryAuthority[] { - const authorities: SupervisoryAuthority[] = [] - for (const fw of getAllFrameworks()) { - if (fw.supervisoryAuthorities) { - for (const sa of fw.supervisoryAuthorities) { - if (!authorities.some(a => a.abbreviation === sa.abbreviation)) { - authorities.push(sa) - } - } - } - } - return authorities -} - -/** Aufsichtsbehoerde(n) fuer ein Land */ -export function getSupervisoryAuthority(country: CountryCode): SupervisoryAuthority[] { - return getAllSupervisoryAuthorities().filter(sa => sa.country === country) -} - -/** Dokumenttypen, die fuer ein Land spezifische Logik brauchen */ -export function getCountrySpecificDocTypes(country: CountryCode): DocumentTypeMatrix[] { - return DOCUMENT_TYPE_MATRIX.filter( - d => d.uniformity === 'country_specific' || - (d.uniformity === 'needs_national_supplement' && country !== 'EU') - ) -} - -/** Dokumenttypen, die EU-weit einheitlich generiert werden koennen */ -export function getEUUniformDocTypes(): DocumentTypeMatrix[] { - return DOCUMENT_TYPE_MATRIX.filter(d => d.uniformity === 'eu_uniform') -} - -/** Pruefen ob ein Land EU/EWR-Mitglied ist (DSGVO direkt anwendbar) */ -export function isGDPRCountry(country: CountryCode): boolean { - const gdprCountries: CountryCode[] = ['EU', 'DE', 'AT', 'FR', 'ES', 'IT', 'NL', 'NO', 'IS'] - return gdprCountries.includes(country) -} - -/** Pruefen ob ein Land einen separaten Rechtsrahmen hat (nicht DSGVO) */ -export function hasSeparateLegalFramework(country: CountryCode): boolean { - return country === 'CH' || country === 'GB' -} - -/** RAG-Quellen fuer eine bestimmte Phase */ -export function getRAGSourcesForPhase(phase: 1 | 2 | 3): LegalFramework[] { - return getAllFrameworks().filter(f => f.ragPhase === phase) -} - -/** Zusammenfassung: Was braucht ein Unternehmen in Land X? */ -export function getRequiredFrameworkSummary(country: CountryCode): { - baseLaw: string - nationalLaw: string | null - supervisoryAuthority: string | null - separateFramework: boolean -} { - const isGDPR = isGDPRCountry(country) - const national = getNationalFrameworks(country) - const authorities = getSupervisoryAuthority(country) - - return { - baseLaw: isGDPR ? 'DSGVO (EU 2016/679)' : (country === 'CH' ? 'revDSG (CH)' : 'UK GDPR'), - nationalLaw: national.length > 0 ? national.map(n => n.abbreviation).join(', ') : null, - supervisoryAuthority: authorities.length > 0 ? authorities.map(a => a.abbreviation).join(', ') : null, - separateFramework: hasSeparateLegalFramework(country), - } -} diff --git a/admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts b/admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts new file mode 100644 index 0000000..676febc --- /dev/null +++ b/admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts @@ -0,0 +1,290 @@ +// ============================================================================= +// Loeschfristen Module - Policy Generator +// Generates deletion policies from profiling answers +// ============================================================================= + +import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types' +import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog' +import type { ProfilingAnswer, ProfilingResult } from './loeschfristen-profiling-data' +import { PROFILING_STEPS } from './loeschfristen-profiling-data' + +// Re-export ProfilingResult so callers can type-annotate +export type { ProfilingResult } + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/** + * Retrieve the value of a specific answer by question ID. + */ +export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown { + const answer = answers.find(a => a.questionId === questionId) + return answer?.value ?? undefined +} + +/** + * Check whether all required questions in a given step have been answered. + */ +export function isStepComplete(answers: ProfilingAnswer[], stepId: import('./loeschfristen-profiling-data').ProfilingStepId): boolean { + const step = PROFILING_STEPS.find(s => s.id === stepId) + if (!step) return false + + return step.questions + .filter(q => q.required) + .every(q => { + const answer = answers.find(a => a.questionId === q.id) + if (!answer) return false + + const val = answer.value + if (val === undefined || val === null) return false + if (typeof val === 'string' && val.trim() === '') return false + if (Array.isArray(val) && val.length === 0) return false + + return true + }) +} + +/** + * Calculate overall profiling progress as a percentage (0-100). + */ +export function getProfilingProgress(answers: ProfilingAnswer[]): number { + const totalRequired = PROFILING_STEPS.reduce( + (sum, step) => sum + step.questions.filter(q => q.required).length, + 0 + ) + if (totalRequired === 0) return 100 + + const answeredRequired = PROFILING_STEPS.reduce((sum, step) => { + return ( + sum + + step.questions.filter(q => q.required).filter(q => { + const answer = answers.find(a => a.questionId === q.id) + if (!answer) return false + const val = answer.value + if (val === undefined || val === null) return false + if (typeof val === 'string' && val.trim() === '') return false + if (Array.isArray(val) && val.length === 0) return false + return true + }).length + ) + }, 0) + + return Math.round((answeredRequired / totalRequired) * 100) +} + +// ============================================================================= +// CORE GENERATOR +// ============================================================================= + +/** + * Generate deletion policies based on the profiling answers. + * + * Logic: + * - Match baseline templates based on boolean and categorical answers + * - Deduplicate matched templates by templateId + * - Convert matched templates to full LoeschfristPolicy objects + * - Add additional storage locations (Cloud, Backup) if applicable + * - Detect legal hold requirements + */ +export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): ProfilingResult { + const matchedTemplateIds = new Set() + + const getBool = (questionId: string): boolean => { + const val = getAnswerValue(answers, questionId) + return val === true + } + + const getString = (questionId: string): string => { + const val = getAnswerValue(answers, questionId) + return typeof val === 'string' ? val : '' + } + + // Always-included templates (universally recommended) + matchedTemplateIds.add('protokolle-gesellschafter') + + // HR data (data-hr = true) + if (getBool('data-hr')) { + matchedTemplateIds.add('personal-akten') + matchedTemplateIds.add('gehaltsabrechnungen') + matchedTemplateIds.add('zeiterfassung') + matchedTemplateIds.add('bewerbungsunterlagen') + matchedTemplateIds.add('krankmeldungen') + matchedTemplateIds.add('schulungsnachweise') + } + + // Buchhaltung (data-buchhaltung = true) + if (getBool('data-buchhaltung')) { + matchedTemplateIds.add('buchhaltungsbelege') + matchedTemplateIds.add('rechnungen') + matchedTemplateIds.add('steuererklaerungen') + } + + // Vertraege (data-vertraege = true) + if (getBool('data-vertraege')) { + matchedTemplateIds.add('vertraege') + matchedTemplateIds.add('geschaeftsbriefe') + matchedTemplateIds.add('kundenstammdaten') + matchedTemplateIds.add('kundenreklamationen') + matchedTemplateIds.add('lieferantenbewertungen') + } + + // Marketing (data-marketing = true) + if (getBool('data-marketing')) { + matchedTemplateIds.add('newsletter-einwilligungen') + matchedTemplateIds.add('crm-kontakthistorie') + matchedTemplateIds.add('cookie-consent-logs') + matchedTemplateIds.add('social-media-daten') + } + + // Video (data-video = true) + if (getBool('data-video')) { + matchedTemplateIds.add('videoueberwachung') + } + + // Website (org-website = true) + if (getBool('org-website')) { + matchedTemplateIds.add('webserver-logs') + matchedTemplateIds.add('cookie-consent-logs') + } + + // Cloud (sys-cloud = true) → E-Mail-Archivierung + if (getBool('sys-cloud')) { + matchedTemplateIds.add('email-archivierung') + } + + // Zutritt (sys-zutritt = true) + if (getBool('sys-zutritt')) { + matchedTemplateIds.add('zutrittsprotokolle') + } + + // ERP/CRM (sys-erp = true) + if (getBool('sys-erp')) { + matchedTemplateIds.add('kundenstammdaten') + matchedTemplateIds.add('crm-kontakthistorie') + } + + // Backup (sys-backup = true) + if (getBool('sys-backup')) { + matchedTemplateIds.add('backup-daten') + } + + // Gesundheitsdaten (special-gesundheit = true) + if (getBool('special-gesundheit')) { + matchedTemplateIds.add('krankmeldungen') + matchedTemplateIds.add('betriebsarzt-doku') + } + + // Resolve matched templates from catalog + const matchedTemplates: BaselineTemplate[] = [] + for (const templateId of matchedTemplateIds) { + const template = BASELINE_TEMPLATES.find(t => t.templateId === templateId) + if (template) { + matchedTemplates.push(template) + } + } + + // Convert to policies + const generatedPolicies: LoeschfristPolicy[] = matchedTemplates.map(template => + templateToPolicy(template) + ) + + // Additional storage locations + const additionalStorageLocations: StorageLocation[] = [] + + if (getBool('sys-cloud')) { + const cloudLocation: StorageLocation = { + id: crypto.randomUUID(), + name: 'Cloud-Speicher', + type: 'CLOUD', + isBackup: false, + provider: null, + deletionCapable: true, + } + additionalStorageLocations.push(cloudLocation) + for (const policy of generatedPolicies) { + policy.storageLocations.push({ ...cloudLocation, id: crypto.randomUUID() }) + } + } + + if (getBool('sys-backup')) { + const backupLocation: StorageLocation = { + id: crypto.randomUUID(), + name: 'Backup-System', + type: 'BACKUP', + isBackup: true, + provider: null, + deletionCapable: true, + } + additionalStorageLocations.push(backupLocation) + for (const policy of generatedPolicies) { + policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() }) + } + } + + // Legal Hold + const hasLegalHoldRequirement = getBool('special-legal-hold') + if (hasLegalHoldRequirement) { + for (const policy of generatedPolicies) { + policy.hasActiveLegalHold = true + policy.deletionTrigger = 'LEGAL_HOLD' + } + } + + // Tag policies with profiling metadata + const branche = getString('org-branche') + const mitarbeiter = getString('org-mitarbeiter') + + for (const policy of generatedPolicies) { + policy.tags = [ + ...policy.tags, + 'profiling-generated', + ...(branche ? [`branche:${branche}`] : []), + ...(mitarbeiter ? [`groesse:${mitarbeiter}`] : []), + ] + } + + return { + matchedTemplates, + generatedPolicies, + additionalStorageLocations, + hasLegalHoldRequirement, + } +} + +// ============================================================================= +// COMPLIANCE SCOPE INTEGRATION +// ============================================================================= + +/** + * Prefill Loeschfristen profiling answers from Compliance Scope Engine answers. + * The Scope Engine acts as the "Single Source of Truth" for organizational questions. + */ +export function prefillFromScopeAnswers( + scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] +): ProfilingAnswer[] { + const { exportToLoeschfristenAnswers } = require('./compliance-scope-profiling') + const exported = exportToLoeschfristenAnswers(scopeAnswers) as Array<{ questionId: string; value: unknown }> + return exported.map(item => ({ + questionId: item.questionId, + value: item.value as string | string[] | boolean | number, + })) +} + +/** + * Get the list of Loeschfristen question IDs that are prefilled from Scope answers. + * These questions should show "Aus Scope-Analyse uebernommen" hint. + */ +export const SCOPE_PREFILLED_LF_QUESTIONS = [ + 'org-branche', + 'org-mitarbeiter', + 'org-geschaeftsmodell', + 'org-website', + 'data-hr', + 'data-buchhaltung', + 'data-vertraege', + 'data-marketing', + 'data-video', + 'sys-cloud', + 'sys-erp', +] diff --git a/admin-compliance/lib/sdk/loeschfristen-profiling.ts b/admin-compliance/lib/sdk/loeschfristen-profiling.ts index 3eed2de..e66164b 100644 --- a/admin-compliance/lib/sdk/loeschfristen-profiling.ts +++ b/admin-compliance/lib/sdk/loeschfristen-profiling.ts @@ -1,565 +1,23 @@ // ============================================================================= -// Loeschfristen Module - Profiling Wizard -// 4-Step Profiling (16 Fragen) zur Generierung von Baseline-Loeschrichtlinien +// Loeschfristen Module - Profiling Wizard (barrel) // ============================================================================= -import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types' -import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog' -import type { ProfilingAnswer, ProfilingStepId, ProfilingResult } from './loeschfristen-profiling-data' +// Types, steps, and step-level data +export type { + ProfilingStepId, + ProfilingQuestion, + ProfilingAnswer, + ProfilingStep, + ProfilingResult, +} from './loeschfristen-profiling-data' +export { PROFILING_STEPS } from './loeschfristen-profiling-data' -// Re-export types + data so existing imports work unchanged -export { type ProfilingStepId, type ProfilingQuestion, type ProfilingAnswer, type ProfilingStep, type ProfilingResult, PROFILING_STEPS } from './loeschfristen-profiling-data' - -export type ProfilingStepId = 'organization' | 'data-categories' | 'systems' | 'special' - -export interface ProfilingQuestion { - id: string - step: ProfilingStepId - question: string // German - helpText?: string - type: 'single' | 'multi' | 'boolean' | 'number' - options?: { value: string; label: string }[] - required: boolean -} - -export interface ProfilingAnswer { - questionId: string - value: string | string[] | boolean | number -} - -export interface ProfilingStep { - id: ProfilingStepId - title: string - description: string - questions: ProfilingQuestion[] -} - -export interface ProfilingResult { - matchedTemplates: BaselineTemplate[] - generatedPolicies: LoeschfristPolicy[] - additionalStorageLocations: StorageLocation[] - hasLegalHoldRequirement: boolean -} - -// ============================================================================= -// PROFILING STEPS (4 Steps, 16 Questions) -// ============================================================================= - -export const PROFILING_STEPS: ProfilingStep[] = [ - // ========================================================================= - // Step 1: Organisation (4 Fragen) - // ========================================================================= - { - id: 'organization', - title: 'Organisation', - description: 'Allgemeine Informationen zu Ihrem Unternehmen, um branchenspezifische Loeschfristen zu ermitteln.', - questions: [ - { - id: 'org-branche', - step: 'organization', - question: 'In welcher Branche ist Ihr Unternehmen taetig?', - helpText: 'Die Branche bestimmt, welche branchenspezifischen Aufbewahrungspflichten relevant sind.', - type: 'single', - options: [ - { value: 'it-software', label: 'IT / Software' }, - { value: 'handel', label: 'Handel' }, - { value: 'dienstleistung', label: 'Dienstleistung' }, - { value: 'gesundheitswesen', label: 'Gesundheitswesen' }, - { value: 'bildung', label: 'Bildung' }, - { value: 'fertigung-industrie', label: 'Fertigung / Industrie' }, - { value: 'finanzwesen', label: 'Finanzwesen' }, - { value: 'oeffentlicher-sektor', label: 'Oeffentlicher Sektor' }, - { value: 'sonstige', label: 'Sonstige' }, - ], - required: true, - }, - { - id: 'org-mitarbeiter', - step: 'organization', - question: 'Wie viele Mitarbeiter hat Ihr Unternehmen?', - helpText: 'Die Unternehmensgroesse beeinflusst den Umfang der erforderlichen Loeschkonzepte.', - type: 'single', - options: [ - { value: '<10', label: 'Weniger als 10' }, - { value: '10-49', label: '10 bis 49' }, - { value: '50-249', label: '50 bis 249' }, - { value: '250+', label: '250 und mehr' }, - ], - required: true, - }, - { - id: 'org-geschaeftsmodell', - step: 'organization', - question: 'Welches Geschaeftsmodell verfolgen Sie?', - helpText: 'B2B und B2C haben unterschiedliche Anforderungen an die Datenhaltung.', - type: 'single', - options: [ - { value: 'b2b', label: 'B2B (Geschaeftskunden)' }, - { value: 'b2c', label: 'B2C (Endkunden)' }, - { value: 'beides', label: 'Beides (B2B und B2C)' }, - ], - required: true, - }, - { - id: 'org-website', - step: 'organization', - question: 'Betreiben Sie eine Website oder Online-Praesenz?', - helpText: 'Websites erzeugen Webserver-Logs und erfordern Cookie-Consent-Verwaltung.', - type: 'boolean', - required: true, - }, - ], - }, - - // ========================================================================= - // Step 2: Datenkategorien (5 Fragen) - // ========================================================================= - { - id: 'data-categories', - title: 'Datenkategorien', - description: 'Welche Arten personenbezogener Daten verarbeiten Sie? Dies bestimmt die relevanten Aufbewahrungsfristen.', - questions: [ - { - id: 'data-hr', - step: 'data-categories', - question: 'Verarbeiten Sie HR-/Personaldaten (Personalakten, Gehaltsabrechnungen, Zeiterfassung)?', - helpText: 'Personalakten unterliegen umfangreichen gesetzlichen Aufbewahrungspflichten (bis zu 10 Jahre).', - type: 'boolean', - required: true, - }, - { - id: 'data-buchhaltung', - step: 'data-categories', - question: 'Fuehren Sie eine Buchhaltung mit Finanzdaten (Rechnungen, Belege, Steuererklarungen)?', - helpText: 'Buchhaltungsunterlagen muessen gemaess HGB und AO bis zu 10 Jahre aufbewahrt werden.', - type: 'boolean', - required: true, - }, - { - id: 'data-vertraege', - step: 'data-categories', - question: 'Verwalten Sie Vertraege mit Kunden oder Lieferanten?', - helpText: 'Vertragsunterlagen und Geschaeftsbriefe haben spezifische Aufbewahrungspflichten.', - type: 'boolean', - required: true, - }, - { - id: 'data-marketing', - step: 'data-categories', - question: 'Betreiben Sie Marketing-Aktivitaeten (Newsletter, CRM-Kampagnen)?', - helpText: 'Marketing-Einwilligungen und Kontakthistorien muessen dokumentiert und verwaltet werden.', - type: 'boolean', - required: true, - }, - { - id: 'data-video', - step: 'data-categories', - question: 'Setzen Sie Videoueberwachung ein?', - helpText: 'Videoueberwachungsdaten haben besonders kurze Loeschfristen (in der Regel 72 Stunden).', - type: 'boolean', - required: true, - }, - ], - }, - - // ========================================================================= - // Step 3: Systeme (4 Fragen) - // ========================================================================= - { - id: 'systems', - title: 'Systeme & Infrastruktur', - description: 'Welche IT-Systeme und Infrastruktur nutzen Sie? Dies beeinflusst die Speicherorte in Ihrem Loeschkonzept.', - questions: [ - { - id: 'sys-cloud', - step: 'systems', - question: 'Nutzen Sie Cloud-Dienste zur Datenspeicherung oder -verarbeitung?', - helpText: 'Cloud-Speicherorte muessen in den Loeschrichtlinien als separate Speicherorte dokumentiert werden.', - type: 'boolean', - required: true, - }, - { - id: 'sys-backup', - step: 'systems', - question: 'Haben Sie Backup-Systeme im Einsatz?', - helpText: 'Backups erfordern eine eigene Loeschstrategie, da Daten dort nach der primaeren Loeschung weiter existieren koennen.', - type: 'boolean', - required: true, - }, - { - id: 'sys-erp', - step: 'systems', - question: 'Setzen Sie ein ERP- oder CRM-System ein?', - helpText: 'ERP-/CRM-Systeme sind haeufig zentrale Speicherorte fuer Kunden- und Geschaeftsdaten.', - type: 'boolean', - required: true, - }, - { - id: 'sys-zutritt', - step: 'systems', - question: 'Nutzen Sie ein Zutrittskontrollsystem?', - helpText: 'Zutrittskontrollsysteme erzeugen Protokolle, die personenbezogene Daten enthalten und einer Loeschfrist unterliegen.', - type: 'boolean', - required: true, - }, - ], - }, - - // ========================================================================= - // Step 4: Spezielle Anforderungen (3 Fragen) - // ========================================================================= - { - id: 'special', - title: 'Spezielle Anforderungen', - description: 'Gibt es besondere rechtliche oder organisatorische Anforderungen, die Ihr Loeschkonzept beeinflussen?', - questions: [ - { - id: 'special-legal-hold', - step: 'special', - question: 'Gibt es Legal-Hold-Anforderungen (z.B. laufende Rechtsstreitigkeiten, behoerdliche Untersuchungen)?', - helpText: 'Bei einem Legal Hold muessen betroffene Daten trotz abgelaufener Loeschfristen aufbewahrt werden.', - type: 'boolean', - required: true, - }, - { - id: 'special-archivierung', - step: 'special', - question: 'Benoetigen Sie eine Langzeitarchivierung von Dokumenten?', - helpText: 'Langzeitarchivierung kann ueber die gesetzlichen Mindestfristen hinausgehen und erfordert eine gesonderte Rechtfertigung.', - type: 'boolean', - required: true, - }, - { - id: 'special-gesundheit', - step: 'special', - question: 'Verarbeiten Sie Gesundheitsdaten (z.B. Krankmeldungen, Arbeitsmedizin)?', - helpText: 'Gesundheitsdaten sind besonders schuetzenswerte Daten nach Art. 9 DSGVO und unterliegen strengeren Anforderungen.', - type: 'boolean', - required: true, - }, - ], - }, -] - -// ============================================================================= -// HELPER FUNCTIONS -// ============================================================================= - -/** - * Retrieve the value of a specific answer by question ID. - */ -export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown { - const answer = answers.find(a => a.questionId === questionId) - return answer?.value ?? undefined -} - -/** - * Check whether all required questions in a given step have been answered. - */ -export function isStepComplete(answers: ProfilingAnswer[], stepId: ProfilingStepId): boolean { - const step = PROFILING_STEPS.find(s => s.id === stepId) - if (!step) return false - - return step.questions - .filter(q => q.required) - .every(q => { - const answer = answers.find(a => a.questionId === q.id) - if (!answer) return false - - // Check that the value is not empty - const val = answer.value - if (val === undefined || val === null) return false - if (typeof val === 'string' && val.trim() === '') return false - if (Array.isArray(val) && val.length === 0) return false - - return true - }) -} - -/** - * Calculate overall profiling progress as a percentage (0-100). - */ -export function getProfilingProgress(answers: ProfilingAnswer[]): number { - const totalRequired = PROFILING_STEPS.reduce( - (sum, step) => sum + step.questions.filter(q => q.required).length, - 0 - ) - if (totalRequired === 0) return 100 - - const answeredRequired = PROFILING_STEPS.reduce((sum, step) => { - return ( - sum + - step.questions.filter(q => q.required).filter(q => { - const answer = answers.find(a => a.questionId === q.id) - if (!answer) return false - const val = answer.value - if (val === undefined || val === null) return false - if (typeof val === 'string' && val.trim() === '') return false - if (Array.isArray(val) && val.length === 0) return false - return true - }).length - ) - }, 0) - - return Math.round((answeredRequired / totalRequired) * 100) -} - -// ============================================================================= -// CORE GENERATOR -// ============================================================================= - -/** - * Generate deletion policies based on the profiling answers. - * - * Logic: - * - Match baseline templates based on boolean and categorical answers - * - Deduplicate matched templates by templateId - * - Convert matched templates to full LoeschfristPolicy objects - * - Add additional storage locations (Cloud, Backup) if applicable - * - Detect legal hold requirements - */ -export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): ProfilingResult { - const matchedTemplateIds = new Set() - - // ------------------------------------------------------------------------- - // Helper to get a boolean answer - // ------------------------------------------------------------------------- - const getBool = (questionId: string): boolean => { - const val = getAnswerValue(answers, questionId) - return val === true - } - - const getString = (questionId: string): string => { - const val = getAnswerValue(answers, questionId) - return typeof val === 'string' ? val : '' - } - - // ------------------------------------------------------------------------- - // Always-included templates (universally recommended) - // ------------------------------------------------------------------------- - matchedTemplateIds.add('protokolle-gesellschafter') - - // ------------------------------------------------------------------------- - // HR data (data-hr = true) - // ------------------------------------------------------------------------- - if (getBool('data-hr')) { - matchedTemplateIds.add('personal-akten') - matchedTemplateIds.add('gehaltsabrechnungen') - matchedTemplateIds.add('zeiterfassung') - matchedTemplateIds.add('bewerbungsunterlagen') - matchedTemplateIds.add('krankmeldungen') - matchedTemplateIds.add('schulungsnachweise') - } - - // ------------------------------------------------------------------------- - // Buchhaltung (data-buchhaltung = true) - // ------------------------------------------------------------------------- - if (getBool('data-buchhaltung')) { - matchedTemplateIds.add('buchhaltungsbelege') - matchedTemplateIds.add('rechnungen') - matchedTemplateIds.add('steuererklaerungen') - } - - // ------------------------------------------------------------------------- - // Vertraege (data-vertraege = true) - // ------------------------------------------------------------------------- - if (getBool('data-vertraege')) { - matchedTemplateIds.add('vertraege') - matchedTemplateIds.add('geschaeftsbriefe') - matchedTemplateIds.add('kundenstammdaten') - matchedTemplateIds.add('kundenreklamationen') - matchedTemplateIds.add('lieferantenbewertungen') - } - - // ------------------------------------------------------------------------- - // Marketing (data-marketing = true) - // ------------------------------------------------------------------------- - if (getBool('data-marketing')) { - matchedTemplateIds.add('newsletter-einwilligungen') - matchedTemplateIds.add('crm-kontakthistorie') - matchedTemplateIds.add('cookie-consent-logs') - matchedTemplateIds.add('social-media-daten') - } - - // ------------------------------------------------------------------------- - // Video (data-video = true) - // ------------------------------------------------------------------------- - if (getBool('data-video')) { - matchedTemplateIds.add('videoueberwachung') - } - - // ------------------------------------------------------------------------- - // Website (org-website = true) - // ------------------------------------------------------------------------- - if (getBool('org-website')) { - matchedTemplateIds.add('webserver-logs') - matchedTemplateIds.add('cookie-consent-logs') - } - - // ------------------------------------------------------------------------- - // Cloud (sys-cloud = true) → E-Mail-Archivierung - // ------------------------------------------------------------------------- - if (getBool('sys-cloud')) { - matchedTemplateIds.add('email-archivierung') - } - - // ------------------------------------------------------------------------- - // Zutritt (sys-zutritt = true) - // ------------------------------------------------------------------------- - if (getBool('sys-zutritt')) { - matchedTemplateIds.add('zutrittsprotokolle') - } - - // ------------------------------------------------------------------------- - // ERP/CRM (sys-erp = true) - // ------------------------------------------------------------------------- - if (getBool('sys-erp')) { - matchedTemplateIds.add('kundenstammdaten') - matchedTemplateIds.add('crm-kontakthistorie') - } - - // ------------------------------------------------------------------------- - // Backup (sys-backup = true) - // ------------------------------------------------------------------------- - if (getBool('sys-backup')) { - matchedTemplateIds.add('backup-daten') - } - - // ------------------------------------------------------------------------- - // Gesundheitsdaten (special-gesundheit = true) - // ------------------------------------------------------------------------- - if (getBool('special-gesundheit')) { - // Ensure krankmeldungen is included even without full HR data - matchedTemplateIds.add('krankmeldungen') - matchedTemplateIds.add('betriebsarzt-doku') - } - - // ------------------------------------------------------------------------- - // Resolve matched templates from catalog - // ------------------------------------------------------------------------- - const matchedTemplates: BaselineTemplate[] = [] - for (const templateId of matchedTemplateIds) { - const template = BASELINE_TEMPLATES.find(t => t.templateId === templateId) - if (template) { - matchedTemplates.push(template) - } - } - - // ------------------------------------------------------------------------- - // Convert to policies - // ------------------------------------------------------------------------- - const generatedPolicies: LoeschfristPolicy[] = matchedTemplates.map(template => - templateToPolicy(template) - ) - - // ------------------------------------------------------------------------- - // Additional storage locations - // ------------------------------------------------------------------------- - const additionalStorageLocations: StorageLocation[] = [] - - if (getBool('sys-cloud')) { - const cloudLocation: StorageLocation = { - id: crypto.randomUUID(), - name: 'Cloud-Speicher', - type: 'CLOUD', - isBackup: false, - provider: null, - deletionCapable: true, - } - additionalStorageLocations.push(cloudLocation) - - // Add Cloud storage location to all generated policies - for (const policy of generatedPolicies) { - policy.storageLocations.push({ ...cloudLocation, id: crypto.randomUUID() }) - } - } - - if (getBool('sys-backup')) { - const backupLocation: StorageLocation = { - id: crypto.randomUUID(), - name: 'Backup-System', - type: 'BACKUP', - isBackup: true, - provider: null, - deletionCapable: true, - } - additionalStorageLocations.push(backupLocation) - - // Add Backup storage location to all generated policies - for (const policy of generatedPolicies) { - policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() }) - } - } - - // ------------------------------------------------------------------------- - // Legal Hold - // ------------------------------------------------------------------------- - const hasLegalHoldRequirement = getBool('special-legal-hold') - - // If legal hold is active, mark all generated policies accordingly - if (hasLegalHoldRequirement) { - for (const policy of generatedPolicies) { - policy.hasActiveLegalHold = true - policy.deletionTrigger = 'LEGAL_HOLD' - } - } - - // ------------------------------------------------------------------------- - // Tag policies with profiling metadata - // ------------------------------------------------------------------------- - const branche = getString('org-branche') - const mitarbeiter = getString('org-mitarbeiter') - - for (const policy of generatedPolicies) { - policy.tags = [ - ...policy.tags, - 'profiling-generated', - ...(branche ? [`branche:${branche}`] : []), - ...(mitarbeiter ? [`groesse:${mitarbeiter}`] : []), - ] - } - - return { - matchedTemplates, - generatedPolicies, - additionalStorageLocations, - hasLegalHoldRequirement, - } -} - -// ============================================================================= -// COMPLIANCE SCOPE INTEGRATION -// ============================================================================= - -/** - * Prefill Loeschfristen profiling answers from Compliance Scope Engine answers. - * The Scope Engine acts as the "Single Source of Truth" for organizational questions. - */ -export function prefillFromScopeAnswers( - scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] -): ProfilingAnswer[] { - const { exportToLoeschfristenAnswers } = require('./compliance-scope-profiling') - const exported = exportToLoeschfristenAnswers(scopeAnswers) as Array<{ questionId: string; value: unknown }> - return exported.map(item => ({ - questionId: item.questionId, - value: item.value as string | string[] | boolean | number, - })) -} - -/** - * Get the list of Loeschfristen question IDs that are prefilled from Scope answers. - * These questions should show "Aus Scope-Analyse uebernommen" hint. - */ -export const SCOPE_PREFILLED_LF_QUESTIONS = [ - 'org-branche', - 'org-mitarbeiter', - 'org-geschaeftsmodell', - 'org-website', - 'data-hr', - 'data-buchhaltung', - 'data-vertraege', - 'data-marketing', - 'data-video', - 'sys-cloud', - 'sys-erp', -] +// Generator, helpers, and scope integration +export { + getAnswerValue, + isStepComplete, + getProfilingProgress, + generatePoliciesFromProfile, + prefillFromScopeAnswers, + SCOPE_PREFILLED_LF_QUESTIONS, +} from './loeschfristen-profiling-generator' diff --git a/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis-retention.ts b/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis-retention.ts new file mode 100644 index 0000000..8223653 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis-retention.ts @@ -0,0 +1,215 @@ +/** + * Retention Period Catalog + * + * Standard GDPR/German-law retention periods and helper functions. + * Split from legal-basis.ts for the 500 LOC hard cap. + */ + +import type { LocalizedText } from '../types' + +export interface RetentionPeriodInfo { + id: string + name: LocalizedText + legalBasis: string + duration: { + value: number + unit: 'DAYS' | 'MONTHS' | 'YEARS' + } + description: LocalizedText + applicableTo: string[] +} + +// ========================================== +// RETENTION PERIODS +// ========================================== + +export const STANDARD_RETENTION_PERIODS: RetentionPeriodInfo[] = [ + // Handelsrechtliche Aufbewahrung + { + id: 'hgb-257', + name: { de: 'Handelsbücher und Buchungsbelege', en: 'Commercial Books and Vouchers' }, + legalBasis: '§ 257 HGB', + duration: { value: 10, unit: 'YEARS' }, + description: { + de: 'Handelsbücher, Inventare, Eröffnungsbilanzen, Jahresabschlüsse, Lageberichte, Konzernabschlüsse, Buchungsbelege', + en: 'Commercial books, inventories, opening balance sheets, annual financial statements, management reports, consolidated financial statements, accounting vouchers', + }, + applicableTo: ['Buchhaltung', 'Jahresabschlüsse', 'Rechnungen', 'Verträge'], + }, + { + id: 'hgb-257-6', + name: { de: 'Handels- und Geschäftsbriefe', en: 'Commercial and Business Correspondence' }, + legalBasis: '§ 257 Abs. 1 Nr. 2, 3 HGB', + duration: { value: 6, unit: 'YEARS' }, + description: { + de: 'Empfangene Handels- und Geschäftsbriefe, Wiedergaben der abgesandten Handels- und Geschäftsbriefe', + en: 'Received commercial and business correspondence, copies of sent correspondence', + }, + applicableTo: ['Geschäftskorrespondenz', 'Angebote', 'Auftragsbestätigungen'], + }, + // Steuerrechtliche Aufbewahrung + { + id: 'ao-147', + name: { de: 'Steuerrechtliche Unterlagen', en: 'Tax Documents' }, + legalBasis: '§ 147 AO', + duration: { value: 10, unit: 'YEARS' }, + description: { + de: 'Bücher und Aufzeichnungen, Inventare, Jahresabschlüsse, Buchungsbelege, steuerrelevante Unterlagen', + en: 'Books and records, inventories, annual financial statements, accounting vouchers, tax-relevant documents', + }, + applicableTo: ['Steuererklärungen', 'Buchhaltung', 'Belege'], + }, + // Arbeitsrechtliche Aufbewahrung + { + id: 'arbeitsrecht-personal', + name: { de: 'Personalunterlagen', en: 'Personnel Records' }, + legalBasis: 'Verschiedene (AGG, ArbZG, etc.)', + duration: { value: 3, unit: 'YEARS' }, + description: { + de: 'Personalakte nach Beendigung des Arbeitsverhältnisses (Regelverjährung)', + en: 'Personnel file after termination of employment (standard limitation period)', + }, + applicableTo: ['Personalakten', 'Arbeitsverträge', 'Zeugnisse'], + }, + { + id: 'arbzg', + name: { de: 'Arbeitszeitaufzeichnungen', en: 'Working Time Records' }, + legalBasis: '§ 16 Abs. 2 ArbZG', + duration: { value: 2, unit: 'YEARS' }, + description: { + de: 'Aufzeichnungen über Arbeitszeiten, die über 8 Stunden hinausgehen', + en: 'Records of working hours exceeding 8 hours', + }, + applicableTo: ['Zeiterfassung', 'Überstunden'], + }, + { + id: 'lohnsteuer', + name: { de: 'Lohnunterlagen', en: 'Payroll Documents' }, + legalBasis: '§ 41 EStG, § 28f SGB IV', + duration: { value: 6, unit: 'YEARS' }, + description: { + de: 'Lohnkonten und Unterlagen für den Lohnsteuerabzug', + en: 'Payroll accounts and documents for wage tax deduction', + }, + applicableTo: ['Lohnabrechnungen', 'Lohnsteuerbescheinigungen'], + }, + { + id: 'sozialversicherung', + name: { de: 'Sozialversicherungsunterlagen', en: 'Social Security Documents' }, + legalBasis: '§ 28f SGB IV', + duration: { value: 5, unit: 'YEARS' }, + description: { + de: 'Unterlagen zum Gesamtsozialversicherungsbeitrag', + en: 'Documents for total social security contributions', + }, + applicableTo: ['Sozialversicherungsmeldungen', 'Beitragsnachweise'], + }, + // Bewerberdaten + { + id: 'bewerbung', + name: { de: 'Bewerbungsunterlagen', en: 'Application Documents' }, + legalBasis: '§ 15 Abs. 4 AGG', + duration: { value: 6, unit: 'MONTHS' }, + description: { + de: 'Bewerbungsunterlagen nach Absage (AGG-Frist)', + en: 'Application documents after rejection (AGG deadline)', + }, + applicableTo: ['Bewerbungen', 'Lebensläufe', 'Zeugnisse von Bewerbern'], + }, + // Datenschutzrechtliche Fristen + { + id: 'einwilligung', + name: { de: 'Einwilligungen', en: 'Consents' }, + legalBasis: 'Art. 7 Abs. 1 DSGVO', + duration: { value: 3, unit: 'YEARS' }, + description: { + de: 'Dokumentation der Einwilligung (Regelverjährung)', + en: 'Documentation of consent (standard limitation period)', + }, + applicableTo: ['Einwilligungsnachweise', 'Opt-in-Dokumentation'], + }, + { + id: 'videoüberwachung', + name: { de: 'Videoüberwachung', en: 'Video Surveillance' }, + legalBasis: 'Verhältnismäßigkeit', + duration: { value: 72, unit: 'DAYS' }, + description: { + de: 'Videoaufnahmen (max. 72 Stunden, sofern kein Vorfall)', + en: 'Video recordings (max. 72 hours, unless incident occurred)', + }, + applicableTo: ['CCTV-Aufnahmen', 'Überwachungsvideos'], + }, + // Löschung nach Vertrag + { + id: 'avv-loeschung', + name: { de: 'AVV-Daten nach Vertragsende', en: 'DPA Data after Contract End' }, + legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO', + duration: { value: 30, unit: 'DAYS' }, + description: { + de: 'Löschung oder Rückgabe aller personenbezogenen Daten nach Vertragsende', + en: 'Deletion or return of all personal data after contract end', + }, + applicableTo: ['Auftragsverarbeitung', 'Dienstleister-Daten'], + }, +] + +// ========================================== +// HELPER FUNCTIONS +// ========================================== + +/** + * Get retention period by ID + */ +export function getRetentionPeriod(id: string): RetentionPeriodInfo | undefined { + return STANDARD_RETENTION_PERIODS.find((rp) => rp.id === id) +} + +/** + * Get retention periods applicable to a category + */ +export function getRetentionPeriodsForCategory(category: string): RetentionPeriodInfo[] { + return STANDARD_RETENTION_PERIODS.filter((rp) => + rp.applicableTo.some((a) => a.toLowerCase().includes(category.toLowerCase())) + ) +} + +/** + * Get longest applicable retention period + */ +export function getLongestRetentionPeriod(categories: string[]): RetentionPeriodInfo | undefined { + const applicable = categories.flatMap((cat) => getRetentionPeriodsForCategory(cat)) + + if (applicable.length === 0) return undefined + + return applicable.reduce((longest, current) => { + const longestMonths = toMonths(longest.duration) + const currentMonths = toMonths(current.duration) + return currentMonths > longestMonths ? current : longest + }) +} + +function toMonths(duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }): number { + switch (duration.unit) { + case 'DAYS': + return duration.value / 30 + case 'MONTHS': + return duration.value + case 'YEARS': + return duration.value * 12 + } +} + +/** + * Format retention period for display + */ +export function formatRetentionPeriod( + duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }, + locale: 'de' | 'en' = 'de' +): string { + const units = { + de: { DAYS: 'Tage', MONTHS: 'Monate', YEARS: 'Jahre' }, + en: { DAYS: 'days', MONTHS: 'months', YEARS: 'years' }, + } + + return `${duration.value} ${units[locale][duration.unit]}` +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts b/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts index 0b99e6d..e52ffab 100644 --- a/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts +++ b/admin-compliance/lib/sdk/vendor-compliance/catalog/legal-basis.ts @@ -20,18 +20,6 @@ export interface LegalBasisInfo { notes?: LocalizedText } -export interface RetentionPeriodInfo { - id: string - name: LocalizedText - legalBasis: string - duration: { - value: number - unit: 'DAYS' | 'MONTHS' | 'YEARS' - } - description: LocalizedText - applicableTo: string[] -} - // ========================================== // LEGAL BASIS INFORMATION (Art. 6 DSGVO) // ========================================== @@ -319,140 +307,6 @@ export const LEGAL_BASIS_INFO: LegalBasisInfo[] = [ }, ] -// ========================================== -// RETENTION PERIODS -// ========================================== - -export const STANDARD_RETENTION_PERIODS: RetentionPeriodInfo[] = [ - // Handelsrechtliche Aufbewahrung - { - id: 'hgb-257', - name: { de: 'Handelsbücher und Buchungsbelege', en: 'Commercial Books and Vouchers' }, - legalBasis: '§ 257 HGB', - duration: { value: 10, unit: 'YEARS' }, - description: { - de: 'Handelsbücher, Inventare, Eröffnungsbilanzen, Jahresabschlüsse, Lageberichte, Konzernabschlüsse, Buchungsbelege', - en: 'Commercial books, inventories, opening balance sheets, annual financial statements, management reports, consolidated financial statements, accounting vouchers', - }, - applicableTo: ['Buchhaltung', 'Jahresabschlüsse', 'Rechnungen', 'Verträge'], - }, - { - id: 'hgb-257-6', - name: { de: 'Handels- und Geschäftsbriefe', en: 'Commercial and Business Correspondence' }, - legalBasis: '§ 257 Abs. 1 Nr. 2, 3 HGB', - duration: { value: 6, unit: 'YEARS' }, - description: { - de: 'Empfangene Handels- und Geschäftsbriefe, Wiedergaben der abgesandten Handels- und Geschäftsbriefe', - en: 'Received commercial and business correspondence, copies of sent correspondence', - }, - applicableTo: ['Geschäftskorrespondenz', 'Angebote', 'Auftragsbestätigungen'], - }, - // Steuerrechtliche Aufbewahrung - { - id: 'ao-147', - name: { de: 'Steuerrechtliche Unterlagen', en: 'Tax Documents' }, - legalBasis: '§ 147 AO', - duration: { value: 10, unit: 'YEARS' }, - description: { - de: 'Bücher und Aufzeichnungen, Inventare, Jahresabschlüsse, Buchungsbelege, steuerrelevante Unterlagen', - en: 'Books and records, inventories, annual financial statements, accounting vouchers, tax-relevant documents', - }, - applicableTo: ['Steuererklärungen', 'Buchhaltung', 'Belege'], - }, - // Arbeitsrechtliche Aufbewahrung - { - id: 'arbeitsrecht-personal', - name: { de: 'Personalunterlagen', en: 'Personnel Records' }, - legalBasis: 'Verschiedene (AGG, ArbZG, etc.)', - duration: { value: 3, unit: 'YEARS' }, - description: { - de: 'Personalakte nach Beendigung des Arbeitsverhältnisses (Regelverjährung)', - en: 'Personnel file after termination of employment (standard limitation period)', - }, - applicableTo: ['Personalakten', 'Arbeitsverträge', 'Zeugnisse'], - }, - { - id: 'arbzg', - name: { de: 'Arbeitszeitaufzeichnungen', en: 'Working Time Records' }, - legalBasis: '§ 16 Abs. 2 ArbZG', - duration: { value: 2, unit: 'YEARS' }, - description: { - de: 'Aufzeichnungen über Arbeitszeiten, die über 8 Stunden hinausgehen', - en: 'Records of working hours exceeding 8 hours', - }, - applicableTo: ['Zeiterfassung', 'Überstunden'], - }, - { - id: 'lohnsteuer', - name: { de: 'Lohnunterlagen', en: 'Payroll Documents' }, - legalBasis: '§ 41 EStG, § 28f SGB IV', - duration: { value: 6, unit: 'YEARS' }, - description: { - de: 'Lohnkonten und Unterlagen für den Lohnsteuerabzug', - en: 'Payroll accounts and documents for wage tax deduction', - }, - applicableTo: ['Lohnabrechnungen', 'Lohnsteuerbescheinigungen'], - }, - { - id: 'sozialversicherung', - name: { de: 'Sozialversicherungsunterlagen', en: 'Social Security Documents' }, - legalBasis: '§ 28f SGB IV', - duration: { value: 5, unit: 'YEARS' }, - description: { - de: 'Unterlagen zum Gesamtsozialversicherungsbeitrag', - en: 'Documents for total social security contributions', - }, - applicableTo: ['Sozialversicherungsmeldungen', 'Beitragsnachweise'], - }, - // Bewerberdaten - { - id: 'bewerbung', - name: { de: 'Bewerbungsunterlagen', en: 'Application Documents' }, - legalBasis: '§ 15 Abs. 4 AGG', - duration: { value: 6, unit: 'MONTHS' }, - description: { - de: 'Bewerbungsunterlagen nach Absage (AGG-Frist)', - en: 'Application documents after rejection (AGG deadline)', - }, - applicableTo: ['Bewerbungen', 'Lebensläufe', 'Zeugnisse von Bewerbern'], - }, - // Datenschutzrechtliche Fristen - { - id: 'einwilligung', - name: { de: 'Einwilligungen', en: 'Consents' }, - legalBasis: 'Art. 7 Abs. 1 DSGVO', - duration: { value: 3, unit: 'YEARS' }, - description: { - de: 'Dokumentation der Einwilligung (Regelverjährung)', - en: 'Documentation of consent (standard limitation period)', - }, - applicableTo: ['Einwilligungsnachweise', 'Opt-in-Dokumentation'], - }, - { - id: 'videoüberwachung', - name: { de: 'Videoüberwachung', en: 'Video Surveillance' }, - legalBasis: 'Verhältnismäßigkeit', - duration: { value: 72, unit: 'DAYS' }, - description: { - de: 'Videoaufnahmen (max. 72 Stunden, sofern kein Vorfall)', - en: 'Video recordings (max. 72 hours, unless incident occurred)', - }, - applicableTo: ['CCTV-Aufnahmen', 'Überwachungsvideos'], - }, - // Löschung nach Vertrag - { - id: 'avv-loeschung', - name: { de: 'AVV-Daten nach Vertragsende', en: 'DPA Data after Contract End' }, - legalBasis: 'Art. 28 Abs. 3 lit. g DSGVO', - duration: { value: 30, unit: 'DAYS' }, - description: { - de: 'Löschung oder Rückgabe aller personenbezogenen Daten nach Vertragsende', - en: 'Deletion or return of all personal data after contract end', - }, - applicableTo: ['Auftragsverarbeitung', 'Dienstleister-Daten'], - }, -] - // ========================================== // HELPER FUNCTIONS // ========================================== @@ -492,7 +346,6 @@ export function getAppropriateLegalBases( ) if (hasSpecialCategory) { - // Return Art. 9 bases plus compatible Art. 6 bases return [ ...getSpecialCategoryLegalBases(), ...getStandardLegalBases().filter((lb) => @@ -504,59 +357,12 @@ export function getAppropriateLegalBases( return getStandardLegalBases() } -/** - * Get retention period by ID - */ -export function getRetentionPeriod(id: string): RetentionPeriodInfo | undefined { - return STANDARD_RETENTION_PERIODS.find((rp) => rp.id === id) -} - -/** - * Get retention periods applicable to a category - */ -export function getRetentionPeriodsForCategory(category: string): RetentionPeriodInfo[] { - return STANDARD_RETENTION_PERIODS.filter((rp) => - rp.applicableTo.some((a) => a.toLowerCase().includes(category.toLowerCase())) - ) -} - -/** - * Get longest applicable retention period - */ -export function getLongestRetentionPeriod(categories: string[]): RetentionPeriodInfo | undefined { - const applicable = categories.flatMap((cat) => getRetentionPeriodsForCategory(cat)) - - if (applicable.length === 0) return undefined - - return applicable.reduce((longest, current) => { - const longestMonths = toMonths(longest.duration) - const currentMonths = toMonths(current.duration) - return currentMonths > longestMonths ? current : longest - }) -} - -function toMonths(duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }): number { - switch (duration.unit) { - case 'DAYS': - return duration.value / 30 - case 'MONTHS': - return duration.value - case 'YEARS': - return duration.value * 12 - } -} - -/** - * Format retention period for display - */ -export function formatRetentionPeriod( - duration: { value: number; unit: 'DAYS' | 'MONTHS' | 'YEARS' }, - locale: 'de' | 'en' = 'de' -): string { - const units = { - de: { DAYS: 'Tage', MONTHS: 'Monate', YEARS: 'Jahre' }, - en: { DAYS: 'days', MONTHS: 'months', YEARS: 'years' }, - } - - return `${duration.value} ${units[locale][duration.unit]}` -} +// Re-export retention periods and functions for backward compatibility +export type { RetentionPeriodInfo } from './legal-basis-retention' +export { + STANDARD_RETENTION_PERIODS, + getRetentionPeriod, + getRetentionPeriodsForCategory, + getLongestRetentionPeriod, + formatRetentionPeriod, +} from './legal-basis-retention' diff --git a/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-country-profiles.ts b/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-country-profiles.ts new file mode 100644 index 0000000..b90e2c2 --- /dev/null +++ b/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-country-profiles.ts @@ -0,0 +1,127 @@ +/** + * Vendor Country Risk Profiles + * + * GDPR transfer risk profiles per country and helper functions. + * Split from vendor-templates.ts for the 500 LOC hard cap. + */ + +import type { TransferMechanismType } from '../types' +import type { CountryRiskProfile } from './vendor-templates' + +export type { CountryRiskProfile } + +// ========================================== +// COUNTRY RISK PROFILES +// ========================================== + +export const COUNTRY_RISK_PROFILES: CountryRiskProfile[] = [ + // EU Countries (Low Risk) + { code: 'DE', name: { de: 'Deutschland', en: 'Germany' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'AT', name: { de: 'Österreich', en: 'Austria' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'FR', name: { de: 'Frankreich', en: 'France' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'NL', name: { de: 'Niederlande', en: 'Netherlands' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'BE', name: { de: 'Belgien', en: 'Belgium' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'IT', name: { de: 'Italien', en: 'Italy' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'ES', name: { de: 'Spanien', en: 'Spain' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'PT', name: { de: 'Portugal', en: 'Portugal' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'PL', name: { de: 'Polen', en: 'Poland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'CZ', name: { de: 'Tschechien', en: 'Czech Republic' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'SE', name: { de: 'Schweden', en: 'Sweden' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'DK', name: { de: 'Dänemark', en: 'Denmark' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'FI', name: { de: 'Finnland', en: 'Finland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'IE', name: { de: 'Irland', en: 'Ireland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'LU', name: { de: 'Luxemburg', en: 'Luxembourg' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + + // EEA Countries + { code: 'NO', name: { de: 'Norwegen', en: 'Norway' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'IS', name: { de: 'Island', en: 'Iceland' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'LI', name: { de: 'Liechtenstein', en: 'Liechtenstein' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, + + // Adequacy Decision Countries + { code: 'CH', name: { de: 'Schweiz', en: 'Switzerland' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'GB', name: { de: 'Vereinigtes Königreich', en: 'United Kingdom' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2021-06-28', riskLevel: 'LOW' }, + { code: 'JP', name: { de: 'Japan', en: 'Japan' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2019-01-23', riskLevel: 'LOW' }, + { code: 'KR', name: { de: 'Südkorea', en: 'South Korea' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2022-12-17', riskLevel: 'LOW' }, + { code: 'IL', name: { de: 'Israel', en: 'Israel' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'NZ', name: { de: 'Neuseeland', en: 'New Zealand' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'CA', name: { de: 'Kanada', en: 'Canada' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW', notes: { de: 'Nur PIPEDA-Bereich', en: 'PIPEDA scope only' } }, + { code: 'AR', name: { de: 'Argentinien', en: 'Argentina' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, + { code: 'UY', name: { de: 'Uruguay', en: 'Uruguay' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, + + // US (Special - DPF) + { code: 'US', name: { de: 'USA', en: 'United States' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2023-07-10', riskLevel: 'MEDIUM', notes: { de: 'EU-US Data Privacy Framework erforderlich', en: 'EU-US Data Privacy Framework required' } }, + + // Third Countries without Adequacy (High Risk) + { code: 'CN', name: { de: 'China', en: 'China' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Staatlicher Datenzugriff möglich', en: 'Government data access possible' } }, + { code: 'RU', name: { de: 'Russland', en: 'Russia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Sanktionen beachten', en: 'Consider sanctions' } }, + { code: 'IN', name: { de: 'Indien', en: 'India' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, + { code: 'BR', name: { de: 'Brasilien', en: 'Brazil' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'LGPD vorhanden', en: 'LGPD in place' } }, + { code: 'AU', name: { de: 'Australien', en: 'Australia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM' }, + { code: 'SG', name: { de: 'Singapur', en: 'Singapore' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'PDPA vorhanden', en: 'PDPA in place' } }, + { code: 'HK', name: { de: 'Hongkong', en: 'Hong Kong' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, + { code: 'AE', name: { de: 'VAE', en: 'UAE' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, + { code: 'ZA', name: { de: 'Südafrika', en: 'South Africa' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'POPIA vorhanden', en: 'POPIA in place' } }, +] + +// ========================================== +// HELPER FUNCTIONS +// ========================================== + +/** + * Get country risk profile + */ +export function getCountryRiskProfile(countryCode: string): CountryRiskProfile | undefined { + return COUNTRY_RISK_PROFILES.find((c) => c.code === countryCode.toUpperCase()) +} + +/** + * Check if country requires transfer mechanism + */ +export function requiresTransferMechanism(countryCode: string): boolean { + const profile = getCountryRiskProfile(countryCode) + if (!profile) return true // Unknown country = requires mechanism + return !profile.isEU && !profile.isEEA && !profile.hasAdequacyDecision +} + +/** + * Get suggested transfer mechanisms for country + */ +export function getSuggestedTransferMechanisms(countryCode: string): TransferMechanismType[] { + const profile = getCountryRiskProfile(countryCode) + + if (!profile) { + return ['SCC_PROCESSOR'] + } + + if (profile.isEU || profile.isEEA) { + return [] // No mechanism needed + } + + if (profile.hasAdequacyDecision) { + return ['ADEQUACY_DECISION'] + } + + // Third country without adequacy + return ['SCC_PROCESSOR', 'BCR'] +} + +/** + * Get all EU/EEA countries + */ +export function getEUEEACountries(): CountryRiskProfile[] { + return COUNTRY_RISK_PROFILES.filter((c) => c.isEU || c.isEEA) +} + +/** + * Get all countries with adequacy decision + */ +export function getAdequateCountries(): CountryRiskProfile[] { + return COUNTRY_RISK_PROFILES.filter((c) => c.hasAdequacyDecision) +} + +/** + * Get all high-risk countries + */ +export function getHighRiskCountries(): CountryRiskProfile[] { + return COUNTRY_RISK_PROFILES.filter((c) => c.riskLevel === 'HIGH' || c.riskLevel === 'VERY_HIGH') +} diff --git a/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts b/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts index 1a1b4fa..947aea7 100644 --- a/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts +++ b/admin-compliance/lib/sdk/vendor-compliance/catalog/vendor-templates.ts @@ -402,60 +402,7 @@ export const VENDOR_TEMPLATES: VendorTemplate[] = [ ] // ========================================== -// COUNTRY RISK PROFILES -// ========================================== - -export const COUNTRY_RISK_PROFILES: CountryRiskProfile[] = [ - // EU Countries (Low Risk) - { code: 'DE', name: { de: 'Deutschland', en: 'Germany' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'AT', name: { de: 'Österreich', en: 'Austria' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'FR', name: { de: 'Frankreich', en: 'France' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'NL', name: { de: 'Niederlande', en: 'Netherlands' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'BE', name: { de: 'Belgien', en: 'Belgium' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'IT', name: { de: 'Italien', en: 'Italy' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'ES', name: { de: 'Spanien', en: 'Spain' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'PT', name: { de: 'Portugal', en: 'Portugal' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'PL', name: { de: 'Polen', en: 'Poland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'CZ', name: { de: 'Tschechien', en: 'Czech Republic' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'SE', name: { de: 'Schweden', en: 'Sweden' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'DK', name: { de: 'Dänemark', en: 'Denmark' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'FI', name: { de: 'Finnland', en: 'Finland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'IE', name: { de: 'Irland', en: 'Ireland' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'LU', name: { de: 'Luxemburg', en: 'Luxembourg' }, isEU: true, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - - // EEA Countries - { code: 'NO', name: { de: 'Norwegen', en: 'Norway' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'IS', name: { de: 'Island', en: 'Iceland' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'LI', name: { de: 'Liechtenstein', en: 'Liechtenstein' }, isEU: false, isEEA: true, hasAdequacyDecision: true, riskLevel: 'LOW' }, - - // Adequacy Decision Countries - { code: 'CH', name: { de: 'Schweiz', en: 'Switzerland' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'GB', name: { de: 'Vereinigtes Königreich', en: 'United Kingdom' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2021-06-28', riskLevel: 'LOW' }, - { code: 'JP', name: { de: 'Japan', en: 'Japan' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2019-01-23', riskLevel: 'LOW' }, - { code: 'KR', name: { de: 'Südkorea', en: 'South Korea' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2022-12-17', riskLevel: 'LOW' }, - { code: 'IL', name: { de: 'Israel', en: 'Israel' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'NZ', name: { de: 'Neuseeland', en: 'New Zealand' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'CA', name: { de: 'Kanada', en: 'Canada' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW', notes: { de: 'Nur PIPEDA-Bereich', en: 'PIPEDA scope only' } }, - { code: 'AR', name: { de: 'Argentinien', en: 'Argentina' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, - { code: 'UY', name: { de: 'Uruguay', en: 'Uruguay' }, isEU: false, isEEA: false, hasAdequacyDecision: true, riskLevel: 'LOW' }, - - // US (Special - DPF) - { code: 'US', name: { de: 'USA', en: 'United States' }, isEU: false, isEEA: false, hasAdequacyDecision: true, adequacyDecisionDate: '2023-07-10', riskLevel: 'MEDIUM', notes: { de: 'EU-US Data Privacy Framework erforderlich', en: 'EU-US Data Privacy Framework required' } }, - - // Third Countries without Adequacy (High Risk) - { code: 'CN', name: { de: 'China', en: 'China' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Staatlicher Datenzugriff möglich', en: 'Government data access possible' } }, - { code: 'RU', name: { de: 'Russland', en: 'Russia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'VERY_HIGH', notes: { de: 'Sanktionen beachten', en: 'Consider sanctions' } }, - { code: 'IN', name: { de: 'Indien', en: 'India' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, - { code: 'BR', name: { de: 'Brasilien', en: 'Brazil' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'LGPD vorhanden', en: 'LGPD in place' } }, - { code: 'AU', name: { de: 'Australien', en: 'Australia' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM' }, - { code: 'SG', name: { de: 'Singapur', en: 'Singapore' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'PDPA vorhanden', en: 'PDPA in place' } }, - { code: 'HK', name: { de: 'Hongkong', en: 'Hong Kong' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, - { code: 'AE', name: { de: 'VAE', en: 'UAE' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'HIGH' }, - { code: 'ZA', name: { de: 'Südafrika', en: 'South Africa' }, isEU: false, isEEA: false, hasAdequacyDecision: false, riskLevel: 'MEDIUM', notes: { de: 'POPIA vorhanden', en: 'POPIA in place' } }, -] - -// ========================================== -// HELPER FUNCTIONS +// HELPER FUNCTIONS (template-specific) // ========================================== /** @@ -472,44 +419,6 @@ export function getVendorTemplatesByCategory(category: ServiceCategory): VendorT return VENDOR_TEMPLATES.filter((t) => t.serviceCategory === category) } -/** - * Get country risk profile - */ -export function getCountryRiskProfile(countryCode: string): CountryRiskProfile | undefined { - return COUNTRY_RISK_PROFILES.find((c) => c.code === countryCode.toUpperCase()) -} - -/** - * Check if country requires transfer mechanism - */ -export function requiresTransferMechanism(countryCode: string): boolean { - const profile = getCountryRiskProfile(countryCode) - if (!profile) return true // Unknown country = requires mechanism - return !profile.isEU && !profile.isEEA && !profile.hasAdequacyDecision -} - -/** - * Get suggested transfer mechanisms for country - */ -export function getSuggestedTransferMechanisms(countryCode: string): TransferMechanismType[] { - const profile = getCountryRiskProfile(countryCode) - - if (!profile) { - return ['SCC_PROCESSOR'] - } - - if (profile.isEU || profile.isEEA) { - return [] // No mechanism needed - } - - if (profile.hasAdequacyDecision) { - return ['ADEQUACY_DECISION'] - } - - // Third country without adequacy - return ['SCC_PROCESSOR', 'BCR'] -} - /** * Calculate inherent risk score for vendor template */ @@ -542,23 +451,13 @@ export function createVendorFormDataFromTemplate( } } -/** - * Get all EU/EEA countries - */ -export function getEUEEACountries(): CountryRiskProfile[] { - return COUNTRY_RISK_PROFILES.filter((c) => c.isEU || c.isEEA) -} - -/** - * Get all countries with adequacy decision - */ -export function getAdequateCountries(): CountryRiskProfile[] { - return COUNTRY_RISK_PROFILES.filter((c) => c.hasAdequacyDecision) -} - -/** - * Get all high-risk countries - */ -export function getHighRiskCountries(): CountryRiskProfile[] { - return COUNTRY_RISK_PROFILES.filter((c) => c.riskLevel === 'HIGH' || c.riskLevel === 'VERY_HIGH') -} +// Re-export country profiles and functions for backward compatibility +export { + COUNTRY_RISK_PROFILES, + getCountryRiskProfile, + requiresTransferMechanism, + getSuggestedTransferMechanisms, + getEUEEACountries, + getAdequateCountries, + getHighRiskCountries, +} from './vendor-country-profiles'