refactor(admin-compliance): split 11 oversized files under 500 LOC hard cap (batch 2)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, Array<{ blockId: string; blockType: ProseBlockOutput['blockType']; sectionName: string; targetWords: number }>> = {
|
||||
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<string> {
|
||||
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<string, unknown>): Promise<NextResponse> {
|
||||
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<string, unknown>, 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 })
|
||||
}
|
||||
@@ -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<string, unknown>): Promise<NextResponse> {
|
||||
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<string, unknown>, 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'
|
||||
@@ -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<string, unknown>): Promise<NextResponse> {
|
||||
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<string, unknown>, 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<string, Array<{ blockId: string; blockType: ProseBlockOutput['blockType']; sectionName: string; targetWords: number }>> = {
|
||||
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<string> {
|
||||
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<string, unknown>): Promise<NextResponse> {
|
||||
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<string, unknown>, 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
|
||||
|
||||
@@ -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<string, string> of placeholder values.
|
||||
* Keys are the {{PLACEHOLDER}} strings used in templates.
|
||||
*/
|
||||
export function contextToPlaceholders(ctx: TemplateContext): Record<string, string> {
|
||||
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<keyof TemplateContext, string[]> = {
|
||||
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<string, unknown>),
|
||||
[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<string, unknown>
|
||||
return sectionObj?.[rest.join('.')]
|
||||
}
|
||||
@@ -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<string, string> of placeholder values.
|
||||
* Keys are the {{PLACEHOLDER}} strings used in templates.
|
||||
*/
|
||||
export function contextToPlaceholders(ctx: TemplateContext): Record<string, string> {
|
||||
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<keyof TemplateContext, string[]> = {
|
||||
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<string, unknown>),
|
||||
[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<string, unknown>
|
||||
return sectionObj?.[rest.join('.')]
|
||||
}
|
||||
export {
|
||||
contextToPlaceholders,
|
||||
computeFlags,
|
||||
getRelevantSections,
|
||||
getUncoveredPlaceholders,
|
||||
getMissingRequired,
|
||||
setContextPath,
|
||||
getContextPath,
|
||||
} from './contextBridge-helpers'
|
||||
|
||||
@@ -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<Record<ScopeDocumentType, DocumentScopeRequirement>> = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<Record<ScopeDocumentType, Docum
|
||||
estimatedEffort: '10-16 Stunden',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
...DOCUMENT_SCOPE_MATRIX_CORE_PART2,
|
||||
}
|
||||
|
||||
export { DOCUMENT_SCOPE_MATRIX_CORE_PART2 } from './document-scope-matrix-core-part2'
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* Compliance Scope Engine - Document Scope Matrix Extended (Part 2)
|
||||
*
|
||||
* Continued from document-scope-matrix-extended.ts for the 500 LOC hard cap.
|
||||
* Contains: iace_ce_assessment, widerrufsbelehrung, preisangaben, fernabsatz_info,
|
||||
* streitbeilegung, produktsicherheit, ai_act_doku
|
||||
*/
|
||||
|
||||
import type { ScopeDocumentType } from './documents'
|
||||
import type { DocumentScopeRequirement } from './documents'
|
||||
|
||||
export const DOCUMENT_SCOPE_MATRIX_EXTENDED_PART2: Partial<Record<ScopeDocumentType, DocumentScopeRequirement>> = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<Record<ScopeDocumentType, D
|
||||
estimatedEffort: '24-40 Stunden',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
...DOCUMENT_SCOPE_MATRIX_EXTENDED_PART2,
|
||||
}
|
||||
|
||||
export { DOCUMENT_SCOPE_MATRIX_EXTENDED_PART2 } from './document-scope-matrix-extended-part2'
|
||||
|
||||
322
admin-compliance/lib/sdk/dsfa/eu-legal-frameworks-national.ts
Normal file
322
admin-compliance/lib/sdk/dsfa/eu-legal-frameworks-national.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* EU/EWR Rechtsgrundlagen — Nationale Ergaenzungsgesetze + Helpers
|
||||
*
|
||||
* Split from eu-legal-frameworks.ts for the 500 LOC hard cap.
|
||||
*/
|
||||
|
||||
import type { CountryCode, LegalDocumentType, LicenseType, LegalFramework, SupervisoryAuthority, DocumentTypeMatrix, RAGLayer, DocumentUniformity } from './eu-legal-frameworks'
|
||||
|
||||
export type { CountryCode, LegalDocumentType, LicenseType, DocumentUniformity }
|
||||
|
||||
// =============================================================================
|
||||
// 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,
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// Helper Functions
|
||||
// =============================================================================
|
||||
|
||||
export function getAllSupervisoryAuthorities(
|
||||
allFrameworks: LegalFramework[]
|
||||
): SupervisoryAuthority[] {
|
||||
const authorities: SupervisoryAuthority[] = []
|
||||
for (const fw of allFrameworks) {
|
||||
if (fw.supervisoryAuthorities) {
|
||||
for (const sa of fw.supervisoryAuthorities) {
|
||||
if (!authorities.some(a => 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 }
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
290
admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts
Normal file
290
admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts
Normal file
@@ -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<string>()
|
||||
|
||||
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',
|
||||
]
|
||||
@@ -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<string>()
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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'
|
||||
|
||||
@@ -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]}`
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user