feat(sdk): VVT master libraries, process templates, Loeschfristen profiling + document

VVT: Master library tables (7 catalogs), 500+ seed entries, process templates
with instantiation, library API endpoints + 18 tests.
Loeschfristen: Baseline catalog, compliance checks, profiling engine, HTML document
generator, MkDocs documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-19 11:56:25 +01:00
parent f2819b99af
commit 2a70441eaa
20 changed files with 6621 additions and 9 deletions

View File

@@ -27,12 +27,18 @@ import {
exportPoliciesAsJSON, exportPoliciesAsCSV,
generateComplianceSummary, downloadFile,
} from '@/lib/sdk/loeschfristen-export'
import {
buildLoeschkonzeptHtml,
type LoeschkonzeptOrgHeader,
type LoeschkonzeptRevision,
createDefaultLoeschkonzeptOrgHeader,
} from '@/lib/sdk/loeschfristen-document'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type Tab = 'uebersicht' | 'editor' | 'generator' | 'export'
type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' | 'loeschkonzept'
// ---------------------------------------------------------------------------
// Helper: TagInput
@@ -130,6 +136,10 @@ export default function LoeschfristenPage() {
// ---- VVT data ----
const [vvtActivities, setVvtActivities] = useState<any[]>([])
// ---- Loeschkonzept document state ----
const [orgHeader, setOrgHeader] = useState<LoeschkonzeptOrgHeader>(createDefaultLoeschkonzeptOrgHeader())
const [revisions, setRevisions] = useState<LoeschkonzeptRevision[]>([])
// --------------------------------------------------------------------------
// Persistence (API-backed)
// --------------------------------------------------------------------------
@@ -247,6 +257,48 @@ export default function LoeschfristenPage() {
})
}, [tab, editingId])
// Load Loeschkonzept org header from VVT organization data + revisions from localStorage
useEffect(() => {
// Load revisions from localStorage
try {
const raw = localStorage.getItem('bp_loeschkonzept_revisions')
if (raw) {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) setRevisions(parsed)
}
} catch { /* ignore */ }
// Load org header from localStorage (user overrides)
try {
const raw = localStorage.getItem('bp_loeschkonzept_orgheader')
if (raw) {
const parsed = JSON.parse(raw)
if (parsed && typeof parsed === 'object') {
setOrgHeader(prev => ({ ...prev, ...parsed }))
return // User has saved org header, skip VVT fetch
}
}
} catch { /* ignore */ }
// Fallback: fetch from VVT organization API
fetch('/api/sdk/v1/compliance/vvt/organization')
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data) {
setOrgHeader(prev => ({
...prev,
organizationName: data.organization_name || data.organizationName || prev.organizationName,
industry: data.industry || prev.industry,
dpoName: data.dpo_name || data.dpoName || prev.dpoName,
dpoContact: data.dpo_contact || data.dpoContact || prev.dpoContact,
responsiblePerson: data.responsible_person || data.responsiblePerson || prev.responsiblePerson,
employeeCount: data.employee_count || data.employeeCount || prev.employeeCount,
}))
}
})
.catch(() => { /* ignore */ })
}, [])
// --------------------------------------------------------------------------
// Derived
// --------------------------------------------------------------------------
@@ -489,6 +541,7 @@ export default function LoeschfristenPage() {
{ key: 'editor', label: 'Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'export', label: 'Export & Compliance' },
{ key: 'loeschkonzept', label: 'Loeschkonzept' },
]
// --------------------------------------------------------------------------
@@ -2278,6 +2331,314 @@ export default function LoeschfristenPage() {
)
}
// ==========================================================================
// Tab 5: Loeschkonzept Document
// ==========================================================================
function handleOrgHeaderChange(field: keyof LoeschkonzeptOrgHeader, value: string | string[]) {
const updated = { ...orgHeader, [field]: value }
setOrgHeader(updated)
localStorage.setItem('bp_loeschkonzept_orgheader', JSON.stringify(updated))
}
function handleAddRevision() {
const newRev: LoeschkonzeptRevision = {
version: orgHeader.loeschkonzeptVersion,
date: new Date().toISOString().split('T')[0],
author: orgHeader.dpoName || orgHeader.responsiblePerson || '',
changes: '',
}
const updated = [...revisions, newRev]
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handleUpdateRevision(index: number, field: keyof LoeschkonzeptRevision, value: string) {
const updated = revisions.map((r, i) => i === index ? { ...r, [field]: value } : r)
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handleRemoveRevision(index: number) {
const updated = revisions.filter((_, i) => i !== index)
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
}
function handlePrintLoeschkonzept() {
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
const printWindow = window.open('', '_blank')
if (printWindow) {
printWindow.document.write(htmlContent)
printWindow.document.close()
printWindow.focus()
setTimeout(() => printWindow.print(), 300)
}
}
function handleDownloadLoeschkonzeptHtml() {
const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions)
downloadFile(htmlContent, `loeschkonzept-${new Date().toISOString().split('T')[0]}.html`, 'text/html;charset=utf-8')
}
function renderLoeschkonzept() {
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
return (
<div className="space-y-4">
{/* Action bar */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">
Loeschkonzept (Art. 5/17/30 DSGVO)
</h3>
<p className="text-sm text-gray-500 mt-0.5">
Druckfertiges Loeschkonzept mit Deckblatt, Loeschregeln, VVT-Verknuepfung und Compliance-Status.
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleDownloadLoeschkonzeptHtml}
disabled={activePolicies.length === 0}
className="bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
HTML herunterladen
</button>
<button
onClick={handlePrintLoeschkonzept}
disabled={activePolicies.length === 0}
className="bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" /></svg>
Als PDF drucken
</button>
</div>
</div>
{activePolicies.length === 0 && (
<div className="bg-yellow-50 text-yellow-700 text-sm rounded-lg p-3 border border-yellow-200">
Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren.
</div>
)}
</div>
{/* Org Header Form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-4">Organisationsdaten (Deckblatt)</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Organisation</label>
<input
type="text"
value={orgHeader.organizationName}
onChange={e => handleOrgHeaderChange('organizationName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name der Organisation"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Branche</label>
<input
type="text"
value={orgHeader.industry}
onChange={e => handleOrgHeaderChange('industry', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z.B. IT / Software"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Datenschutzbeauftragter</label>
<input
type="text"
value={orgHeader.dpoName}
onChange={e => handleOrgHeaderChange('dpoName', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name des DSB"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">DSB-Kontakt</label>
<input
type="text"
value={orgHeader.dpoContact}
onChange={e => handleOrgHeaderChange('dpoContact', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="E-Mail oder Telefon"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Verantwortlicher (Art. 4 Nr. 7)</label>
<input
type="text"
value={orgHeader.responsiblePerson}
onChange={e => handleOrgHeaderChange('responsiblePerson', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="Name des Verantwortlichen"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Mitarbeiter</label>
<input
type="text"
value={orgHeader.employeeCount}
onChange={e => handleOrgHeaderChange('employeeCount', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z.B. 50-249"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Version</label>
<input
type="text"
value={orgHeader.loeschkonzeptVersion}
onChange={e => handleOrgHeaderChange('loeschkonzeptVersion', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="1.0"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Pruefintervall</label>
<select
value={orgHeader.reviewInterval}
onChange={e => handleOrgHeaderChange('reviewInterval', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="Vierteljaehrlich">Vierteljaehrlich</option>
<option value="Halbjaehrlich">Halbjaehrlich</option>
<option value="Jaehrlich">Jaehrlich</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Letzte Pruefung</label>
<input
type="date"
value={orgHeader.lastReviewDate}
onChange={e => handleOrgHeaderChange('lastReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Naechste Pruefung</label>
<input
type="date"
value={orgHeader.nextReviewDate}
onChange={e => handleOrgHeaderChange('nextReviewDate', e.target.value)}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
</div>
{/* Revisions */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-sm font-semibold text-gray-900">Aenderungshistorie</h4>
<button
onClick={handleAddRevision}
className="text-xs bg-purple-50 text-purple-700 hover:bg-purple-100 rounded-lg px-3 py-1.5 font-medium transition"
>
+ Revision hinzufuegen
</button>
</div>
{revisions.length === 0 ? (
<p className="text-sm text-gray-400">
Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt.
</p>
) : (
<div className="space-y-3">
{revisions.map((rev, idx) => (
<div key={idx} className="grid grid-cols-[80px_120px_1fr_1fr_32px] gap-2 items-start">
<input
type="text"
value={rev.version}
onChange={e => handleUpdateRevision(idx, 'version', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="1.1"
/>
<input
type="date"
value={rev.date}
onChange={e => handleUpdateRevision(idx, 'date', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
/>
<input
type="text"
value={rev.author}
onChange={e => handleUpdateRevision(idx, 'author', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="Autor"
/>
<input
type="text"
value={rev.changes}
onChange={e => handleUpdateRevision(idx, 'changes', e.target.value)}
className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs"
placeholder="Beschreibung der Aenderungen"
/>
<button
onClick={() => handleRemoveRevision(idx)}
className="text-red-400 hover:text-red-600 p-1"
title="Revision entfernen"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
))}
</div>
)}
</div>
{/* Document Preview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h4 className="text-sm font-semibold text-gray-900 mb-4">Dokument-Vorschau</h4>
<div className="bg-gray-50 rounded-lg p-6 border border-gray-200">
{/* Cover preview */}
<div className="text-center mb-6">
<div className="text-2xl font-bold text-purple-700 mb-1">Loeschkonzept</div>
<div className="text-sm text-purple-500 mb-4">gemaess Art. 5/17/30 DSGVO</div>
<div className="text-sm text-gray-600">
{orgHeader.organizationName || <span className="text-gray-400 italic">Organisation nicht angegeben</span>}
</div>
<div className="text-xs text-gray-400 mt-2">
Version {orgHeader.loeschkonzeptVersion} | {new Date().toLocaleDateString('de-DE')}
</div>
</div>
{/* Section list */}
<div className="border-t border-gray-200 pt-4">
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">11 Sektionen</div>
<div className="grid grid-cols-2 gap-1 text-xs text-gray-600">
<div>1. Ziel und Zweck</div>
<div>7. Legal Hold Verfahren</div>
<div>2. Geltungsbereich</div>
<div>8. Verantwortlichkeiten</div>
<div>3. Grundprinzipien</div>
<div>9. Pruef-/Revisionszyklus</div>
<div>4. Loeschregeln-Uebersicht</div>
<div>10. Compliance-Status</div>
<div>5. Detaillierte Loeschregeln</div>
<div>11. Aenderungshistorie</div>
<div>6. VVT-Verknuepfung</div>
</div>
</div>
{/* Stats */}
<div className="border-t border-gray-200 pt-4 mt-4 flex gap-6 text-xs text-gray-500">
<span><strong className="text-gray-700">{activePolicies.length}</strong> Loeschregeln</span>
<span><strong className="text-gray-700">{policies.filter(p => p.linkedVVTActivityIds.length > 0).length}</strong> VVT-Verknuepfungen</span>
<span><strong className="text-gray-700">{revisions.length}</strong> Revisionen</span>
{complianceResult && (
<span>Compliance-Score: <strong className={complianceResult.score >= 75 ? 'text-green-600' : complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'}>{complianceResult.score}/100</strong></span>
)}
</div>
</div>
</div>
</div>
)
}
// ==========================================================================
// Main render
// ==========================================================================
@@ -2317,6 +2678,7 @@ export default function LoeschfristenPage() {
{tab === 'editor' && renderEditor()}
{tab === 'generator' && renderGenerator()}
{tab === 'export' && renderExport()}
{tab === 'loeschkonzept' && renderLoeschkonzept()}
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,510 @@
/**
* Integration Tests: Company Profile → Compliance Scope → VVT Generator
*
* Tests the complete data pipeline from Company Profile master data
* through the Compliance Scope Engine to VVT activity generation.
*/
import { describe, it, expect } from 'vitest'
import {
prefillFromCompanyProfile,
exportToVVTAnswers,
getAutoFilledScoringAnswers,
SCOPE_QUESTION_BLOCKS,
} from '../compliance-scope-profiling'
import {
generateActivities,
PROFILING_QUESTIONS,
DEPARTMENT_DATA_CATEGORIES,
SCOPE_PREFILLED_VVT_QUESTIONS,
} from '../vvt-profiling'
import type { ScopeProfilingAnswer } from '../compliance-scope-types'
// Helper
function ans(questionId: string, value: unknown): ScopeProfilingAnswer {
return { questionId, value } as ScopeProfilingAnswer
}
// =============================================================================
// 1. Company Profile → Scope Prefill
// =============================================================================
describe('CompanyProfile → Scope prefill', () => {
it('prefills org_has_dsb when dpoName is set', () => {
const profile = { dpoName: 'Max Mustermann' } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'org_has_dsb')?.value).toBe(true)
})
it('does NOT prefill org_has_dsb when dpoName is empty', () => {
const profile = { dpoName: '' } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'org_has_dsb')).toBeUndefined()
})
it('maps offerings to prod_type correctly', () => {
const profile = { offerings: ['WebApp', 'SaaS', 'API'] } as any
const answers = prefillFromCompanyProfile(profile)
const prodType = answers.find((a) => a.questionId === 'prod_type')
expect(prodType?.value).toEqual(expect.arrayContaining(['webapp', 'saas', 'api']))
})
it('detects webshop in offerings', () => {
const profile = { offerings: ['Webshop'] } as any
const answers = prefillFromCompanyProfile(profile)
expect(answers.find((a) => a.questionId === 'prod_webshop')?.value).toBe(true)
})
it('returns empty array when profile has no relevant data', () => {
const profile = {} as any
const answers = prefillFromCompanyProfile(profile)
expect(answers).toEqual([])
})
})
describe('CompanyProfile → Scope scoring answers', () => {
it('maps employeeCount to org_employee_count', () => {
const profile = { employeeCount: '50-249' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_employee_count')?.value).toBe('50-249')
})
it('maps industry to org_industry', () => {
const profile = { industry: ['IT & Software', 'Finanzdienstleistungen'] } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_industry')?.value).toBe(
'IT & Software, Finanzdienstleistungen'
)
})
it('maps annualRevenue to org_annual_revenue', () => {
const profile = { annualRevenue: '1-10M' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_annual_revenue')?.value).toBe('1-10M')
})
it('maps businessModel to org_business_model', () => {
const profile = { businessModel: 'B2B' } as any
const answers = getAutoFilledScoringAnswers(profile)
expect(answers.find((a) => a.questionId === 'org_business_model')?.value).toBe('B2B')
})
})
// =============================================================================
// 2. Scope → VVT Answer Mapping (exportToVVTAnswers)
// =============================================================================
describe('Scope → VVT answer export', () => {
it('maps scope questions with mapsToVVTQuestion property', () => {
// Block 9: dk_dept_hr maps to dept_hr_categories
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'SALARY_DATA', 'HEALTH_DATA']),
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'SALARY_DATA', 'HEALTH_DATA'])
})
it('maps multiple department data categories', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'BANK_ACCOUNT']),
ans('dk_dept_finance', ['INVOICE_DATA', 'TAX_ID']),
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA']),
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'BANK_ACCOUNT'])
expect(vvtAnswers.dept_finance_categories).toEqual(['INVOICE_DATA', 'TAX_ID'])
expect(vvtAnswers.dept_marketing_categories).toEqual(['EMAIL', 'TRACKING_DATA'])
})
it('ignores scope questions without mapsToVVTQuestion', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('vvt_has_vvt', true), // No mapsToVVTQuestion property
]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(Object.keys(vvtAnswers)).toHaveLength(0)
})
it('handles empty scope answers', () => {
const vvtAnswers = exportToVVTAnswers([])
expect(vvtAnswers).toEqual({})
})
})
// =============================================================================
// 3. Scope → VVT Profiling Prefill
// Note: prefillFromScopeAnswers() uses dynamic require('./compliance-scope-profiling')
// which doesn't resolve in vitest. We test the same pipeline by calling
// exportToVVTAnswers() directly (which is what prefillFromScopeAnswers wraps).
// =============================================================================
describe('Scope → VVT Profiling Prefill (via exportToVVTAnswers)', () => {
it('converts scope answers to VVT ProfilingAnswers format', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA']),
ans('dk_dept_finance', ['BANK_ACCOUNT']),
]
const exported = exportToVVTAnswers(scopeAnswers)
// Same transformation as prefillFromScopeAnswers
const profiling: Record<string, unknown> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) profiling[key] = value
}
expect(profiling.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA'])
expect(profiling.dept_finance_categories).toEqual(['BANK_ACCOUNT'])
})
it('filters out null/undefined values', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', null)]
const exported = exportToVVTAnswers(scopeAnswers)
const profiling: Record<string, unknown> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) profiling[key] = value
}
expect(profiling.dept_hr_categories).toBeUndefined()
})
})
// =============================================================================
// 4. VVT Generator — generateActivities
// =============================================================================
describe('generateActivities', () => {
it('always generates 4 IT baseline activities', () => {
const result = generateActivities({})
const names = result.generatedActivities.map((a) => a.name)
expect(result.generatedActivities.length).toBeGreaterThanOrEqual(4)
// IT baselines are always added
const itTemplates = result.generatedActivities.filter(
(a) => a.businessFunction === 'it_operations'
)
expect(itTemplates.length).toBeGreaterThanOrEqual(4)
})
it('triggers HR templates when dept_hr=true', () => {
const result = generateActivities({ dept_hr: true })
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThanOrEqual(3) // mitarbeiter, gehalt, zeiterfassung
})
it('triggers finance templates when dept_finance=true', () => {
const result = generateActivities({ dept_finance: true })
const financeActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'finance'
)
expect(financeActivities.length).toBeGreaterThanOrEqual(2) // buchhaltung, zahlungsverkehr
})
it('enriches activities with US cloud third-country transfer', () => {
const result = generateActivities({ dept_hr: true, transfer_cloud_us: true })
// Every activity should have a US third-country transfer
for (const activity of result.generatedActivities) {
expect(activity.thirdCountryTransfers.some((t) => t.country === 'US')).toBe(true)
}
})
it('adds HEALTH_DATA to HR activities when data_health=true', () => {
const result = generateActivities({ dept_hr: true, data_health: true })
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThan(0)
for (const hr of hrActivities) {
expect(hr.personalDataCategories).toContain('HEALTH_DATA')
}
})
it('calculates Art. 30 Abs. 5 exemption correctly', () => {
// < 250 employees, no special categories → exempt
const result1 = generateActivities({ org_employees: 50 })
expect(result1.art30Abs5Exempt).toBe(true)
// >= 250 employees → not exempt
const result2 = generateActivities({ org_employees: 500 })
expect(result2.art30Abs5Exempt).toBe(false)
// < 250 but with special categories → not exempt
const result3 = generateActivities({ org_employees: 50, data_health: true })
expect(result3.art30Abs5Exempt).toBe(false)
})
it('generates unique VVT IDs for all activities', () => {
const result = generateActivities({
dept_hr: true,
dept_finance: true,
dept_sales: true,
dept_marketing: true,
})
const ids = result.generatedActivities.map((a) => a.vvtId)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
})
it('calculates coverage score > 0 for template-generated activities', () => {
const result = generateActivities({ dept_hr: true })
expect(result.coverageScore).toBeGreaterThan(0)
})
})
// =============================================================================
// 5. Full Pipeline: Company Profile → Scope → VVT
// =============================================================================
describe('Full Pipeline: CompanyProfile → Scope → VVT Generation', () => {
// Helper: replicate what prefillFromScopeAnswers does (avoiding dynamic require)
function scopeToProfilingAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, string | string[] | number | boolean> {
const exported = exportToVVTAnswers(scopeAnswers)
const profiling: Record<string, string | string[] | number | boolean> = {}
for (const [key, value] of Object.entries(exported)) {
if (value !== undefined && value !== null) {
profiling[key] = value as string | string[] | number | boolean
}
}
return profiling
}
it('complete flow: profile with DSB → scope prefill → VVT generation', () => {
// Step 1: Company Profile
const profile = {
dpoName: 'Dr. Datenschutz',
employeeCount: '50-249',
industry: ['IT & Software'],
offerings: ['WebApp', 'SaaS'],
} as any
// Step 2: Prefill scope from profile
const profileAnswers = prefillFromCompanyProfile(profile)
const scoringAnswers = getAutoFilledScoringAnswers(profile)
// Simulate user answering scope questions + auto-prefilled from profile
const userAnswers: ScopeProfilingAnswer[] = [
// Block 8: departments
ans('vvt_departments', ['personal', 'finanzen', 'it']),
// Block 9: data categories per department
ans('dk_dept_hr', ['NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA']),
ans('dk_dept_finance', ['NAME', 'BANK_ACCOUNT', 'INVOICE_DATA', 'TAX_ID']),
ans('dk_dept_it', ['USER_ACCOUNTS', 'LOG_DATA', 'DEVICE_DATA']),
// Block 2: data types
ans('data_art9', true),
ans('data_minors', false),
]
const allScopeAnswers = [...profileAnswers, ...scoringAnswers, ...userAnswers]
// Step 3: Export to VVT format
const vvtAnswers = exportToVVTAnswers(allScopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual([
'NAME',
'ADDRESS',
'SALARY_DATA',
'HEALTH_DATA',
])
expect(vvtAnswers.dept_finance_categories).toEqual([
'NAME',
'BANK_ACCOUNT',
'INVOICE_DATA',
'TAX_ID',
])
// Step 4: Prefill VVT profiling from scope (via direct export)
const profilingAnswers = scopeToProfilingAnswers(allScopeAnswers)
// Verify data survived the transformation
expect(profilingAnswers.dept_hr_categories).toEqual([
'NAME',
'ADDRESS',
'SALARY_DATA',
'HEALTH_DATA',
])
// Step 5: Generate VVT activities
// Add department triggers that match Block 8 selections
profilingAnswers.dept_hr = true
profilingAnswers.dept_finance = true
const result = generateActivities(profilingAnswers)
// Verify activities were generated
expect(result.generatedActivities.length).toBeGreaterThan(4) // 4 IT baseline + HR + Finance
// Verify HR activities exist
const hrActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'hr'
)
expect(hrActivities.length).toBeGreaterThanOrEqual(3)
// Verify finance activities exist
const financeActivities = result.generatedActivities.filter(
(a) => a.businessFunction === 'finance'
)
expect(financeActivities.length).toBeGreaterThanOrEqual(2)
})
it('end-to-end: departments selected in scope generate correct VVT activities', () => {
// Simulate a complete scope session with department selections
const scopeAnswers: ScopeProfilingAnswer[] = [
// Block 2: data_art9 maps to data_health in VVT
ans('data_art9', true),
// Block 4: tech_third_country maps to transfer_cloud_us
ans('tech_third_country', true),
// Block 8: departments
ans('vvt_departments', ['personal', 'marketing', 'kundenservice']),
// Block 9: per-department data categories
ans('dk_dept_hr', ['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS']),
ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA', 'CONSENT_DATA']),
ans('dk_dept_support', ['NAME', 'TICKET_DATA', 'COMMUNICATION_DATA']),
]
// Transform to VVT answers
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
// Verify Block 9 data categories are mapped correctly
expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS'])
expect(vvtAnswers.dept_marketing_categories).toEqual([
'EMAIL',
'TRACKING_DATA',
'CONSENT_DATA',
])
expect(vvtAnswers.dept_support_categories).toEqual([
'NAME',
'TICKET_DATA',
'COMMUNICATION_DATA',
])
// Verify the full pipeline using direct export
const profilingAnswers = scopeToProfilingAnswers(scopeAnswers)
expect(profilingAnswers.dept_hr_categories).toBeDefined()
expect(profilingAnswers.dept_marketing_categories).toBeDefined()
expect(profilingAnswers.dept_support_categories).toBeDefined()
})
})
// =============================================================================
// 6. DEPARTMENT_DATA_CATEGORIES Integrity
// =============================================================================
describe('DEPARTMENT_DATA_CATEGORIES consistency', () => {
it('all 12 departments are defined', () => {
const expected = [
'dept_hr',
'dept_recruiting',
'dept_finance',
'dept_sales',
'dept_marketing',
'dept_support',
'dept_it',
'dept_recht',
'dept_produktion',
'dept_logistik',
'dept_einkauf',
'dept_facility',
]
for (const dept of expected) {
expect(DEPARTMENT_DATA_CATEGORIES[dept]).toBeDefined()
expect(DEPARTMENT_DATA_CATEGORIES[dept].categories.length).toBeGreaterThan(0)
}
})
it('every department has a label and icon', () => {
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
expect(dept.label).toBeTruthy()
expect(dept.icon).toBeTruthy()
}
})
it('every category has id and label', () => {
for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) {
for (const cat of dept.categories) {
expect(cat.id).toBeTruthy()
expect(cat.label).toBeTruthy()
expect(cat.info).toBeTruthy()
}
}
})
it('Art. 9 categories are correctly flagged', () => {
const art9Categories = [
{ dept: 'dept_hr', id: 'HEALTH_DATA' },
{ dept: 'dept_hr', id: 'RELIGIOUS_BELIEFS' },
{ dept: 'dept_recruiting', id: 'HEALTH_DATA' },
{ dept: 'dept_recht', id: 'CRIMINAL_DATA' },
{ dept: 'dept_produktion', id: 'HEALTH_DATA' },
{ dept: 'dept_facility', id: 'HEALTH_DATA' },
]
for (const { dept, id } of art9Categories) {
const cat = DEPARTMENT_DATA_CATEGORIES[dept].categories.find((c) => c.id === id)
expect(cat?.isArt9).toBe(true)
}
})
})
// =============================================================================
// 7. Block 9 ↔ VVT Mapping Integrity
// =============================================================================
describe('Block 9 Scope ↔ VVT question mapping', () => {
it('every Block 9 question has mapsToVVTQuestion', () => {
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
expect(block9).toBeDefined()
for (const q of block9!.questions) {
expect(q.mapsToVVTQuestion).toBeTruthy()
expect(q.mapsToVVTQuestion).toMatch(/^dept_\w+_categories$/)
}
})
it('Block 9 question options match DEPARTMENT_DATA_CATEGORIES', () => {
const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail')
expect(block9).toBeDefined()
// dk_dept_hr should have same options as DEPARTMENT_DATA_CATEGORIES.dept_hr
const hrQuestion = block9!.questions.find((q) => q.id === 'dk_dept_hr')
expect(hrQuestion).toBeDefined()
const expectedIds = DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map((c) => c.id)
const actualIds = hrQuestion!.options!.map((o) => o.value)
expect(actualIds).toEqual(expectedIds)
})
it('SCOPE_PREFILLED_VVT_QUESTIONS lists all cross-module questions', () => {
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('org_industry')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('dept_hr')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('data_health')
expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('transfer_cloud_us')
expect(SCOPE_PREFILLED_VVT_QUESTIONS.length).toBeGreaterThanOrEqual(15)
})
})
// =============================================================================
// 8. Edge Cases
// =============================================================================
describe('Edge cases', () => {
it('generateActivities with no answers still produces IT baselines', () => {
const result = generateActivities({})
expect(result.generatedActivities.length).toBe(4) // 4 IT baselines
expect(result.art30Abs5Exempt).toBe(true) // 0 employees, no special categories
})
it('same template triggered by multiple questions is only generated once', () => {
const result = generateActivities({
dept_sales: true, // triggers sales-kundenverwaltung
sys_crm: true, // also triggers sales-kundenverwaltung
})
const salesKunden = result.generatedActivities.filter((a) =>
a.name.toLowerCase().includes('kundenverwaltung')
)
// Should be deduplicated (Set-based triggeredIds)
expect(salesKunden.length).toBe(1)
})
it('empty department category selections produce valid but empty mappings', () => {
const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', [])]
const vvtAnswers = exportToVVTAnswers(scopeAnswers)
expect(vvtAnswers.dept_hr_categories).toEqual([])
})
})

View File

@@ -1,9 +1,9 @@
/**
* Loeschfristen Baseline-Katalog
*
* 18 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
* 25 vordefinierte Aufbewahrungsfristen-Templates fuer gaengige
* Datenobjekte in deutschen Unternehmen. Basierend auf AO, HGB,
* UStG, BGB, ArbZG, AGG, BDSG und BSIG.
* UStG, BGB, ArbZG, AGG, BDSG, BSIG und ArbMedVV.
*
* Werden genutzt, um neue Loeschfrist-Policies schnell aus
* bewaehrten Vorlagen zu erstellen.
@@ -48,7 +48,7 @@ export interface BaselineTemplate {
}
// =============================================================================
// BASELINE TEMPLATES (18 Vorlagen)
// BASELINE TEMPLATES (25 Vorlagen)
// =============================================================================
export const BASELINE_TEMPLATES: BaselineTemplate[] = [
@@ -519,6 +519,188 @@ export const BASELINE_TEMPLATES: BaselineTemplate[] = [
reviewInterval: 'ANNUAL',
tags: ['datenschutz', 'consent'],
},
// ==================== 19. E-Mail-Archivierung ====================
{
templateId: 'email-archivierung',
dataObjectName: 'E-Mail-Archivierung',
description:
'Archivierte geschaeftliche E-Mails inkl. Anhaenge, die als Handelsbriefe oder steuerrelevante Korrespondenz einzustufen sind.',
affectedGroups: ['Mitarbeiter', 'Kunden', 'Lieferanten'],
dataCategories: ['E-Mail-Korrespondenz', 'Anhaenge', 'Metadaten'],
primaryPurpose:
'Erfuellung der handelsrechtlichen Aufbewahrungspflicht fuer geschaeftliche Korrespondenz, die als Handelsbrief einzuordnen ist.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess 257 HGB fuer empfangene und versandte Handelsbriefe (6 Jahre) bzw. buchhalterisch relevante E-Mails (10 Jahre).',
retentionDuration: 6,
retentionUnit: 'YEARS',
retentionDescription: '6 Jahre nach Versand/Empfang der E-Mail',
startEvent: 'Versand- bzw. Empfangsdatum der E-Mail',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung durch das E-Mail-Archivierungssystem nach Ablauf der konfigurierten Aufbewahrungsfrist. Vor Loeschung wird geprueft, ob die E-Mail in laufenden Verfahren benoetigt wird.',
responsibleRole: 'IT-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['kommunikation', 'hgb'],
},
// ==================== 20. Zutrittsprotokolle ====================
{
templateId: 'zutrittsprotokolle',
dataObjectName: 'Zutrittsprotokolle',
description:
'Protokolle des Zutrittskontrollsystems inkl. Zeitstempel, Kartennummer, Zutrittsort und Zugangsentscheidung (gewaehrt/verweigert).',
affectedGroups: ['Mitarbeiter', 'Besucher'],
dataCategories: ['Zutrittsdaten', 'Zeitstempel', 'Kartennummern', 'Standortdaten'],
primaryPurpose:
'Sicherstellung der physischen Sicherheit, Nachvollziehbarkeit von Zutritten und Unterstuetzung bei der Aufklaerung von Sicherheitsvorfaellen.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BSIG',
retentionDriverDetail:
'Aufbewahrung gemaess BSI-Grundschutz-Empfehlung fuer Zutrittsprotokolle zur Analyse von Sicherheitsvorfaellen (90 Tage).',
retentionDuration: 90,
retentionUnit: 'DAYS',
retentionDescription: '90 Tage nach Zeitpunkt des Zutritts',
startEvent: 'Zeitpunkt des protokollierten Zutrittsereignisses',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Rotation und Loeschung der Zutrittsprotokolle durch das Zutrittskontrollsystem nach Ablauf der 90-Tage-Frist.',
responsibleRole: 'Facility Management',
reviewInterval: 'QUARTERLY',
tags: ['sicherheit', 'zutritt'],
},
// ==================== 21. Schulungsnachweise ====================
{
templateId: 'schulungsnachweise',
dataObjectName: 'Schulungsnachweise',
description:
'Teilnahmebestaetigungen, Zertifikate und Protokolle von Mitarbeiterschulungen (Datenschutz, Arbeitssicherheit, Compliance).',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Schulungsdaten', 'Zertifikate', 'Teilnahmelisten'],
primaryPurpose:
'Nachweis der Durchfuehrung gesetzlich vorgeschriebener Schulungen und Dokumentation der Mitarbeiterqualifikation.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'CUSTOM',
retentionDriverDetail:
'Aufbewahrung fuer 3 Jahre nach Ende des Beschaeftigungsverhaeltnisses als Nachweis der ordnungsgemaessen Schulungsdurchfuehrung.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Ende des Beschaeftigungsverhaeltnisses',
startEvent: 'Ende des Beschaeftigungsverhaeltnisses des geschulten Mitarbeiters',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch die HR-Abteilung vor Loeschung, da Schulungsnachweise als Compliance-Nachweis in Audits relevant sein koennen.',
responsibleRole: 'HR-Abteilung',
reviewInterval: 'ANNUAL',
tags: ['hr', 'schulung'],
},
// ==================== 22. Betriebsarzt-Dokumentation ====================
{
templateId: 'betriebsarzt-doku',
dataObjectName: 'Betriebsarzt-Dokumentation',
description:
'Ergebnisse arbeitsmedizinischer Vorsorgeuntersuchungen, Eignungsuntersuchungen und arbeitsmedizinische Empfehlungen.',
affectedGroups: ['Mitarbeiter'],
dataCategories: ['Gesundheitsdaten', 'Vorsorgeuntersuchungen', 'Eignungsbefunde'],
primaryPurpose:
'Erfuellung der Dokumentationspflicht fuer arbeitsmedizinische Vorsorge gemaess ArbMedVV und Nachweisfuehrung gegenueber Berufsgenossenschaften.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'CUSTOM',
retentionDriverDetail:
'Aufbewahrungspflicht gemaess ArbMedVV (Verordnung zur arbeitsmedizinischen Vorsorge) und Berufsgenossenschaftliche Grundsaetze: bis zu 40 Jahre bei Exposition gegenueber krebserzeugenden Gefahrstoffen.',
retentionDuration: 40,
retentionUnit: 'YEARS',
retentionDescription: '40 Jahre nach letzter Exposition (bei Gefahrstoffen), sonst 10 Jahre nach Ende der Taetigkeit',
startEvent: 'Ende der expositionsrelevanten Taetigkeit bzw. Ende des Beschaeftigungsverhaeltnisses',
deletionMethod: 'PHYSICAL_DESTROY',
deletionMethodDetail:
'Physische Vernichtung der Papierunterlagen durch zertifizierten Aktenvernichtungsdienstleister (DIN 66399, Sicherheitsstufe P-5). Digitale Daten werden kryptographisch geloescht.',
responsibleRole: 'Betriebsarzt / Arbeitsmedizinischer Dienst',
reviewInterval: 'ANNUAL',
tags: ['hr', 'gesundheit'],
},
// ==================== 23. Kundenreklamationen ====================
{
templateId: 'kundenreklamationen',
dataObjectName: 'Kundenreklamationen',
description:
'Reklamationsvorgaenge inkl. Beschwerdeinhalt, Kommunikationsverlauf, Massnahmen und Ergebnis der Reklamationsbearbeitung.',
affectedGroups: ['Kunden'],
dataCategories: ['Reklamationsdaten', 'Kommunikation', 'Massnahmenprotokolle'],
primaryPurpose:
'Dokumentation und Bearbeitung von Kundenreklamationen, Qualitaetssicherung und Absicherung gegen Gewaehrleistungsansprueche.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'BGB_195',
retentionDriverDetail:
'Aufbewahrung fuer die Dauer der regelmaessigen Verjaehrungsfrist gemaess 195 BGB (3 Jahre) zur Absicherung gegen Gewaehrleistungs- und Schadensersatzansprueche.',
retentionDuration: 3,
retentionUnit: 'YEARS',
retentionDescription: '3 Jahre nach Abschluss des Reklamationsvorgangs',
startEvent: 'Abschluss des Reklamationsvorgangs (letzte Massnahme)',
deletionMethod: 'ANONYMIZATION',
deletionMethodDetail:
'Anonymisierung der personenbezogenen Daten nach Ablauf der Frist. Anonymisierte Reklamationsstatistiken bleiben fuer die Qualitaetssicherung erhalten.',
responsibleRole: 'Qualitaetsmanagement',
reviewInterval: 'ANNUAL',
tags: ['kunden', 'qualitaet'],
},
// ==================== 24. Lieferantenbewertungen ====================
{
templateId: 'lieferantenbewertungen',
dataObjectName: 'Lieferantenbewertungen',
description:
'Bewertungen und Auditergebnisse von Lieferanten und Auftragsverarbeitern inkl. Qualitaets-, Compliance- und Datenschutz-Bewertungen.',
affectedGroups: ['Lieferanten', 'Auftragsverarbeiter'],
dataCategories: ['Bewertungsdaten', 'Auditberichte', 'Vertragsdaten'],
primaryPurpose:
'Dokumentation der Sorgfaltspflicht bei der Auswahl und Ueberwachung von Auftragsverarbeitern gemaess Art. 28 DSGVO und Qualitaetssicherung in der Lieferkette.',
deletionTrigger: 'RETENTION_DRIVER',
retentionDriver: 'HGB_257',
retentionDriverDetail:
'Aufbewahrung gemaess 257 HGB als handelsrelevante Unterlagen sowie zur Nachweisfuehrung der Sorgfaltspflicht bei der Auftragsverarbeitung.',
retentionDuration: 6,
retentionUnit: 'YEARS',
retentionDescription: '6 Jahre nach Ende der Geschaeftsbeziehung',
startEvent: 'Ende der Geschaeftsbeziehung mit dem Lieferanten/Auftragsverarbeiter',
deletionMethod: 'MANUAL_REVIEW_DELETE',
deletionMethodDetail:
'Manuelle Pruefung durch den Einkauf/Compliance-Abteilung vor Loeschung, um sicherzustellen, dass keine Nachweispflichten aus laufenden Vertraegen oder Audits bestehen.',
responsibleRole: 'Einkauf / Compliance',
reviewInterval: 'ANNUAL',
tags: ['lieferanten', 'einkauf'],
},
// ==================== 25. Social-Media-Marketingdaten ====================
{
templateId: 'social-media-daten',
dataObjectName: 'Social-Media-Marketingdaten',
description:
'Personenbezogene Daten aus Social-Media-Kampagnen inkl. Nutzerinteraktionen, Custom Audiences, Retargeting-Listen und Kampagnen-Analytics.',
affectedGroups: ['Kunden', 'Interessenten', 'Website-Besucher'],
dataCategories: ['Interaktionsdaten', 'Zielgruppendaten', 'Tracking-Daten', 'Profilmerkmale'],
primaryPurpose:
'Durchfuehrung zielgerichteter Marketing-Kampagnen auf Social-Media-Plattformen und Analyse der Kampagneneffektivitaet.',
deletionTrigger: 'PURPOSE_END',
retentionDriver: null,
retentionDriverDetail:
'Keine gesetzliche Aufbewahrungspflicht. Daten werden bis zum Widerruf der Einwilligung bzw. bis zum Ende der Kampagne gespeichert (Art. 6 Abs. 1 lit. a DSGVO).',
retentionDuration: null,
retentionUnit: null,
retentionDescription: 'Bis zum Widerruf der Einwilligung oder Ende des Kampagnenzwecks',
startEvent: 'Widerruf der Einwilligung oder Ende der Marketing-Kampagne',
deletionMethod: 'AUTO_DELETE',
deletionMethodDetail:
'Automatische Loeschung der personenbezogenen Daten in den Social-Media-Werbekonten und internen Systemen nach Zweckwegfall. Custom Audiences werden bei Plattformanbietern geloescht.',
responsibleRole: 'Marketing',
reviewInterval: 'SEMI_ANNUAL',
tags: ['marketing', 'social'],
},
]
// =============================================================================

View File

@@ -6,8 +6,10 @@
import {
LoeschfristPolicy,
PolicyStatus,
RetentionDriverType,
isPolicyOverdue,
getActiveLegalHolds,
RETENTION_DRIVER_META,
} from './loeschfristen-types'
// =============================================================================
@@ -22,6 +24,10 @@ export type ComplianceIssueType =
| 'LEGAL_HOLD_CONFLICT'
| 'STALE_DRAFT'
| 'UNCOVERED_VVT_CATEGORY'
| 'MISSING_DELETION_METHOD'
| 'MISSING_STORAGE_LOCATIONS'
| 'EXCESSIVE_RETENTION'
| 'MISSING_DATA_CATEGORIES'
export type ComplianceIssueSeverity = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
@@ -219,6 +225,108 @@ function checkStaleDraft(policy: LoeschfristPolicy): ComplianceIssue | null {
return null
}
/**
* Check 8: MISSING_DELETION_METHOD (MEDIUM)
* Active policy without a deletion method detail description.
*/
function checkMissingDeletionMethod(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'ACTIVE' && !policy.deletionMethodDetail.trim()) {
return createIssue(
policy,
'MISSING_DELETION_METHOD',
'MEDIUM',
'Keine Loeschmethode beschrieben',
`Die aktive Policy "${policy.dataObjectName}" hat keine detaillierte Beschreibung der Loeschmethode. Fuer ein auditfaehiges Loeschkonzept muss dokumentiert sein, wie die Loeschung technisch durchgefuehrt wird.`,
'Ergaenzen Sie eine detaillierte Beschreibung der Loeschmethode (z.B. automatisches Loeschen durch Datenbank-Job, manuelle Pruefung durch Fachabteilung, kryptographische Loeschung).'
)
}
return null
}
/**
* Check 9: MISSING_STORAGE_LOCATIONS (MEDIUM)
* Active policy without any documented storage locations.
*/
function checkMissingStorageLocations(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status === 'ACTIVE' && policy.storageLocations.length === 0) {
return createIssue(
policy,
'MISSING_STORAGE_LOCATIONS',
'MEDIUM',
'Keine Speicherorte dokumentiert',
`Die aktive Policy "${policy.dataObjectName}" hat keine Speicherorte hinterlegt. Ohne Speicherort-Dokumentation ist unklar, wo die Daten gespeichert sind und wo die Loeschung durchgefuehrt werden muss.`,
'Dokumentieren Sie mindestens einen Speicherort (z.B. Datenbank, Cloud-Speicher, E-Mail-System, Papierarchiv).'
)
}
return null
}
/**
* Check 10: EXCESSIVE_RETENTION (HIGH)
* Retention duration exceeds 2x the legal default for the driver.
*/
function checkExcessiveRetention(policy: LoeschfristPolicy): ComplianceIssue | null {
if (
policy.retentionDriver &&
policy.retentionDriver !== 'CUSTOM' &&
policy.retentionDuration !== null &&
policy.retentionUnit !== null
) {
const meta = RETENTION_DRIVER_META[policy.retentionDriver]
if (meta.defaultDuration !== null && meta.defaultUnit !== null) {
// Normalize both to days for comparison
const policyDays = toDays(policy.retentionDuration, policy.retentionUnit)
const legalDays = toDays(meta.defaultDuration, meta.defaultUnit)
if (legalDays > 0 && policyDays > legalDays * 2) {
return createIssue(
policy,
'EXCESSIVE_RETENTION',
'HIGH',
'Ueberschreitung der gesetzlichen Aufbewahrungsfrist',
`Die Policy "${policy.dataObjectName}" hat eine Aufbewahrungsdauer von ${policy.retentionDuration} ${policy.retentionUnit === 'YEARS' ? 'Jahren' : policy.retentionUnit === 'MONTHS' ? 'Monaten' : 'Tagen'}, die mehr als das Doppelte der gesetzlichen Frist (${meta.defaultDuration} ${meta.defaultUnit === 'YEARS' ? 'Jahre' : meta.defaultUnit === 'MONTHS' ? 'Monate' : 'Tage'} nach ${meta.statute}) betraegt. Ueberlange Speicherung widerspricht dem Grundsatz der Speicherbegrenzung (Art. 5 Abs. 1 lit. e DSGVO).`,
'Pruefen Sie, ob die verlaengerte Aufbewahrungsdauer gerechtfertigt ist. Falls nicht, reduzieren Sie sie auf die gesetzliche Mindestfrist.'
)
}
}
}
return null
}
/**
* Check 11: MISSING_DATA_CATEGORIES (LOW)
* Non-draft policy without any data categories assigned.
*/
function checkMissingDataCategories(policy: LoeschfristPolicy): ComplianceIssue | null {
if (policy.status !== 'DRAFT' && policy.dataCategories.length === 0) {
return createIssue(
policy,
'MISSING_DATA_CATEGORIES',
'LOW',
'Keine Datenkategorien zugeordnet',
`Die Policy "${policy.dataObjectName}" (Status: ${policy.status}) hat keine Datenkategorien zugeordnet. Ohne Datenkategorien ist unklar, welche personenbezogenen Daten von dieser Loeschregel betroffen sind.`,
'Ordnen Sie mindestens eine Datenkategorie zu (z.B. Stammdaten, Kontaktdaten, Finanzdaten, Gesundheitsdaten).'
)
}
return null
}
/**
* Helper: convert retention duration to days for comparison.
*/
function toDays(duration: number, unit: string): number {
switch (unit) {
case 'DAYS': return duration
case 'MONTHS': return duration * 30
case 'YEARS': return duration * 365
default: return duration
}
}
// =============================================================================
// MAIN COMPLIANCE CHECK
// =============================================================================
@@ -248,6 +356,10 @@ export function runComplianceCheck(
checkNoResponsible(policy),
checkLegalHoldConflict(policy),
checkStaleDraft(policy),
checkMissingDeletionMethod(policy),
checkMissingStorageLocations(policy),
checkExcessiveRetention(policy),
checkMissingDataCategories(policy),
]
for (const issue of checks) {

View File

@@ -0,0 +1,827 @@
// =============================================================================
// Loeschfristen Module - Loeschkonzept Document Generator
// Generates a printable, audit-ready HTML document according to DSGVO Art. 5/17/30
// =============================================================================
import type {
LoeschfristPolicy,
RetentionDriverType,
} from './loeschfristen-types'
import {
RETENTION_DRIVER_META,
DELETION_METHOD_LABELS,
STATUS_LABELS,
TRIGGER_LABELS,
REVIEW_INTERVAL_LABELS,
formatRetentionDuration,
getEffectiveDeletionTrigger,
getActiveLegalHolds,
} from './loeschfristen-types'
import type { ComplianceCheckResult, ComplianceIssueSeverity } from './loeschfristen-compliance'
// =============================================================================
// TYPES
// =============================================================================
export interface LoeschkonzeptOrgHeader {
organizationName: string
industry: string
dpoName: string
dpoContact: string
responsiblePerson: string
locations: string[]
employeeCount: string
loeschkonzeptVersion: string
lastReviewDate: string
nextReviewDate: string
reviewInterval: string
}
export interface LoeschkonzeptRevision {
version: string
date: string
author: string
changes: string
}
// =============================================================================
// DEFAULTS
// =============================================================================
export function createDefaultLoeschkonzeptOrgHeader(): LoeschkonzeptOrgHeader {
const now = new Date()
const nextYear = new Date()
nextYear.setFullYear(nextYear.getFullYear() + 1)
return {
organizationName: '',
industry: '',
dpoName: '',
dpoContact: '',
responsiblePerson: '',
locations: [],
employeeCount: '',
loeschkonzeptVersion: '1.0',
lastReviewDate: now.toISOString().split('T')[0],
nextReviewDate: nextYear.toISOString().split('T')[0],
reviewInterval: 'Jaehrlich',
}
}
// =============================================================================
// SEVERITY LABELS (for Compliance Status section)
// =============================================================================
const SEVERITY_LABELS_DE: Record<ComplianceIssueSeverity, string> = {
CRITICAL: 'Kritisch',
HIGH: 'Hoch',
MEDIUM: 'Mittel',
LOW: 'Niedrig',
}
const SEVERITY_COLORS: Record<ComplianceIssueSeverity, string> = {
CRITICAL: '#dc2626',
HIGH: '#ea580c',
MEDIUM: '#d97706',
LOW: '#6b7280',
}
// =============================================================================
// HTML DOCUMENT BUILDER
// =============================================================================
export function buildLoeschkonzeptHtml(
policies: LoeschfristPolicy[],
orgHeader: LoeschkonzeptOrgHeader,
vvtActivities: Array<{ id: string; vvt_id?: string; vvtId?: string; name?: string; activity_name?: string }>,
complianceResult: ComplianceCheckResult | null,
revisions: LoeschkonzeptRevision[]
): string {
const today = new Date().toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
const orgName = orgHeader.organizationName || 'Organisation'
// Collect unique storage locations across all policies
const allStorageLocations = new Set<string>()
for (const p of activePolicies) {
for (const loc of p.storageLocations) {
allStorageLocations.add(loc.name || loc.type)
}
}
// Collect unique responsible roles
const roleMap = new Map<string, string[]>()
for (const p of activePolicies) {
const role = p.responsibleRole || p.responsiblePerson || 'Nicht zugewiesen'
if (!roleMap.has(role)) roleMap.set(role, [])
roleMap.get(role)!.push(p.dataObjectName || p.policyId)
}
// Collect active legal holds
const allActiveLegalHolds: Array<{ policy: string; hold: LoeschfristPolicy['legalHolds'][0] }> = []
for (const p of activePolicies) {
for (const h of getActiveLegalHolds(p)) {
allActiveLegalHolds.push({ policy: p.dataObjectName || p.policyId, hold: h })
}
}
// Build VVT cross-reference data
const vvtRefs: Array<{ policyName: string; policyId: string; vvtId: string; vvtName: string }> = []
for (const p of activePolicies) {
for (const linkedId of p.linkedVVTActivityIds) {
const activity = vvtActivities.find(a => a.id === linkedId)
if (activity) {
vvtRefs.push({
policyName: p.dataObjectName || p.policyId,
policyId: p.policyId,
vvtId: activity.vvt_id || activity.vvtId || linkedId.substring(0, 8),
vvtName: activity.activity_name || activity.name || 'Unbenannte Verarbeitungstaetigkeit',
})
}
}
}
// =========================================================================
// HTML Template
// =========================================================================
let html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Loeschkonzept — ${escHtml(orgName)}</title>
<style>
@page { size: A4; margin: 20mm 18mm 22mm 18mm; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 10pt;
line-height: 1.5;
color: #1e293b;
}
/* Cover */
.cover {
min-height: 90vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
page-break-after: always;
}
.cover h1 {
font-size: 28pt;
color: #5b21b6;
margin-bottom: 8px;
font-weight: 700;
}
.cover .subtitle {
font-size: 14pt;
color: #7c3aed;
margin-bottom: 40px;
}
.cover .org-info {
background: #f5f3ff;
border: 1px solid #ddd6fe;
border-radius: 8px;
padding: 24px 40px;
text-align: left;
width: 400px;
margin-bottom: 24px;
}
.cover .org-info div { margin-bottom: 6px; }
.cover .org-info .label { font-weight: 600; color: #5b21b6; display: inline-block; min-width: 160px; }
.cover .legal-ref {
font-size: 9pt;
color: #64748b;
margin-top: 20px;
}
/* TOC */
.toc {
page-break-after: always;
padding-top: 40px;
}
.toc h2 {
font-size: 18pt;
color: #5b21b6;
margin-bottom: 20px;
border-bottom: 2px solid #5b21b6;
padding-bottom: 8px;
}
.toc-entry {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dotted #cbd5e1;
font-size: 10pt;
}
.toc-entry .toc-num { font-weight: 600; color: #5b21b6; min-width: 40px; }
/* Sections */
.section {
page-break-inside: avoid;
margin-bottom: 24px;
}
.section-header {
font-size: 14pt;
color: #5b21b6;
font-weight: 700;
margin: 30px 0 12px 0;
border-bottom: 2px solid #ddd6fe;
padding-bottom: 6px;
}
.section-body { margin-bottom: 16px; }
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0 16px 0;
font-size: 9pt;
}
th, td {
border: 1px solid #e2e8f0;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
th {
background: #f5f3ff;
color: #5b21b6;
font-weight: 600;
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
tr:nth-child(even) td { background: #faf5ff; }
/* Detail cards */
.policy-detail {
page-break-inside: avoid;
border: 1px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
}
.policy-detail-header {
background: #f5f3ff;
padding: 8px 12px;
font-weight: 700;
color: #5b21b6;
border-bottom: 1px solid #ddd6fe;
display: flex;
justify-content: space-between;
}
.policy-detail-body { padding: 0; }
.policy-detail-body table { margin: 0; }
.policy-detail-body th { width: 200px; }
/* Badges */
.badge {
display: inline-block;
padding: 1px 8px;
border-radius: 9999px;
font-size: 8pt;
font-weight: 600;
}
.badge-active { background: #dcfce7; color: #166534; }
.badge-draft { background: #f3f4f6; color: #374151; }
.badge-review { background: #fef9c3; color: #854d0e; }
.badge-critical { background: #fecaca; color: #991b1b; }
.badge-high { background: #fed7aa; color: #9a3412; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #f3f4f6; color: #4b5563; }
/* Principles */
.principle {
margin-bottom: 10px;
padding-left: 20px;
position: relative;
}
.principle::before {
content: '';
position: absolute;
left: 0;
top: 6px;
width: 10px;
height: 10px;
background: #7c3aed;
border-radius: 50%;
}
.principle strong { color: #5b21b6; }
/* Score */
.score-box {
display: inline-block;
padding: 4px 16px;
border-radius: 8px;
font-size: 18pt;
font-weight: 700;
margin-right: 12px;
}
.score-excellent { background: #dcfce7; color: #166534; }
.score-good { background: #dbeafe; color: #1e40af; }
.score-needs-work { background: #fef3c7; color: #92400e; }
.score-poor { background: #fecaca; color: #991b1b; }
/* Footer */
.page-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px 18mm;
font-size: 7.5pt;
color: #94a3b8;
display: flex;
justify-content: space-between;
border-top: 1px solid #e2e8f0;
}
/* Print */
@media print {
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
.page-break { page-break-after: always; }
}
</style>
</head>
<body>
`
// =========================================================================
// Section 0: Cover Page
// =========================================================================
html += `
<div class="cover">
<h1>Loeschkonzept</h1>
<div class="subtitle">gemaess Art. 5 Abs. 1 lit. e, Art. 17, Art. 30 DSGVO</div>
<div class="org-info">
<div><span class="label">Organisation:</span> ${escHtml(orgName)}</div>
${orgHeader.industry ? `<div><span class="label">Branche:</span> ${escHtml(orgHeader.industry)}</div>` : ''}
${orgHeader.dpoName ? `<div><span class="label">Datenschutzbeauftragter:</span> ${escHtml(orgHeader.dpoName)}</div>` : ''}
${orgHeader.dpoContact ? `<div><span class="label">DSB-Kontakt:</span> ${escHtml(orgHeader.dpoContact)}</div>` : ''}
${orgHeader.responsiblePerson ? `<div><span class="label">Verantwortlicher:</span> ${escHtml(orgHeader.responsiblePerson)}</div>` : ''}
${orgHeader.employeeCount ? `<div><span class="label">Mitarbeiter:</span> ${escHtml(orgHeader.employeeCount)}</div>` : ''}
${orgHeader.locations.length > 0 ? `<div><span class="label">Standorte:</span> ${escHtml(orgHeader.locations.join(', '))}</div>` : ''}
</div>
<div class="legal-ref">
Version ${escHtml(orgHeader.loeschkonzeptVersion)} | Stand: ${today}<br/>
Letzte Pruefung: ${formatDateDE(orgHeader.lastReviewDate)} | Naechste Pruefung: ${formatDateDE(orgHeader.nextReviewDate)}<br/>
Pruefintervall: ${escHtml(orgHeader.reviewInterval)}
</div>
</div>
`
// =========================================================================
// Table of Contents
// =========================================================================
const sections = [
'Ziel und Zweck',
'Geltungsbereich',
'Grundprinzipien der Datenspeicherung',
'Loeschregeln-Uebersicht',
'Detaillierte Loeschregeln',
'VVT-Verknuepfung',
'Legal Hold Verfahren',
'Verantwortlichkeiten',
'Pruef- und Revisionszyklus',
'Compliance-Status',
'Aenderungshistorie',
]
html += `
<div class="toc">
<h2>Inhaltsverzeichnis</h2>
${sections.map((s, i) => `<div class="toc-entry"><span><span class="toc-num">${i + 1}.</span> ${escHtml(s)}</span></div>`).join('\n ')}
</div>
`
// =========================================================================
// Section 1: Ziel und Zweck
// =========================================================================
html += `
<div class="section">
<div class="section-header">1. Ziel und Zweck</div>
<div class="section-body">
<p>Dieses Loeschkonzept definiert die systematischen Regeln und Verfahren fuer die Loeschung
personenbezogener Daten bei <strong>${escHtml(orgName)}</strong>. Es dient der Umsetzung
folgender DSGVO-Anforderungen:</p>
<table>
<tr><th>Rechtsgrundlage</th><th>Inhalt</th></tr>
<tr><td><strong>Art. 5 Abs. 1 lit. e DSGVO</strong></td><td>Grundsatz der Speicherbegrenzung — personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke der Verarbeitung erforderlich ist.</td></tr>
<tr><td><strong>Art. 17 DSGVO</strong></td><td>Recht auf Loeschung (&bdquo;Recht auf Vergessenwerden&ldquo;) — Betroffene haben das Recht, die Loeschung ihrer Daten zu verlangen.</td></tr>
<tr><td><strong>Art. 30 DSGVO</strong></td><td>Verzeichnis von Verarbeitungstaetigkeiten — vorgesehene Fristen fuer die Loeschung der verschiedenen Datenkategorien muessen dokumentiert werden.</td></tr>
</table>
<p>Das Loeschkonzept ist fester Bestandteil des Datenschutz-Managementsystems und wird
regelmaessig ueberprueft und aktualisiert.</p>
</div>
</div>
`
// =========================================================================
// Section 2: Geltungsbereich
// =========================================================================
const storageListHtml = allStorageLocations.size > 0
? Array.from(allStorageLocations).map(s => `<li>${escHtml(s)}</li>`).join('')
: '<li><em>Keine Speicherorte dokumentiert</em></li>'
html += `
<div class="section">
<div class="section-header">2. Geltungsbereich</div>
<div class="section-body">
<p>Dieses Loeschkonzept gilt fuer alle personenbezogenen Daten, die von <strong>${escHtml(orgName)}</strong>
verarbeitet werden. Es umfasst <strong>${activePolicies.length}</strong> Loeschregeln fuer folgende Systeme und Speicherorte:</p>
<ul style="margin: 8px 0 8px 24px;">
${storageListHtml}
</ul>
<p>Saemtliche Verarbeitungstaetigkeiten, die im Verzeichnis von Verarbeitungstaetigkeiten (VVT)
erfasst sind, werden durch dieses Loeschkonzept abgedeckt.</p>
</div>
</div>
`
// =========================================================================
// Section 3: Grundprinzipien
// =========================================================================
html += `
<div class="section">
<div class="section-header">3. Grundprinzipien der Datenspeicherung</div>
<div class="section-body">
<div class="principle"><strong>Speicherbegrenzung:</strong> Personenbezogene Daten werden nur so lange gespeichert, wie es fuer den jeweiligen Verarbeitungszweck erforderlich ist (Art. 5 Abs. 1 lit. e DSGVO).</div>
<div class="principle"><strong>3-Level-Loeschlogik:</strong> Die Loeschung folgt einer dreistufigen Priorisierung: (1) Zweckende, (2) gesetzliche Aufbewahrungspflichten, (3) Legal Hold — jeweils mit der laengsten Frist als massgeblich.</div>
<div class="principle"><strong>Dokumentationspflicht:</strong> Jede Loeschregel ist dokumentiert mit Rechtsgrundlage, Frist, Loeschmethode und Verantwortlichkeit.</div>
<div class="principle"><strong>Regelmaessige Ueberpruefung:</strong> Alle Loeschregeln werden im definierten Intervall ueberprueft und bei Bedarf angepasst.</div>
<div class="principle"><strong>Datenschutz durch Technikgestaltung:</strong> Loeschmechanismen werden moeglichst automatisiert, um menschliche Fehler zu minimieren (Art. 25 DSGVO).</div>
</div>
</div>
`
// =========================================================================
// Section 4: Loeschregeln-Uebersicht
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">4. Loeschregeln-Uebersicht</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt eine Uebersicht aller ${activePolicies.length} aktiven Loeschregeln:</p>
<table>
<tr>
<th>LF-Nr.</th>
<th>Datenobjekt</th>
<th>Loeschtrigger</th>
<th>Aufbewahrungsfrist</th>
<th>Loeschmethode</th>
<th>Status</th>
</tr>
`
for (const p of activePolicies) {
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
const method = DELETION_METHOD_LABELS[p.deletionMethod]
const statusLabel = STATUS_LABELS[p.status]
const statusClass = p.status === 'ACTIVE' ? 'badge-active' : p.status === 'REVIEW_NEEDED' ? 'badge-review' : 'badge-draft'
html += ` <tr>
<td>${escHtml(p.policyId)}</td>
<td>${escHtml(p.dataObjectName)}</td>
<td>${escHtml(trigger)}</td>
<td>${escHtml(duration)}</td>
<td>${escHtml(method)}</td>
<td><span class="badge ${statusClass}">${escHtml(statusLabel)}</span></td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 5: Detaillierte Loeschregeln
// =========================================================================
html += `
<div class="section">
<div class="section-header">5. Detaillierte Loeschregeln</div>
<div class="section-body">
`
for (const p of activePolicies) {
const trigger = TRIGGER_LABELS[getEffectiveDeletionTrigger(p)]
const duration = formatRetentionDuration(p.retentionDuration, p.retentionUnit)
const method = DELETION_METHOD_LABELS[p.deletionMethod]
const statusLabel = STATUS_LABELS[p.status]
const driverLabel = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.label || p.retentionDriver : '-'
const driverStatute = p.retentionDriver ? RETENTION_DRIVER_META[p.retentionDriver]?.statute || '' : ''
const locations = p.storageLocations.map(l => l.name || l.type).join(', ') || '-'
const responsible = [p.responsiblePerson, p.responsibleRole].filter(s => s.trim()).join(' / ') || '-'
const activeHolds = getActiveLegalHolds(p)
html += `
<div class="policy-detail">
<div class="policy-detail-header">
<span>${escHtml(p.policyId)}${escHtml(p.dataObjectName)}</span>
<span class="badge ${p.status === 'ACTIVE' ? 'badge-active' : 'badge-draft'}">${escHtml(statusLabel)}</span>
</div>
<div class="policy-detail-body">
<table>
<tr><th>Beschreibung</th><td>${escHtml(p.description || '-')}</td></tr>
<tr><th>Betroffenengruppen</th><td>${escHtml(p.affectedGroups.join(', ') || '-')}</td></tr>
<tr><th>Datenkategorien</th><td>${escHtml(p.dataCategories.join(', ') || '-')}</td></tr>
<tr><th>Verarbeitungszweck</th><td>${escHtml(p.primaryPurpose || '-')}</td></tr>
<tr><th>Loeschtrigger</th><td>${escHtml(trigger)}</td></tr>
<tr><th>Aufbewahrungstreiber</th><td>${escHtml(driverLabel)}${driverStatute ? ` (${escHtml(driverStatute)})` : ''}</td></tr>
<tr><th>Aufbewahrungsfrist</th><td>${escHtml(duration)}</td></tr>
<tr><th>Startereignis</th><td>${escHtml(p.startEvent || '-')}</td></tr>
<tr><th>Loeschmethode</th><td>${escHtml(method)}</td></tr>
<tr><th>Loeschmethode (Detail)</th><td>${escHtml(p.deletionMethodDetail || '-')}</td></tr>
<tr><th>Speicherorte</th><td>${escHtml(locations)}</td></tr>
<tr><th>Verantwortlich</th><td>${escHtml(responsible)}</td></tr>
<tr><th>Pruefintervall</th><td>${escHtml(REVIEW_INTERVAL_LABELS[p.reviewInterval] || p.reviewInterval)}</td></tr>
${activeHolds.length > 0 ? `<tr><th>Aktive Legal Holds</th><td>${activeHolds.map(h => `${escHtml(h.reason)} (seit ${formatDateDE(h.startDate)})`).join('<br/>')}</td></tr>` : ''}
</table>
</div>
</div>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 6: VVT-Verknuepfung
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">6. VVT-Verknuepfung</div>
<div class="section-body">
<p>Die folgende Tabelle zeigt die Verknuepfung zwischen Loeschregeln und Verarbeitungstaetigkeiten
im VVT (Art. 30 DSGVO):</p>
`
if (vvtRefs.length > 0) {
html += ` <table>
<tr><th>Loeschregel</th><th>LF-Nr.</th><th>VVT-Nr.</th><th>Verarbeitungstaetigkeit</th></tr>
`
for (const ref of vvtRefs) {
html += ` <tr>
<td>${escHtml(ref.policyName)}</td>
<td>${escHtml(ref.policyId)}</td>
<td>${escHtml(ref.vvtId)}</td>
<td>${escHtml(ref.vvtName)}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p><em>Noch keine VVT-Verknuepfungen dokumentiert. Verknuepfen Sie Ihre Loeschregeln
mit den entsprechenden Verarbeitungstaetigkeiten im Editor-Tab.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 7: Legal Hold Verfahren
// =========================================================================
html += `
<div class="section">
<div class="section-header">7. Legal Hold Verfahren</div>
<div class="section-body">
<p>Ein Legal Hold (Aufbewahrungspflicht aufgrund rechtlicher Verfahren) setzt die regulaere
Loeschung aus. Betroffene Daten duerfen trotz abgelaufener Loeschfrist nicht geloescht werden,
bis der Legal Hold aufgehoben wird.</p>
<p><strong>Verfahrensschritte:</strong></p>
<ol style="margin: 8px 0 8px 24px;">
<li>Rechtsabteilung/DSB identifiziert betroffene Datenkategorien</li>
<li>Legal Hold wird im System aktiviert (Status: Aktiv)</li>
<li>Automatische Loeschung wird fuer betroffene Policies ausgesetzt</li>
<li>Regelmaessige Pruefung, ob der Legal Hold noch erforderlich ist</li>
<li>Nach Aufhebung: Regulaere Loeschfristen greifen wieder</li>
</ol>
`
if (allActiveLegalHolds.length > 0) {
html += ` <p><strong>Aktuell aktive Legal Holds (${allActiveLegalHolds.length}):</strong></p>
<table>
<tr><th>Datenobjekt</th><th>Grund</th><th>Rechtsgrundlage</th><th>Seit</th><th>Voraussichtlich bis</th></tr>
`
for (const { policy, hold } of allActiveLegalHolds) {
html += ` <tr>
<td>${escHtml(policy)}</td>
<td>${escHtml(hold.reason)}</td>
<td>${escHtml(hold.legalBasis)}</td>
<td>${formatDateDE(hold.startDate)}</td>
<td>${hold.expectedEndDate ? formatDateDE(hold.expectedEndDate) : 'Unbefristet'}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p><em>Derzeit sind keine aktiven Legal Holds vorhanden.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 8: Verantwortlichkeiten
// =========================================================================
html += `
<div class="section">
<div class="section-header">8. Verantwortlichkeiten</div>
<div class="section-body">
<p>Die folgende Rollenmatrix zeigt, welche Organisationseinheiten fuer welche Datenobjekte
die Loeschverantwortung tragen:</p>
<table>
<tr><th>Rolle / Verantwortlich</th><th>Datenobjekte</th><th>Anzahl</th></tr>
`
for (const [role, objects] of roleMap.entries()) {
html += ` <tr>
<td>${escHtml(role)}</td>
<td>${objects.map(o => escHtml(o)).join(', ')}</td>
<td>${objects.length}</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Section 9: Pruef- und Revisionszyklus
// =========================================================================
html += `
<div class="section">
<div class="section-header">9. Pruef- und Revisionszyklus</div>
<div class="section-body">
<table>
<tr><th>Eigenschaft</th><th>Wert</th></tr>
<tr><td>Aktuelles Pruefintervall</td><td>${escHtml(orgHeader.reviewInterval)}</td></tr>
<tr><td>Letzte Pruefung</td><td>${formatDateDE(orgHeader.lastReviewDate)}</td></tr>
<tr><td>Naechste Pruefung</td><td>${formatDateDE(orgHeader.nextReviewDate)}</td></tr>
<tr><td>Aktuelle Version</td><td>${escHtml(orgHeader.loeschkonzeptVersion)}</td></tr>
</table>
<p style="margin-top: 8px;">Bei jeder Pruefung wird das Loeschkonzept auf folgende Punkte ueberprueft:</p>
<ul style="margin: 8px 0 8px 24px;">
<li>Vollstaendigkeit aller Loeschregeln (neue Verarbeitungen erfasst?)</li>
<li>Aktualitaet der gesetzlichen Aufbewahrungsfristen</li>
<li>Wirksamkeit der technischen Loeschmechanismen</li>
<li>Einhaltung der definierten Loeschfristen</li>
<li>Angemessenheit der Verantwortlichkeiten</li>
</ul>
</div>
</div>
`
// =========================================================================
// Section 10: Compliance-Status
// =========================================================================
html += `
<div class="section page-break">
<div class="section-header">10. Compliance-Status</div>
<div class="section-body">
`
if (complianceResult) {
const scoreClass = complianceResult.score >= 90 ? 'score-excellent'
: complianceResult.score >= 75 ? 'score-good'
: complianceResult.score >= 50 ? 'score-needs-work'
: 'score-poor'
const scoreLabel = complianceResult.score >= 90 ? 'Ausgezeichnet'
: complianceResult.score >= 75 ? 'Gut'
: complianceResult.score >= 50 ? 'Verbesserungswuerdig'
: 'Mangelhaft'
html += ` <p><span class="score-box ${scoreClass}">${complianceResult.score}/100</span> ${escHtml(scoreLabel)}</p>
<table style="margin-top: 12px;">
<tr><th>Kennzahl</th><th>Wert</th></tr>
<tr><td>Gepruefte Policies</td><td>${complianceResult.stats.total}</td></tr>
<tr><td>Bestanden</td><td>${complianceResult.stats.passed}</td></tr>
<tr><td>Beanstandungen</td><td>${complianceResult.stats.failed}</td></tr>
</table>
`
if (complianceResult.issues.length > 0) {
html += ` <p style="margin-top: 12px;"><strong>Befunde nach Schweregrad:</strong></p>
<table>
<tr><th>Schweregrad</th><th>Anzahl</th><th>Befunde</th></tr>
`
const severityOrder: ComplianceIssueSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']
for (const sev of severityOrder) {
const count = complianceResult.stats.bySeverity[sev]
if (count === 0) continue
const issuesForSev = complianceResult.issues.filter(i => i.severity === sev)
html += ` <tr>
<td><span class="badge badge-${sev.toLowerCase()}" style="color: ${SEVERITY_COLORS[sev]}">${SEVERITY_LABELS_DE[sev]}</span></td>
<td>${count}</td>
<td>${issuesForSev.map(i => escHtml(i.title)).join('; ')}</td>
</tr>
`
}
html += ` </table>
`
} else {
html += ` <p style="margin-top: 8px;"><em>Keine Beanstandungen. Alle Policies sind konform.</em></p>
`
}
} else {
html += ` <p><em>Compliance-Check wurde noch nicht ausgefuehrt. Fuehren Sie den Check im
Export-Tab durch, um den Status in das Dokument aufzunehmen.</em></p>
`
}
html += ` </div>
</div>
`
// =========================================================================
// Section 11: Aenderungshistorie
// =========================================================================
html += `
<div class="section">
<div class="section-header">11. Aenderungshistorie</div>
<div class="section-body">
<table>
<tr><th>Version</th><th>Datum</th><th>Autor</th><th>Aenderungen</th></tr>
`
if (revisions.length > 0) {
for (const rev of revisions) {
html += ` <tr>
<td>${escHtml(rev.version)}</td>
<td>${formatDateDE(rev.date)}</td>
<td>${escHtml(rev.author)}</td>
<td>${escHtml(rev.changes)}</td>
</tr>
`
}
} else {
html += ` <tr>
<td>${escHtml(orgHeader.loeschkonzeptVersion)}</td>
<td>${today}</td>
<td>${escHtml(orgHeader.dpoName || orgHeader.responsiblePerson || '-')}</td>
<td>Erstversion des Loeschkonzepts</td>
</tr>
`
}
html += ` </table>
</div>
</div>
`
// =========================================================================
// Footer
// =========================================================================
html += `
<div class="page-footer">
<span>Loeschkonzept — ${escHtml(orgName)}</span>
<span>Stand: ${today} | Version ${escHtml(orgHeader.loeschkonzeptVersion)}</span>
</div>
</body>
</html>`
return html
}
// =============================================================================
// INTERNAL HELPERS
// =============================================================================
function escHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function formatDateDE(dateStr: string | null | undefined): string {
if (!dateStr) return '-'
try {
const date = new Date(dateStr)
if (isNaN(date.getTime())) return '-'
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
} catch {
return '-'
}
}

View File

@@ -1,6 +1,6 @@
// =============================================================================
// Loeschfristen Module - Profiling Wizard
// 4-Step Profiling (15 Fragen) zur Generierung von Baseline-Loeschrichtlinien
// 4-Step Profiling (16 Fragen) zur Generierung von Baseline-Loeschrichtlinien
// =============================================================================
import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
@@ -42,7 +42,7 @@ export interface ProfilingResult {
}
// =============================================================================
// PROFILING STEPS (4 Steps, 15 Questions)
// PROFILING STEPS (4 Steps, 16 Questions)
// =============================================================================
export const PROFILING_STEPS: ProfilingStep[] = [
@@ -163,7 +163,7 @@ export const PROFILING_STEPS: ProfilingStep[] = [
},
// =========================================================================
// Step 3: Systeme (3 Fragen)
// Step 3: Systeme (4 Fragen)
// =========================================================================
{
id: 'systems',
@@ -194,6 +194,14 @@ export const PROFILING_STEPS: ProfilingStep[] = [
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,
},
],
},
@@ -340,6 +348,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('zeiterfassung')
matchedTemplateIds.add('bewerbungsunterlagen')
matchedTemplateIds.add('krankmeldungen')
matchedTemplateIds.add('schulungsnachweise')
}
// -------------------------------------------------------------------------
@@ -358,6 +367,8 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('vertraege')
matchedTemplateIds.add('geschaeftsbriefe')
matchedTemplateIds.add('kundenstammdaten')
matchedTemplateIds.add('kundenreklamationen')
matchedTemplateIds.add('lieferantenbewertungen')
}
// -------------------------------------------------------------------------
@@ -367,6 +378,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
matchedTemplateIds.add('newsletter-einwilligungen')
matchedTemplateIds.add('crm-kontakthistorie')
matchedTemplateIds.add('cookie-consent-logs')
matchedTemplateIds.add('social-media-daten')
}
// -------------------------------------------------------------------------
@@ -384,6 +396,20 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
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)
// -------------------------------------------------------------------------
@@ -405,6 +431,7 @@ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): Profili
if (getBool('special-gesundheit')) {
// Ensure krankmeldungen is included even without full HR data
matchedTemplateIds.add('krankmeldungen')
matchedTemplateIds.add('betriebsarzt-doku')
}
// -------------------------------------------------------------------------

View File

@@ -103,6 +103,21 @@ export interface VVTActivity {
owner: string
createdAt: string
updatedAt: string
// Library refs (optional, parallel to freetext)
purposeRefs?: string[]
legalBasisRefs?: string[]
dataSubjectRefs?: string[]
dataCategoryRefs?: string[]
recipientRefs?: string[]
retentionRuleRef?: string
transferMechanismRefs?: string[]
tomRefs?: string[]
linkedLoeschfristenIds?: string[]
linkedTomMeasureIds?: string[]
sourceTemplateId?: string
riskScore?: number
art30Completeness?: VVTCompleteness
}
// Processor-Record (Art. 30 Abs. 2)
@@ -186,6 +201,82 @@ export const ART9_CATEGORIES: string[] = [
'CRIMINAL_DATA',
]
// =============================================================================
// LIBRARY TYPES (Master Libraries)
// =============================================================================
export interface VVTLibraryItem {
id: string
labelDe: string
descriptionDe?: string
sortOrder?: number
}
export interface VVTDataCategory extends VVTLibraryItem {
parentId?: string | null
isArt9: boolean
isArt10?: boolean
riskWeight: number
defaultRetentionRule?: string
defaultLegalBasis?: string
children?: VVTDataCategory[]
}
export interface VVTLibRecipient extends VVTLibraryItem {
type: 'INTERNAL' | 'PROCESSOR' | 'CONTROLLER' | 'AUTHORITY'
isThirdCountry?: boolean
country?: string
}
export interface VVTLibLegalBasis extends VVTLibraryItem {
article: string
type: string
isArt9?: boolean
}
export interface VVTLibRetentionRule extends VVTLibraryItem {
legalBasis?: string
duration: number
durationUnit: 'DAYS' | 'MONTHS' | 'YEARS'
startEvent?: string
deletionProcedure?: string
}
export interface VVTLibTom extends VVTLibraryItem {
category: 'accessControl' | 'confidentiality' | 'integrity' | 'availability' | 'separation'
art32Reference?: string
}
export interface VVTProcessTemplate {
id: string
name: string
description?: string
businessFunction: BusinessFunction
purposeRefs: string[]
legalBasisRefs: string[]
dataSubjectRefs: string[]
dataCategoryRefs: string[]
recipientRefs: string[]
tomRefs: string[]
transferMechanismRefs: string[]
retentionRuleRef?: string
typicalSystems: string[]
protectionLevel: string
dpiaRequired: boolean
riskScore?: number
tags: string[]
isSystem: boolean
sortOrder: number
}
export interface VVTCompleteness {
score: number
missing: string[]
warnings: string[]
passed: number
total: number
}
// =============================================================================
// HELPER: Create empty activity
// =============================================================================

View File

@@ -1755,6 +1755,20 @@ class VVTActivityCreate(BaseModel):
next_review_at: Optional[datetime] = None
created_by: Optional[str] = None
dsfa_id: Optional[str] = None
# Library refs (optional, parallel to freetext)
purpose_refs: Optional[List[str]] = None
legal_basis_refs: Optional[List[str]] = None
data_subject_refs: Optional[List[str]] = None
data_category_refs: Optional[List[str]] = None
recipient_refs: Optional[List[str]] = None
retention_rule_ref: Optional[str] = None
transfer_mechanism_refs: Optional[List[str]] = None
tom_refs: Optional[List[str]] = None
source_template_id: Optional[str] = None
risk_score: Optional[int] = None
linked_loeschfristen_ids: Optional[List[str]] = None
linked_tom_measure_ids: Optional[List[str]] = None
art30_completeness: Optional[Dict[str, Any]] = None
class VVTActivityUpdate(BaseModel):
@@ -1783,6 +1797,20 @@ class VVTActivityUpdate(BaseModel):
next_review_at: Optional[datetime] = None
created_by: Optional[str] = None
dsfa_id: Optional[str] = None
# Library refs
purpose_refs: Optional[List[str]] = None
legal_basis_refs: Optional[List[str]] = None
data_subject_refs: Optional[List[str]] = None
data_category_refs: Optional[List[str]] = None
recipient_refs: Optional[List[str]] = None
retention_rule_ref: Optional[str] = None
transfer_mechanism_refs: Optional[List[str]] = None
tom_refs: Optional[List[str]] = None
source_template_id: Optional[str] = None
risk_score: Optional[int] = None
linked_loeschfristen_ids: Optional[List[str]] = None
linked_tom_measure_ids: Optional[List[str]] = None
art30_completeness: Optional[Dict[str, Any]] = None
class VVTActivityResponse(BaseModel):
@@ -1813,6 +1841,20 @@ class VVTActivityResponse(BaseModel):
next_review_at: Optional[datetime] = None
created_by: Optional[str] = None
dsfa_id: Optional[str] = None
# Library refs
purpose_refs: Optional[List[str]] = None
legal_basis_refs: Optional[List[str]] = None
data_subject_refs: Optional[List[str]] = None
data_category_refs: Optional[List[str]] = None
recipient_refs: Optional[List[str]] = None
retention_rule_ref: Optional[str] = None
transfer_mechanism_refs: Optional[List[str]] = None
tom_refs: Optional[List[str]] = None
source_template_id: Optional[str] = None
risk_score: Optional[int] = None
linked_loeschfristen_ids: Optional[List[str]] = None
linked_tom_measure_ids: Optional[List[str]] = None
art30_completeness: Optional[Dict[str, Any]] = None
created_at: datetime
updated_at: Optional[datetime] = None

View File

@@ -0,0 +1,427 @@
"""
FastAPI routes for VVT Master Libraries + Process Templates.
Library endpoints (read-only, global):
GET /vvt/libraries — Overview: all library types + counts
GET /vvt/libraries/data-subjects — Data subjects (filter: typical_for)
GET /vvt/libraries/data-categories — Hierarchical (filter: parent_id, is_art9, flat)
GET /vvt/libraries/recipients — Recipients (filter: type)
GET /vvt/libraries/legal-bases — Legal bases (filter: is_art9, type)
GET /vvt/libraries/retention-rules — Retention rules
GET /vvt/libraries/transfer-mechanisms — Transfer mechanisms
GET /vvt/libraries/purposes — Purposes (filter: typical_for)
GET /vvt/libraries/toms — TOMs (filter: category)
Template endpoints:
GET /vvt/templates — List templates (filter: business_function, search)
GET /vvt/templates/{id} — Single template with resolved labels
POST /vvt/templates/{id}/instantiate — Create VVT activity from template
"""
import logging
import uuid
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from classroom_engine.database import get_db
from ..db.vvt_library_models import (
VVTLibDataSubjectDB,
VVTLibDataCategoryDB,
VVTLibRecipientDB,
VVTLibLegalBasisDB,
VVTLibRetentionRuleDB,
VVTLibTransferMechanismDB,
VVTLibPurposeDB,
VVTLibTomDB,
VVTProcessTemplateDB,
)
from ..db.vvt_models import VVTActivityDB, VVTAuditLogDB
from .tenant_utils import get_tenant_id
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/vvt", tags=["compliance-vvt-libraries"])
# ============================================================================
# Helper: row → dict
# ============================================================================
def _row_to_dict(row, extra_fields=None):
"""Generic row → dict for library items."""
d = {
"id": row.id,
"label_de": row.label_de,
}
if hasattr(row, 'description_de') and row.description_de:
d["description_de"] = row.description_de
if hasattr(row, 'sort_order'):
d["sort_order"] = row.sort_order
if extra_fields:
for f in extra_fields:
if hasattr(row, f):
val = getattr(row, f)
if val is not None:
d[f] = val
return d
# ============================================================================
# Library Overview
# ============================================================================
@router.get("/libraries")
async def get_libraries_overview(db: Session = Depends(get_db)):
"""Overview of all library types with item counts."""
return {
"libraries": [
{"type": "data-subjects", "count": db.query(VVTLibDataSubjectDB).count()},
{"type": "data-categories", "count": db.query(VVTLibDataCategoryDB).count()},
{"type": "recipients", "count": db.query(VVTLibRecipientDB).count()},
{"type": "legal-bases", "count": db.query(VVTLibLegalBasisDB).count()},
{"type": "retention-rules", "count": db.query(VVTLibRetentionRuleDB).count()},
{"type": "transfer-mechanisms", "count": db.query(VVTLibTransferMechanismDB).count()},
{"type": "purposes", "count": db.query(VVTLibPurposeDB).count()},
{"type": "toms", "count": db.query(VVTLibTomDB).count()},
]
}
# ============================================================================
# Data Subjects
# ============================================================================
@router.get("/libraries/data-subjects")
async def list_data_subjects(
typical_for: Optional[str] = Query(None, description="Filter by business function"),
db: Session = Depends(get_db),
):
query = db.query(VVTLibDataSubjectDB).order_by(VVTLibDataSubjectDB.sort_order)
rows = query.all()
items = [_row_to_dict(r, ["art9_relevant", "typical_for"]) for r in rows]
if typical_for:
items = [i for i in items if typical_for in (i.get("typical_for") or [])]
return items
# ============================================================================
# Data Categories (hierarchical)
# ============================================================================
@router.get("/libraries/data-categories")
async def list_data_categories(
flat: Optional[bool] = Query(False, description="Return flat list instead of tree"),
parent_id: Optional[str] = Query(None),
is_art9: Optional[bool] = Query(None),
db: Session = Depends(get_db),
):
query = db.query(VVTLibDataCategoryDB).order_by(VVTLibDataCategoryDB.sort_order)
if parent_id is not None:
query = query.filter(VVTLibDataCategoryDB.parent_id == parent_id)
if is_art9 is not None:
query = query.filter(VVTLibDataCategoryDB.is_art9 == is_art9)
rows = query.all()
extra = ["parent_id", "is_art9", "is_art10", "risk_weight", "default_retention_rule", "default_legal_basis"]
items = [_row_to_dict(r, extra) for r in rows]
if flat or parent_id is not None or is_art9 is not None:
return items
# Build tree
by_parent: dict = {}
for item in items:
pid = item.get("parent_id")
by_parent.setdefault(pid, []).append(item)
tree = []
for item in by_parent.get(None, []):
children = by_parent.get(item["id"], [])
if children:
item["children"] = children
tree.append(item)
return tree
# ============================================================================
# Recipients
# ============================================================================
@router.get("/libraries/recipients")
async def list_recipients(
type: Optional[str] = Query(None, description="INTERNAL, PROCESSOR, CONTROLLER, AUTHORITY"),
db: Session = Depends(get_db),
):
query = db.query(VVTLibRecipientDB).order_by(VVTLibRecipientDB.sort_order)
if type:
query = query.filter(VVTLibRecipientDB.type == type)
rows = query.all()
return [_row_to_dict(r, ["type", "is_third_country", "country"]) for r in rows]
# ============================================================================
# Legal Bases
# ============================================================================
@router.get("/libraries/legal-bases")
async def list_legal_bases(
is_art9: Optional[bool] = Query(None),
type: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
query = db.query(VVTLibLegalBasisDB).order_by(VVTLibLegalBasisDB.sort_order)
if is_art9 is not None:
query = query.filter(VVTLibLegalBasisDB.is_art9 == is_art9)
if type:
query = query.filter(VVTLibLegalBasisDB.type == type)
rows = query.all()
return [_row_to_dict(r, ["article", "type", "is_art9", "typical_national_law"]) for r in rows]
# ============================================================================
# Retention Rules
# ============================================================================
@router.get("/libraries/retention-rules")
async def list_retention_rules(db: Session = Depends(get_db)):
rows = db.query(VVTLibRetentionRuleDB).order_by(VVTLibRetentionRuleDB.sort_order).all()
return [_row_to_dict(r, ["legal_basis", "duration", "duration_unit", "start_event", "deletion_procedure"]) for r in rows]
# ============================================================================
# Transfer Mechanisms
# ============================================================================
@router.get("/libraries/transfer-mechanisms")
async def list_transfer_mechanisms(db: Session = Depends(get_db)):
rows = db.query(VVTLibTransferMechanismDB).order_by(VVTLibTransferMechanismDB.sort_order).all()
return [_row_to_dict(r, ["article", "requires_tia"]) for r in rows]
# ============================================================================
# Purposes
# ============================================================================
@router.get("/libraries/purposes")
async def list_purposes(
typical_for: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
rows = db.query(VVTLibPurposeDB).order_by(VVTLibPurposeDB.sort_order).all()
items = [_row_to_dict(r, ["typical_legal_basis", "typical_for"]) for r in rows]
if typical_for:
items = [i for i in items if typical_for in (i.get("typical_for") or [])]
return items
# ============================================================================
# TOMs
# ============================================================================
@router.get("/libraries/toms")
async def list_toms(
category: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
query = db.query(VVTLibTomDB).order_by(VVTLibTomDB.sort_order)
if category:
query = query.filter(VVTLibTomDB.category == category)
rows = query.all()
return [_row_to_dict(r, ["category", "art32_reference"]) for r in rows]
# ============================================================================
# Process Templates
# ============================================================================
def _template_to_dict(t: VVTProcessTemplateDB) -> dict:
return {
"id": t.id,
"name": t.name,
"description": t.description,
"business_function": t.business_function,
"purpose_refs": t.purpose_refs or [],
"legal_basis_refs": t.legal_basis_refs or [],
"data_subject_refs": t.data_subject_refs or [],
"data_category_refs": t.data_category_refs or [],
"recipient_refs": t.recipient_refs or [],
"tom_refs": t.tom_refs or [],
"transfer_mechanism_refs": t.transfer_mechanism_refs or [],
"retention_rule_ref": t.retention_rule_ref,
"typical_systems": t.typical_systems or [],
"protection_level": t.protection_level or "MEDIUM",
"dpia_required": t.dpia_required or False,
"risk_score": t.risk_score,
"tags": t.tags or [],
"is_system": t.is_system,
"sort_order": t.sort_order,
}
def _resolve_labels(template_dict: dict, db: Session) -> dict:
"""Resolve library IDs to labels within the template dict."""
resolvers = {
"purpose_refs": (VVTLibPurposeDB, "purpose_labels"),
"legal_basis_refs": (VVTLibLegalBasisDB, "legal_basis_labels"),
"data_subject_refs": (VVTLibDataSubjectDB, "data_subject_labels"),
"data_category_refs": (VVTLibDataCategoryDB, "data_category_labels"),
"recipient_refs": (VVTLibRecipientDB, "recipient_labels"),
"tom_refs": (VVTLibTomDB, "tom_labels"),
"transfer_mechanism_refs": (VVTLibTransferMechanismDB, "transfer_mechanism_labels"),
}
for refs_key, (model, labels_key) in resolvers.items():
ids = template_dict.get(refs_key) or []
if ids:
rows = db.query(model).filter(model.id.in_(ids)).all()
label_map = {r.id: r.label_de for r in rows}
template_dict[labels_key] = {rid: label_map.get(rid, rid) for rid in ids}
# Resolve single retention rule
rr = template_dict.get("retention_rule_ref")
if rr:
row = db.query(VVTLibRetentionRuleDB).filter(VVTLibRetentionRuleDB.id == rr).first()
if row:
template_dict["retention_rule_label"] = row.label_de
return template_dict
@router.get("/templates")
async def list_templates(
business_function: Optional[str] = Query(None),
search: Optional[str] = Query(None),
db: Session = Depends(get_db),
):
"""List process templates (system + tenant)."""
query = db.query(VVTProcessTemplateDB).order_by(VVTProcessTemplateDB.sort_order)
if business_function:
query = query.filter(VVTProcessTemplateDB.business_function == business_function)
if search:
term = f"%{search}%"
query = query.filter(
(VVTProcessTemplateDB.name.ilike(term)) |
(VVTProcessTemplateDB.description.ilike(term))
)
templates = query.all()
return [_template_to_dict(t) for t in templates]
@router.get("/templates/{template_id}")
async def get_template(
template_id: str,
db: Session = Depends(get_db),
):
"""Get a single template with resolved library labels."""
t = db.query(VVTProcessTemplateDB).filter(VVTProcessTemplateDB.id == template_id).first()
if not t:
raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found")
result = _template_to_dict(t)
return _resolve_labels(result, db)
@router.post("/templates/{template_id}/instantiate", status_code=201)
async def instantiate_template(
template_id: str,
http_request: Request,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Create a new VVT activity from a process template."""
t = db.query(VVTProcessTemplateDB).filter(VVTProcessTemplateDB.id == template_id).first()
if not t:
raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found")
# Generate unique VVT-ID
count = db.query(VVTActivityDB).filter(VVTActivityDB.tenant_id == tid).count()
vvt_id = f"VVT-{count + 1:04d}"
# Resolve library IDs to freetext labels for backward-compat fields
purpose_labels = _resolve_ids(db, VVTLibPurposeDB, t.purpose_refs or [])
legal_labels = _resolve_ids(db, VVTLibLegalBasisDB, t.legal_basis_refs or [])
subject_labels = _resolve_ids(db, VVTLibDataSubjectDB, t.data_subject_refs or [])
category_labels = _resolve_ids(db, VVTLibDataCategoryDB, t.data_category_refs or [])
recipient_labels = _resolve_ids(db, VVTLibRecipientDB, t.recipient_refs or [])
# Resolve retention rule
retention_period = {}
if t.retention_rule_ref:
rr = db.query(VVTLibRetentionRuleDB).filter(VVTLibRetentionRuleDB.id == t.retention_rule_ref).first()
if rr:
retention_period = {
"description": rr.label_de,
"legalBasis": rr.legal_basis or "",
"deletionProcedure": rr.deletion_procedure or "",
"duration": rr.duration,
"durationUnit": rr.duration_unit,
}
# Build structured TOMs from tom_refs
structured_toms = {"accessControl": [], "confidentiality": [], "integrity": [], "availability": [], "separation": []}
if t.tom_refs:
tom_rows = db.query(VVTLibTomDB).filter(VVTLibTomDB.id.in_(t.tom_refs)).all()
for tr in tom_rows:
cat = tr.category
if cat in structured_toms:
structured_toms[cat].append(tr.label_de)
act = VVTActivityDB(
tenant_id=tid,
vvt_id=vvt_id,
name=t.name,
description=t.description or "",
purposes=purpose_labels,
legal_bases=[{"type": lid, "description": lbl} for lid, lbl in zip(t.legal_basis_refs or [], legal_labels)],
data_subject_categories=subject_labels,
personal_data_categories=category_labels,
recipient_categories=[{"type": "unknown", "name": lbl} for lbl in recipient_labels],
retention_period=retention_period,
business_function=t.business_function,
systems=[{"systemId": s, "name": s} for s in (t.typical_systems or [])],
protection_level=t.protection_level or "MEDIUM",
dpia_required=t.dpia_required or False,
structured_toms=structured_toms,
status="DRAFT",
created_by=http_request.headers.get("X-User-ID", "system"),
# Library refs
purpose_refs=t.purpose_refs,
legal_basis_refs=t.legal_basis_refs,
data_subject_refs=t.data_subject_refs,
data_category_refs=t.data_category_refs,
recipient_refs=t.recipient_refs,
retention_rule_ref=t.retention_rule_ref,
transfer_mechanism_refs=t.transfer_mechanism_refs,
tom_refs=t.tom_refs,
source_template_id=t.id,
risk_score=t.risk_score,
)
db.add(act)
db.flush()
# Audit log
audit = VVTAuditLogDB(
tenant_id=tid,
action="CREATE",
entity_type="activity",
entity_id=act.id,
changed_by=http_request.headers.get("X-User-ID", "system"),
new_values={"vvt_id": vvt_id, "source_template_id": t.id, "name": t.name},
)
db.add(audit)
db.commit()
db.refresh(act)
# Return full response
from .vvt_routes import _activity_to_response
return _activity_to_response(act)
def _resolve_ids(db: Session, model, ids: list) -> list:
"""Resolve list of library IDs to list of label_de strings."""
if not ids:
return []
rows = db.query(model).filter(model.id.in_(ids)).all()
label_map = {r.id: r.label_de for r in rows}
return [label_map.get(i, i) for i in ids]

View File

@@ -174,6 +174,20 @@ def _activity_to_response(act: VVTActivityDB) -> VVTActivityResponse:
next_review_at=act.next_review_at,
created_by=act.created_by,
dsfa_id=str(act.dsfa_id) if act.dsfa_id else None,
# Library refs
purpose_refs=act.purpose_refs,
legal_basis_refs=act.legal_basis_refs,
data_subject_refs=act.data_subject_refs,
data_category_refs=act.data_category_refs,
recipient_refs=act.recipient_refs,
retention_rule_ref=act.retention_rule_ref,
transfer_mechanism_refs=act.transfer_mechanism_refs,
tom_refs=act.tom_refs,
source_template_id=act.source_template_id,
risk_score=act.risk_score,
linked_loeschfristen_ids=act.linked_loeschfristen_ids,
linked_tom_measure_ids=act.linked_tom_measure_ids,
art30_completeness=act.art30_completeness,
created_at=act.created_at,
updated_at=act.updated_at,
)
@@ -336,6 +350,107 @@ async def delete_activity(
return {"success": True, "message": f"Activity {activity_id} deleted"}
# ============================================================================
# Art. 30 Completeness Check
# ============================================================================
@router.get("/activities/{activity_id}/completeness")
async def get_activity_completeness(
activity_id: str,
tid: str = Depends(get_tenant_id),
db: Session = Depends(get_db),
):
"""Calculate Art. 30 completeness score for a VVT activity."""
act = db.query(VVTActivityDB).filter(
VVTActivityDB.id == activity_id,
VVTActivityDB.tenant_id == tid,
).first()
if not act:
raise HTTPException(status_code=404, detail=f"Activity {activity_id} not found")
return _calculate_completeness(act)
def _calculate_completeness(act: VVTActivityDB) -> dict:
"""Calculate Art. 30 completeness — required fields per DSGVO Art. 30 Abs. 1."""
missing = []
warnings = []
total_checks = 10
passed = 0
# 1. Name/Zweck
if act.name:
passed += 1
else:
missing.append("name")
# 2. Verarbeitungszwecke
has_purposes = bool(act.purposes) or bool(act.purpose_refs)
if has_purposes:
passed += 1
else:
missing.append("purposes")
# 3. Rechtsgrundlage
has_legal = bool(act.legal_bases) or bool(act.legal_basis_refs)
if has_legal:
passed += 1
else:
missing.append("legal_bases")
# 4. Betroffenenkategorien
has_subjects = bool(act.data_subject_categories) or bool(act.data_subject_refs)
if has_subjects:
passed += 1
else:
missing.append("data_subjects")
# 5. Datenkategorien
has_categories = bool(act.personal_data_categories) or bool(act.data_category_refs)
if has_categories:
passed += 1
else:
missing.append("data_categories")
# 6. Empfaenger
has_recipients = bool(act.recipient_categories) or bool(act.recipient_refs)
if has_recipients:
passed += 1
else:
missing.append("recipients")
# 7. Drittland-Uebermittlung (checked but not strictly required)
passed += 1 # always passes — no transfer is valid state
# 8. Loeschfristen
has_retention = bool(act.retention_period and act.retention_period.get('description')) or bool(act.retention_rule_ref)
if has_retention:
passed += 1
else:
missing.append("retention_period")
# 9. TOM-Beschreibung
has_tom = bool(act.tom_description) or bool(act.tom_refs) or bool(act.structured_toms)
if has_tom:
passed += 1
else:
missing.append("tom_description")
# 10. Verantwortlicher
if act.responsible:
passed += 1
else:
missing.append("responsible")
# Warnings
if act.dpia_required and not act.dsfa_id:
warnings.append("dpia_required_but_no_dsfa_linked")
if act.third_country_transfers and not act.transfer_mechanism_refs:
warnings.append("third_country_transfer_without_mechanism")
score = int((passed / total_checks) * 100)
return {"score": score, "missing": missing, "warnings": warnings, "passed": passed, "total": total_checks}
# ============================================================================
# Audit Log
# ============================================================================

View File

@@ -0,0 +1,164 @@
"""
SQLAlchemy models for VVT Master Libraries + Process Templates.
Tables (global, no tenant_id):
- vvt_lib_data_subjects
- vvt_lib_data_categories (hierarchical, self-referencing)
- vvt_lib_recipients
- vvt_lib_legal_bases
- vvt_lib_retention_rules
- vvt_lib_transfer_mechanisms
- vvt_lib_purposes
- vvt_lib_toms
Tenant-scoped:
- vvt_process_templates (system + tenant-specific)
"""
from datetime import datetime
from sqlalchemy import (
Column, String, Text, Boolean, Integer, DateTime, JSON, Index,
ForeignKey,
)
from sqlalchemy.dialects.postgresql import UUID
from classroom_engine.database import Base
class VVTLibDataSubjectDB(Base):
__tablename__ = 'vvt_lib_data_subjects'
id = Column(String(50), primary_key=True)
label_de = Column(String(200), nullable=False)
description_de = Column(Text)
art9_relevant = Column(Boolean, default=False)
typical_for = Column(JSON, default=list)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibDataCategoryDB(Base):
__tablename__ = 'vvt_lib_data_categories'
id = Column(String(50), primary_key=True)
parent_id = Column(String(50), ForeignKey('vvt_lib_data_categories.id', ondelete='SET NULL'), nullable=True)
label_de = Column(String(200), nullable=False)
description_de = Column(Text)
is_art9 = Column(Boolean, default=False)
is_art10 = Column(Boolean, default=False)
risk_weight = Column(Integer, default=1)
default_retention_rule = Column(String(50))
default_legal_basis = Column(String(50))
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibRecipientDB(Base):
__tablename__ = 'vvt_lib_recipients'
id = Column(String(50), primary_key=True)
type = Column(String(20), nullable=False)
label_de = Column(String(200), nullable=False)
description_de = Column(Text)
is_third_country = Column(Boolean, default=False)
country = Column(String(5))
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibLegalBasisDB(Base):
__tablename__ = 'vvt_lib_legal_bases'
id = Column(String(50), primary_key=True)
article = Column(String(50), nullable=False)
type = Column(String(30), nullable=False)
label_de = Column(String(300), nullable=False)
description_de = Column(Text)
is_art9 = Column(Boolean, default=False)
typical_national_law = Column(String(100))
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibRetentionRuleDB(Base):
__tablename__ = 'vvt_lib_retention_rules'
id = Column(String(50), primary_key=True)
label_de = Column(String(300), nullable=False)
description_de = Column(Text)
legal_basis = Column(String(200))
duration = Column(Integer, nullable=False)
duration_unit = Column(String(10), nullable=False)
start_event = Column(String(200))
deletion_procedure = Column(String(500))
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibTransferMechanismDB(Base):
__tablename__ = 'vvt_lib_transfer_mechanisms'
id = Column(String(50), primary_key=True)
label_de = Column(String(300), nullable=False)
description_de = Column(Text)
article = Column(String(50))
requires_tia = Column(Boolean, default=False)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibPurposeDB(Base):
__tablename__ = 'vvt_lib_purposes'
id = Column(String(50), primary_key=True)
label_de = Column(String(300), nullable=False)
description_de = Column(Text)
typical_legal_basis = Column(String(50))
typical_for = Column(JSON, default=list)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTLibTomDB(Base):
__tablename__ = 'vvt_lib_toms'
id = Column(String(50), primary_key=True)
category = Column(String(30), nullable=False)
label_de = Column(String(300), nullable=False)
description_de = Column(Text)
art32_reference = Column(String(100))
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
class VVTProcessTemplateDB(Base):
__tablename__ = 'vvt_process_templates'
id = Column(String(80), primary_key=True)
name = Column(String(300), nullable=False)
description = Column(Text)
business_function = Column(String(50))
purpose_refs = Column(JSON, default=list)
legal_basis_refs = Column(JSON, default=list)
data_subject_refs = Column(JSON, default=list)
data_category_refs = Column(JSON, default=list)
recipient_refs = Column(JSON, default=list)
tom_refs = Column(JSON, default=list)
transfer_mechanism_refs = Column(JSON, default=list)
retention_rule_ref = Column(String(50))
typical_systems = Column(JSON, default=list)
protection_level = Column(String(10), default='MEDIUM')
dpia_required = Column(Boolean, default=False)
risk_score = Column(Integer)
tags = Column(JSON, default=list)
is_system = Column(Boolean, default=True)
tenant_id = Column(UUID(as_uuid=True), nullable=True)
sort_order = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column(DateTime(timezone=True), default=datetime.utcnow, onupdate=datetime.utcnow)
__table_args__ = (
Index('idx_vvt_process_templates_bf', 'business_function'),
Index('idx_vvt_process_templates_system', 'is_system'),
)

View File

@@ -79,6 +79,26 @@ class VVTActivityDB(Base):
next_review_at = Column(DateTime(timezone=True), nullable=True)
created_by = Column(String(200), default='system')
dsfa_id = Column(UUID(as_uuid=True), nullable=True)
# Library refs (Phase 1 — parallel to freetext fields)
purpose_refs = Column(JSON, nullable=True)
legal_basis_refs = Column(JSON, nullable=True)
data_subject_refs = Column(JSON, nullable=True)
data_category_refs = Column(JSON, nullable=True)
recipient_refs = Column(JSON, nullable=True)
retention_rule_ref = Column(String(50), nullable=True)
transfer_mechanism_refs = Column(JSON, nullable=True)
tom_refs = Column(JSON, nullable=True)
# Cross-module links
linked_loeschfristen_ids = Column(JSON, nullable=True)
linked_tom_measure_ids = Column(JSON, nullable=True)
# Template + risk
source_template_id = Column(String(80), nullable=True)
risk_score = Column(Integer, nullable=True)
art30_completeness = Column(JSON, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@@ -0,0 +1,105 @@
-- Migration 064: VVT Master Libraries — 8 global reference tables
-- These are shared across all tenants (no tenant_id).
BEGIN;
-- 1. Data Subjects (Betroffenenkategorien)
CREATE TABLE IF NOT EXISTS vvt_lib_data_subjects (
id VARCHAR(50) PRIMARY KEY,
label_de VARCHAR(200) NOT NULL,
description_de TEXT,
art9_relevant BOOLEAN DEFAULT FALSE,
typical_for JSONB DEFAULT '[]'::jsonb,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 2. Data Categories (Datenkategorien — hierarchisch)
CREATE TABLE IF NOT EXISTS vvt_lib_data_categories (
id VARCHAR(50) PRIMARY KEY,
parent_id VARCHAR(50) REFERENCES vvt_lib_data_categories(id) ON DELETE SET NULL,
label_de VARCHAR(200) NOT NULL,
description_de TEXT,
is_art9 BOOLEAN DEFAULT FALSE,
is_art10 BOOLEAN DEFAULT FALSE,
risk_weight INTEGER DEFAULT 1 CHECK (risk_weight BETWEEN 1 AND 5),
default_retention_rule VARCHAR(50),
default_legal_basis VARCHAR(50),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vvt_lib_data_categories_parent ON vvt_lib_data_categories(parent_id);
-- 3. Recipients (Empfaengerkategorien)
CREATE TABLE IF NOT EXISTS vvt_lib_recipients (
id VARCHAR(50) PRIMARY KEY,
type VARCHAR(20) NOT NULL CHECK (type IN ('INTERNAL', 'PROCESSOR', 'CONTROLLER', 'AUTHORITY')),
label_de VARCHAR(200) NOT NULL,
description_de TEXT,
is_third_country BOOLEAN DEFAULT FALSE,
country VARCHAR(5),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 4. Legal Bases (Rechtsgrundlagen)
CREATE TABLE IF NOT EXISTS vvt_lib_legal_bases (
id VARCHAR(50) PRIMARY KEY,
article VARCHAR(50) NOT NULL,
type VARCHAR(30) NOT NULL CHECK (type IN ('CONSENT', 'CONTRACT', 'LEGAL_OBLIGATION', 'VITAL_INTEREST', 'PUBLIC_TASK', 'LEGITIMATE_INTEREST', 'ART9', 'NATIONAL')),
label_de VARCHAR(300) NOT NULL,
description_de TEXT,
is_art9 BOOLEAN DEFAULT FALSE,
typical_national_law VARCHAR(100),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 5. Retention Rules (Aufbewahrungsfristen)
CREATE TABLE IF NOT EXISTS vvt_lib_retention_rules (
id VARCHAR(50) PRIMARY KEY,
label_de VARCHAR(300) NOT NULL,
description_de TEXT,
legal_basis VARCHAR(200),
duration INTEGER NOT NULL,
duration_unit VARCHAR(10) NOT NULL CHECK (duration_unit IN ('DAYS', 'MONTHS', 'YEARS')),
start_event VARCHAR(200),
deletion_procedure VARCHAR(500),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 6. Transfer Mechanisms (Uebermittlungsmechanismen)
CREATE TABLE IF NOT EXISTS vvt_lib_transfer_mechanisms (
id VARCHAR(50) PRIMARY KEY,
label_de VARCHAR(300) NOT NULL,
description_de TEXT,
article VARCHAR(50),
requires_tia BOOLEAN DEFAULT FALSE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 7. Purposes (Verarbeitungszwecke)
CREATE TABLE IF NOT EXISTS vvt_lib_purposes (
id VARCHAR(50) PRIMARY KEY,
label_de VARCHAR(300) NOT NULL,
description_de TEXT,
typical_legal_basis VARCHAR(50),
typical_for JSONB DEFAULT '[]'::jsonb,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 8. TOMs (Technisch-Organisatorische Massnahmen)
CREATE TABLE IF NOT EXISTS vvt_lib_toms (
id VARCHAR(50) PRIMARY KEY,
category VARCHAR(30) NOT NULL CHECK (category IN ('accessControl', 'confidentiality', 'integrity', 'availability', 'separation')),
label_de VARCHAR(300) NOT NULL,
description_de TEXT,
art32_reference VARCHAR(100),
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMIT;

View File

@@ -0,0 +1,200 @@
-- Migration 065: VVT Library Seed Data (~150 entries)
-- All content self-authored, MIT-compatible.
BEGIN;
-- =============================================================================
-- Data Subjects (15)
-- =============================================================================
INSERT INTO vvt_lib_data_subjects (id, label_de, description_de, art9_relevant, typical_for, sort_order) VALUES
('EMPLOYEES', 'Beschaeftigte', 'Aktuelle Mitarbeiterinnen und Mitarbeiter', FALSE, '["hr","it_operations"]', 1),
('APPLICANTS', 'Bewerber', 'Stellenbewerberinnen und -bewerber', FALSE, '["hr"]', 2),
('CUSTOMERS', 'Kunden', 'Aktive Kundinnen und Kunden', FALSE, '["sales_crm","support","finance"]', 3),
('PROSPECTIVE_CUSTOMERS', 'Interessenten', 'Potenzielle Kundinnen und Kunden', FALSE, '["marketing","sales_crm"]', 4),
('SUPPLIERS', 'Lieferanten', 'Geschaeftspartner als Lieferanten', FALSE, '["finance"]', 5),
('BUSINESS_PARTNERS', 'Geschaeftspartner', 'Kooperationspartner, Berater, Dienstleister', FALSE, '["management","finance"]', 6),
('VISITORS', 'Besucher', 'Betriebsbesucher und Gaeste', FALSE, '["management"]', 7),
('WEBSITE_USERS', 'Website-Nutzer', 'Besucher der Unternehmenswebsite', FALSE, '["marketing","it_operations"]', 8),
('APP_USERS', 'App-Nutzer', 'Nutzer mobiler Anwendungen', FALSE, '["product_engineering"]', 9),
('NEWSLETTER_SUBSCRIBERS', 'Newsletter-Abonnenten', 'Empfaenger von Newslettern', FALSE, '["marketing"]', 10),
('MEMBERS', 'Mitglieder', 'Vereins- oder Verbandsmitglieder', FALSE, '["management"]', 11),
('PATIENTS', 'Patienten', 'Patientinnen und Patienten', TRUE, '["other"]', 12),
('STUDENTS', 'Schueler/Studierende', 'Lernende in Bildungseinrichtungen', FALSE, '["other"]', 13),
('MINORS', 'Minderjaehrige', 'Personen unter 16 Jahren (Art. 8 DSGVO)', FALSE, '["other"]', 14),
('OTHER', 'Sonstige', 'Andere Betroffenenkategorien', FALSE, '[]', 15)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Data Categories — Parent categories (9)
-- =============================================================================
INSERT INTO vvt_lib_data_categories (id, parent_id, label_de, description_de, is_art9, is_art10, risk_weight, sort_order) VALUES
('IDENTIFICATION', NULL, 'Identifikationsdaten', 'Daten zur Identifizierung natuerlicher Personen', FALSE, FALSE, 2, 1),
('CONTACT_DATA', NULL, 'Kontaktdaten', 'Kommunikationsdaten und Adressen', FALSE, FALSE, 1, 2),
('FINANCIAL', NULL, 'Finanzdaten', 'Bank-, Gehalts- und Zahlungsdaten', FALSE, FALSE, 3, 3),
('EMPLOYMENT', NULL, 'Beschaeftigungsdaten', 'Arbeitsverhaeltnis und Qualifikation', FALSE, FALSE, 2, 4),
('DIGITAL_IDENTITY', NULL, 'Digitale Identitaet', 'Online-Kennungen und Zugangsdaten', FALSE, FALSE, 2, 5),
('COMMUNICATION', NULL, 'Kommunikationsdaten', 'Nachrichten und Vertragsdaten', FALSE, FALSE, 2, 6),
('MEDIA', NULL, 'Medien- und Standortdaten', 'Bild, Video, Standort', FALSE, FALSE, 3, 7),
('ART9_SPECIAL', NULL, 'Besondere Kategorien (Art. 9)', 'Besonders schuetzenswerte Daten', TRUE, FALSE, 5, 8),
('ART10', NULL, 'Strafrechtliche Daten (Art. 10)', 'Daten ueber strafrechtliche Verurteilungen', FALSE, TRUE, 5, 9)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Data Categories — Child categories (26)
-- =============================================================================
INSERT INTO vvt_lib_data_categories (id, parent_id, label_de, description_de, is_art9, is_art10, risk_weight, default_retention_rule, default_legal_basis, sort_order) VALUES
('NAME', 'IDENTIFICATION', 'Name', 'Vor- und Nachname, Geburtsname', FALSE, FALSE, 1, NULL, NULL, 10),
('DOB', 'IDENTIFICATION', 'Geburtsdatum', 'Geburtstag und -ort', FALSE, FALSE, 2, NULL, NULL, 11),
('ADDRESS', 'CONTACT_DATA', 'Anschrift', 'Wohn- und Postadresse', FALSE, FALSE, 1, NULL, NULL, 20),
('CONTACT', 'CONTACT_DATA', 'Kontaktinformationen', 'Telefon, E-Mail, Fax', FALSE, FALSE, 1, NULL, NULL, 21),
('ID_NUMBER', 'IDENTIFICATION', 'Ausweisnummer', 'Personalausweis-, Reisepassnummer', FALSE, FALSE, 3, NULL, NULL, 12),
('SOCIAL_SECURITY', 'IDENTIFICATION', 'Sozialversicherungsnummer', 'SV-Nummer', FALSE, FALSE, 4, 'BDSG_35_DELETE', 'ART6_1C', 13),
('TAX_ID', 'FINANCIAL', 'Steuer-ID', 'Steueridentifikationsnummer', FALSE, FALSE, 3, 'AO_147_10Y', 'ART6_1C', 30),
('BANK_ACCOUNT', 'FINANCIAL', 'Bankverbindung', 'IBAN, BIC, Kontonummer', FALSE, FALSE, 3, 'HGB_257_10Y', 'ART6_1B', 31),
('PAYMENT_DATA', 'FINANCIAL', 'Zahlungsdaten', 'Kreditkartendaten, Zahlungshistorie', FALSE, FALSE, 4, 'HGB_257_10Y', 'ART6_1B', 32),
('SALARY_DATA', 'FINANCIAL', 'Gehaltsdaten', 'Brutto/Netto, Zulagen, Abzuege', FALSE, FALSE, 4, 'AO_147_10Y', 'BDSG_26', 33),
('EMPLOYMENT_DATA', 'EMPLOYMENT', 'Arbeitsvertragsdaten', 'Vertragsdetails, Position, Abteilung', FALSE, FALSE, 2, 'HGB_257_10Y', 'BDSG_26', 40),
('EDUCATION_DATA', 'EMPLOYMENT', 'Ausbildungsdaten', 'Zeugnisse, Qualifikationen, Zertifikate', FALSE, FALSE, 2, 'AGG_15_6M', 'BDSG_26', 41),
('IP_ADDRESS', 'DIGITAL_IDENTITY', 'IP-Adresse', 'IPv4/IPv6 Adressen', FALSE, FALSE, 2, 'CUSTOM_90D', 'ART6_1F', 50),
('DEVICE_ID', 'DIGITAL_IDENTITY', 'Geraete-ID', 'Browser-Fingerprint, Device-ID', FALSE, FALSE, 2, 'CUSTOM_14M', 'ART6_1A', 51),
('LOGIN_DATA', 'DIGITAL_IDENTITY', 'Zugangsdaten', 'Benutzername, Passwort-Hash', FALSE, FALSE, 3, NULL, 'ART6_1B', 52),
('USAGE_DATA', 'DIGITAL_IDENTITY', 'Nutzungsdaten', 'Klickverhalten, Seitenaufrufe, Sessions', FALSE, FALSE, 2, 'CUSTOM_14M', 'ART6_1A', 53),
('COMMUNICATION_DATA', 'COMMUNICATION', 'Korrespondenz', 'E-Mails, Chat-Nachrichten, Briefe', FALSE, FALSE, 2, 'BGB_195_3Y', NULL, 60),
('CONTRACT_DATA', 'COMMUNICATION', 'Vertragsdaten', 'Vertragsdetails, Bestellungen', FALSE, FALSE, 2, 'HGB_257_10Y', 'ART6_1B', 61),
('PHOTO_VIDEO', 'MEDIA', 'Bild-/Videodaten', 'Fotos, Videos von Personen', FALSE, FALSE, 3, 'CONSENT_REVOKE', 'ART6_1A', 70),
('LOCATION_DATA', 'MEDIA', 'Standortdaten', 'GPS-Koordinaten, Aufenthaltsorte', FALSE, FALSE, 3, 'CUSTOM_90D', 'ART6_1A', 71),
('HEALTH_DATA', 'ART9_SPECIAL', 'Gesundheitsdaten', 'Krankheitsdaten, Atteste, Behinderung', TRUE, FALSE, 5, 'BDSG_35_DELETE', 'ART9_2H', 80),
('GENETIC_DATA', 'ART9_SPECIAL', 'Genetische Daten', 'DNA-Analysen, genetische Merkmale', TRUE, FALSE, 5, 'BDSG_35_DELETE', 'ART9_2A', 81),
('BIOMETRIC_DATA', 'ART9_SPECIAL', 'Biometrische Daten', 'Fingerabdruck, Gesichtserkennung', TRUE, FALSE, 5, 'BDSG_35_DELETE', 'ART9_2A', 82),
('RACIAL_ETHNIC', 'ART9_SPECIAL', 'Rassische/ethnische Herkunft', 'Ethnische Zugehoerigkeit', TRUE, FALSE, 5, NULL, 'ART9_2A', 83),
('POLITICAL_OPINIONS', 'ART9_SPECIAL', 'Politische Meinungen', 'Parteizugehoerigkeit, politische Haltung', TRUE, FALSE, 5, NULL, 'ART9_2A', 84),
('RELIGIOUS_BELIEFS', 'ART9_SPECIAL', 'Religioese Ueberzeugungen', 'Konfession, religioese Praktiken', TRUE, FALSE, 5, NULL, 'ART9_2A', 85),
('TRADE_UNION', 'ART9_SPECIAL', 'Gewerkschaftszugehoerigkeit', 'Mitgliedschaft in Gewerkschaften', TRUE, FALSE, 5, NULL, 'ART9_2A', 86),
('SEX_LIFE', 'ART9_SPECIAL', 'Sexualleben/Orientierung', 'Sexuelle Orientierung', TRUE, FALSE, 5, NULL, 'ART9_2A', 87),
('CRIMINAL_DATA', 'ART10', 'Strafrechtliche Daten', 'Verurteilungen, Straftaten, Fuehrungszeugnis', FALSE, TRUE, 5, 'BDSG_35_DELETE', 'BDSG_24', 90)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Legal Bases (12)
-- =============================================================================
INSERT INTO vvt_lib_legal_bases (id, article, type, label_de, description_de, is_art9, typical_national_law, sort_order) VALUES
('ART6_1A', 'Art. 6 Abs. 1 lit. a', 'CONSENT', 'Einwilligung', 'Die betroffene Person hat ihre Einwilligung gegeben', FALSE, NULL, 1),
('ART6_1B', 'Art. 6 Abs. 1 lit. b', 'CONTRACT', 'Vertragserfullung', 'Erforderlich fuer die Erfuellung eines Vertrags', FALSE, NULL, 2),
('ART6_1C', 'Art. 6 Abs. 1 lit. c', 'LEGAL_OBLIGATION', 'Rechtliche Verpflichtung', 'Erforderlich zur Erfuellung einer rechtlichen Verpflichtung', FALSE, NULL, 3),
('ART6_1D', 'Art. 6 Abs. 1 lit. d', 'VITAL_INTEREST', 'Lebenswichtige Interessen', 'Schutz lebenswichtiger Interessen', FALSE, NULL, 4),
('ART6_1E', 'Art. 6 Abs. 1 lit. e', 'PUBLIC_TASK', 'Oeffentliches Interesse', 'Wahrnehmung einer Aufgabe im oeffentlichen Interesse', FALSE, NULL, 5),
('ART6_1F', 'Art. 6 Abs. 1 lit. f', 'LEGITIMATE_INTEREST', 'Berechtigtes Interesse', 'Wahrung berechtigter Interessen des Verantwortlichen', FALSE, NULL, 6),
('ART9_2A', 'Art. 9 Abs. 2 lit. a', 'ART9', 'Ausdrueckliche Einwilligung (Art. 9)', 'Ausdrueckliche Einwilligung fuer besondere Kategorien', TRUE, NULL, 7),
('ART9_2B', 'Art. 9 Abs. 2 lit. b', 'ART9', 'Arbeitsrecht (Art. 9)', 'Erforderlich im Arbeitsrecht', TRUE, 'BDSG § 26', 8),
('ART9_2H', 'Art. 9 Abs. 2 lit. h', 'ART9', 'Gesundheitsvorsorge (Art. 9)', 'Gesundheitsvorsorge oder Arbeitsmedizin', TRUE, NULL, 9),
('BDSG_26', '§ 26 BDSG', 'NATIONAL', 'Beschaeftigtenverhaeltnis', 'Datenverarbeitung fuer Zwecke des Beschaeftigungsverhaeltnisses', FALSE, 'BDSG § 26', 10),
('BDSG_24', '§ 24 BDSG', 'NATIONAL', 'Strafrechtliche Daten', 'Verarbeitung strafrechtlicher Daten (Art. 10 DSGVO)', FALSE, 'BDSG § 24', 11),
('UWG_7', '§ 7 UWG', 'NATIONAL', 'Werbung mit Einwilligung', 'Werbliche Ansprache nach UWG', FALSE, 'UWG § 7', 12)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Retention Rules (12)
-- =============================================================================
INSERT INTO vvt_lib_retention_rules (id, label_de, description_de, legal_basis, duration, duration_unit, start_event, deletion_procedure, sort_order) VALUES
('HGB_257_10Y', '10 Jahre (HGB § 257)', 'Handelsrechtliche Aufbewahrungspflicht fuer Handelsbuecher, Jahresabschluesse, Buchungsbelege', 'HGB § 257', 10, 'YEARS', 'Ende des Kalenderjahres', 'Vernichtung nach Ablauf der Aufbewahrungsfrist', 1),
('AO_147_10Y', '10 Jahre (AO § 147)', 'Steuerrechtliche Aufbewahrungspflicht fuer Buchungsbelege', 'AO § 147', 10, 'YEARS', 'Ende des Kalenderjahres', 'Vernichtung nach Ablauf der Aufbewahrungsfrist', 2),
('AO_147_6Y', '6 Jahre (AO § 147)', 'Steuerrechtliche Aufbewahrungspflicht fuer Geschaeftsbriefe', 'AO § 147', 6, 'YEARS', 'Ende des Kalenderjahres', 'Vernichtung nach Ablauf der Aufbewahrungsfrist', 3),
('AGG_15_6M', '6 Monate (AGG § 15)', 'Frist fuer Schadensersatzansprueche nach AGG', 'AGG § 15', 6, 'MONTHS', 'Ablehnung / Ende des Verfahrens', 'Loeschung personenbezogener Bewerbungsdaten', 4),
('ARBZG_16_2Y', '2 Jahre (ArbZG § 16)', 'Aufzeichnungspflicht der Arbeitszeiten', 'ArbZG § 16', 2, 'YEARS', 'Ende des Aufzeichnungszeitraums', 'Vernichtung der Arbeitszeitaufzeichnungen', 5),
('BGB_195_3Y', '3 Jahre (BGB § 195)', 'Regelverjaehrungsfrist fuer vertragliche Ansprueche', 'BGB § 195', 3, 'YEARS', 'Ende des Jahres der Anspruchsentstehung', 'Loeschung nach Ablauf der Verjaehrungsfrist', 6),
('CONSENT_REVOKE', 'Bis Widerruf', 'Speicherung bis zum Widerruf der Einwilligung', 'Art. 7 Abs. 3 DSGVO', 0, 'DAYS', 'Widerruf der Einwilligung', 'Unverzuegliche Loeschung nach Widerruf', 7),
('PURPOSE_END', 'Bis Zweckerfuellung', 'Speicherung bis der Verarbeitungszweck erreicht ist', 'Art. 5 Abs. 1 lit. e DSGVO', 0, 'DAYS', 'Zweckerfuellung', 'Loeschung nach Zweckerfuellung', 8),
('BDSG_35_DELETE', 'Unverzuegliche Loeschung', 'Loeschung sobald Speicherung nicht mehr erforderlich', 'BDSG § 35', 0, 'DAYS', 'Wegfall der Erforderlichkeit', 'Unverzuegliche Loeschung', 9),
('CUSTOM_90D', '90 Tage', 'Benutzerdefinierte Aufbewahrungsfrist von 90 Tagen', NULL, 90, 'DAYS', 'Erstellung des Datensatzes', 'Automatische Loeschung nach 90 Tagen', 10),
('CUSTOM_14M', '14 Monate', 'Benutzerdefinierte Aufbewahrungsfrist von 14 Monaten (z.B. Analytics)', NULL, 14, 'MONTHS', 'Erstellung des Datensatzes', 'Automatische Loeschung nach 14 Monaten', 11),
('CUSTOM_30D', '30 Tage', 'Benutzerdefinierte Aufbewahrungsfrist von 30 Tagen', NULL, 30, 'DAYS', 'Erstellung des Datensatzes', 'Automatische Loeschung nach 30 Tagen', 12)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Recipients (15)
-- =============================================================================
INSERT INTO vvt_lib_recipients (id, type, label_de, description_de, is_third_country, country, sort_order) VALUES
('INTERNAL_HR', 'INTERNAL', 'Personalabteilung', 'Interne HR-Abteilung', FALSE, 'DE', 1),
('INTERNAL_FINANCE', 'INTERNAL', 'Finanzabteilung', 'Interne Buchhaltung und Finanzen', FALSE, 'DE', 2),
('INTERNAL_IT', 'INTERNAL', 'IT-Abteilung', 'Interne IT-Administration', FALSE, 'DE', 3),
('INTERNAL_MANAGEMENT', 'INTERNAL', 'Geschaeftsfuehrung', 'Geschaeftsfuehrung und Vorstand', FALSE, 'DE', 4),
('INTERNAL_MARKETING', 'INTERNAL', 'Marketingabteilung', 'Internes Marketing-Team', FALSE, 'DE', 5),
('INTERNAL_SUPPORT', 'INTERNAL', 'Kundenservice', 'Interner Support und Service', FALSE, 'DE', 6),
('PROCESSOR_PAYROLL', 'PROCESSOR', 'Lohnabrechnungsdienstleister', 'Externer Gehaltsabrechnungs-Dienstleister', FALSE, 'DE', 7),
('PROCESSOR_HOSTING', 'PROCESSOR', 'Hosting-Provider', 'Cloud- oder Server-Hosting-Anbieter', FALSE, NULL, 8),
('PROCESSOR_ANALYTICS', 'PROCESSOR', 'Analytics-Anbieter', 'Web-Analytics und Tracking-Dienstleister', FALSE, NULL, 9),
('PROCESSOR_EMAIL', 'PROCESSOR', 'E-Mail-Dienstleister', 'Newsletter- und E-Mail-Versand-Anbieter', FALSE, NULL, 10),
('PROCESSOR_HELPDESK', 'PROCESSOR', 'Helpdesk-Anbieter', 'Ticketsystem- und Support-Plattform', FALSE, NULL, 11),
('AUTHORITY_FINANZAMT', 'AUTHORITY', 'Finanzamt', 'Zustaendiges Finanzamt', FALSE, 'DE', 12),
('AUTHORITY_SOZIALVERSICHERUNG', 'AUTHORITY', 'Sozialversicherungstraeger', 'Renten-, Kranken-, Arbeitslosen-, Pflegeversicherung', FALSE, 'DE', 13),
('AUTHORITY_KRANKENKASSE', 'AUTHORITY', 'Krankenkasse', 'Gesetzliche oder private Krankenkasse', FALSE, 'DE', 14),
('AUTHORITY_DATENSCHUTZ', 'AUTHORITY', 'Datenschutzbehoerde', 'Zustaendige Datenschutz-Aufsichtsbehoerde', FALSE, 'DE', 15)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Transfer Mechanisms (8)
-- =============================================================================
INSERT INTO vvt_lib_transfer_mechanisms (id, label_de, description_de, article, requires_tia, sort_order) VALUES
('ADEQUACY_DECISION', 'Angemessenheitsbeschluss', 'EU-Angemessenheitsbeschluss gemaess Art. 45 DSGVO', 'Art. 45 DSGVO', FALSE, 1),
('SCC_CONTROLLER', 'Standardvertragsklauseln (C2C)', 'Standardvertragsklauseln Controller-zu-Controller', 'Art. 46 Abs. 2 lit. c DSGVO', TRUE, 2),
('SCC_PROCESSOR', 'Standardvertragsklauseln (C2P)', 'Standardvertragsklauseln Controller-zu-Processor', 'Art. 46 Abs. 2 lit. c DSGVO', TRUE, 3),
('BCR', 'Binding Corporate Rules', 'Verbindliche interne Datenschutzvorschriften', 'Art. 47 DSGVO', FALSE, 4),
('CONSENT_49A', 'Einwilligung (Art. 49)', 'Ausdrueckliche Einwilligung der betroffenen Person', 'Art. 49 Abs. 1 lit. a DSGVO', FALSE, 5),
('DEROGATION_49', 'Ausnahme (Art. 49)', 'Ausnahme fuer bestimmte Faelle gemaess Art. 49', 'Art. 49 DSGVO', FALSE, 6),
('DPF', 'EU-US Data Privacy Framework', 'Zertifizierung unter dem EU-US Data Privacy Framework', 'Art. 45 DSGVO (DPF)', FALSE, 7),
('TIA', 'Transfer Impact Assessment', 'Einzelfallbezogene Risikobewertung fuer Drittlandtransfers', 'Art. 46 DSGVO + Schrems II', TRUE, 8)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- Purposes (20)
-- =============================================================================
INSERT INTO vvt_lib_purposes (id, label_de, description_de, typical_legal_basis, typical_for, sort_order) VALUES
('EMPLOYMENT_ADMIN', 'Personalverwaltung', 'Verwaltung des Beschaeftigungsverhaeltnisses', 'BDSG_26', '["hr"]', 1),
('PAYROLL', 'Gehaltsabrechnung', 'Durchfuehrung der Lohn- und Gehaltsabrechnung', 'BDSG_26', '["hr","finance"]', 2),
('RECRUITING', 'Bewerbermanagement', 'Durchfuehrung von Bewerbungsverfahren', 'BDSG_26', '["hr"]', 3),
('TIME_TRACKING', 'Zeiterfassung', 'Erfassung und Verwaltung von Arbeitszeiten', 'ART6_1C', '["hr"]', 4),
('ACCOUNTING', 'Buchhaltung', 'Fuehrung der Handelsbuecher und Finanzberichterstattung', 'ART6_1C', '["finance"]', 5),
('INVOICING', 'Rechnungsstellung', 'Erstellung und Verwaltung von Rechnungen', 'ART6_1B', '["finance"]', 6),
('CRM', 'Kundenbeziehungsmanagement', 'Verwaltung und Pflege von Kundenbeziehungen', 'ART6_1B', '["sales_crm"]', 7),
('DIRECT_MARKETING', 'Direktmarketing', 'Newsletter-Versand und Werbemassnahmen', 'ART6_1A', '["marketing"]', 8),
('WEBSITE_ANALYTICS', 'Web-Analyse', 'Analyse des Nutzerverhaltens auf der Website', 'ART6_1A', '["marketing","it_operations"]', 9),
('CUSTOMER_SUPPORT', 'Kundenbetreuung', 'Bearbeitung von Kundenanfragen und Support-Tickets', 'ART6_1B', '["support"]', 10),
('IT_ADMIN', 'IT-Administration', 'Verwaltung der IT-Infrastruktur und Benutzerkonten', 'ART6_1F', '["it_operations"]', 11),
('BACKUP_RECOVERY', 'Datensicherung', 'Backup-Erstellung und Wiederherstellung', 'ART6_1F', '["it_operations"]', 12),
('SECURITY_MONITORING', 'Sicherheitsueberwachung', 'Log-Analyse und Intrusion Detection', 'ART6_1F', '["it_operations"]', 13),
('IAM', 'Identitaets- und Zugriffsmanagement', 'Verwaltung von Benutzeridentitaeten und Berechtigungen', 'ART6_1F', '["it_operations"]', 14),
('VIDEO_CONFERENCING', 'Videokonferenz', 'Durchfuehrung von Online-Meetings und Videokonferenzen', 'ART6_1B', '["other"]', 15),
('VISITOR_MANAGEMENT', 'Besucherverwaltung', 'Erfassung und Verwaltung von Betriebsbesuchern', 'ART6_1F', '["management"]', 16),
('PAYMENT_PROCESSING', 'Zahlungsabwicklung', 'Verarbeitung und Abwicklung von Zahlungen', 'ART6_1B', '["finance"]', 17),
('SOCIAL_MEDIA', 'Social-Media-Marketing', 'Betrieb von Social-Media-Praesenzen', 'ART6_1A', '["marketing"]', 18),
('SALES_REPORTING', 'Vertriebssteuerung', 'Vertriebsanalysen und Berichterstattung', 'ART6_1F', '["sales_crm"]', 19),
('COMPLIANCE_DOCS', 'Compliance-Dokumentation', 'Erstellung und Pflege von Compliance-Dokumenten', 'ART6_1C', '["legal","management"]', 20)
ON CONFLICT (id) DO NOTHING;
-- =============================================================================
-- TOMs (20)
-- =============================================================================
INSERT INTO vvt_lib_toms (id, category, label_de, description_de, art32_reference, sort_order) VALUES
('AC_RBAC', 'accessControl', 'Rollenbasierte Zugriffskontrolle (RBAC)', 'Zugriff nur nach Rolle und Berechtigung', 'Art. 32 Abs. 1 lit. b', 1),
('AC_MFA', 'accessControl', 'Multi-Faktor-Authentifizierung', 'Zwei- oder mehrstufige Anmeldung', 'Art. 32 Abs. 1 lit. b', 2),
('AC_NEED_TO_KNOW', 'accessControl', 'Need-to-Know-Prinzip', 'Zugriff nur auf fuer die Aufgabe erforderliche Daten', 'Art. 32 Abs. 1 lit. b', 3),
('AC_PAM', 'accessControl', 'Privileged Access Management', 'Verwaltung und Ueberwachung privilegierter Zugaenge', 'Art. 32 Abs. 1 lit. b', 4),
('CONF_ENCRYPTION_REST', 'confidentiality', 'Verschluesselung ruhender Daten', 'AES-256 Verschluesselung fuer gespeicherte Daten', 'Art. 32 Abs. 1 lit. a', 5),
('CONF_ENCRYPTION_TRANSIT', 'confidentiality', 'Transportverschluesselung', 'TLS 1.3 fuer alle Datenuebertragungen', 'Art. 32 Abs. 1 lit. a', 6),
('CONF_PSEUDONYMIZATION', 'confidentiality', 'Pseudonymisierung', 'Verarbeitung ohne direkten Personenbezug', 'Art. 32 Abs. 1 lit. a', 7),
('CONF_NDA', 'confidentiality', 'Vertraulichkeitsvereinbarungen', 'NDAs fuer Mitarbeiter und Auftragnehmer', 'Art. 32 Abs. 1 lit. b', 8),
('INT_AUDIT_LOG', 'integrity', 'Audit-Logging', 'Lueckenlose Protokollierung aller Datenzugriffe', 'Art. 32 Abs. 1 lit. b', 9),
('INT_FOUR_EYES', 'integrity', 'Vier-Augen-Prinzip', 'Kritische Aenderungen nur mit Freigabe durch zweite Person', 'Art. 32 Abs. 1 lit. b', 10),
('INT_CHECKSUMS', 'integrity', 'Pruefsummen und Hashing', 'Integritaetspruefung durch kryptographische Hashes', 'Art. 32 Abs. 1 lit. b', 11),
('INT_CHANGE_MGMT', 'integrity', 'Change Management', 'Dokumentierter Aenderungsprozess fuer IT-Systeme', 'Art. 32 Abs. 1 lit. b', 12),
('AVAIL_BACKUP', 'availability', 'Regelmaessige Backups', 'Taegliche und woechentliche Datensicherungen', 'Art. 32 Abs. 1 lit. c', 13),
('AVAIL_REDUNDANCY', 'availability', 'Redundante Systeme', 'Hochverfuegbarkeit durch Systemredundanz', 'Art. 32 Abs. 1 lit. c', 14),
('AVAIL_321_RULE', 'availability', '3-2-1 Backup-Regel', 'Drei Kopien, zwei Medien, ein externer Standort', 'Art. 32 Abs. 1 lit. c', 15),
('AVAIL_MONITORING', 'availability', 'System-Monitoring', 'Kontinuierliche Ueberwachung der Systemverfuegbarkeit', 'Art. 32 Abs. 1 lit. c', 16),
('SEP_TENANT_ISOLATION', 'separation', 'Mandantentrennung', 'Logische Trennung der Daten verschiedener Mandanten', 'Art. 32 Abs. 1 lit. b', 17),
('SEP_NETWORK_SEG', 'separation', 'Netzwerksegmentierung', 'Trennung von Netzwerkbereichen (VLANs, Firewalls)', 'Art. 32 Abs. 1 lit. b', 18),
('SEP_DATA_SEPARATION', 'separation', 'Datentrennung', 'Separate Datenbanken oder Schemas pro Zweck', 'Art. 32 Abs. 1 lit. b', 19),
('SEP_ENV_SEPARATION', 'separation', 'Umgebungstrennung', 'Getrennte Entwicklungs-, Test- und Produktionsumgebungen', 'Art. 32 Abs. 1 lit. b', 20)
ON CONFLICT (id) DO NOTHING;
COMMIT;

View File

@@ -0,0 +1,54 @@
-- Migration 066: VVT Process Templates + Activity extensions
-- Template table + new ref columns on compliance_vvt_activities
BEGIN;
-- =============================================================================
-- Process Templates
-- =============================================================================
CREATE TABLE IF NOT EXISTS vvt_process_templates (
id VARCHAR(80) PRIMARY KEY,
name VARCHAR(300) NOT NULL,
description TEXT,
business_function VARCHAR(50),
purpose_refs JSONB DEFAULT '[]'::jsonb,
legal_basis_refs JSONB DEFAULT '[]'::jsonb,
data_subject_refs JSONB DEFAULT '[]'::jsonb,
data_category_refs JSONB DEFAULT '[]'::jsonb,
recipient_refs JSONB DEFAULT '[]'::jsonb,
tom_refs JSONB DEFAULT '[]'::jsonb,
transfer_mechanism_refs JSONB DEFAULT '[]'::jsonb,
retention_rule_ref VARCHAR(50),
typical_systems JSONB DEFAULT '[]'::jsonb,
protection_level VARCHAR(10) DEFAULT 'MEDIUM',
dpia_required BOOLEAN DEFAULT FALSE,
risk_score INTEGER,
tags JSONB DEFAULT '[]'::jsonb,
is_system BOOLEAN DEFAULT TRUE,
tenant_id UUID,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vvt_process_templates_bf ON vvt_process_templates(business_function);
CREATE INDEX IF NOT EXISTS idx_vvt_process_templates_system ON vvt_process_templates(is_system);
-- =============================================================================
-- New columns on compliance_vvt_activities (all DEFAULT NULL for backward compat)
-- =============================================================================
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS purpose_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS legal_basis_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS data_subject_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS data_category_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS recipient_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS retention_rule_ref VARCHAR(50) DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS transfer_mechanism_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS tom_refs JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS linked_loeschfristen_ids JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS linked_tom_measure_ids JSONB DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS source_template_id VARCHAR(80) DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS risk_score INTEGER DEFAULT NULL;
ALTER TABLE compliance_vvt_activities ADD COLUMN IF NOT EXISTS art30_completeness JSONB DEFAULT NULL;
COMMIT;

View File

@@ -0,0 +1,305 @@
-- Migration 067: VVT Process Templates Seed — 18 templates from vvt-baseline-catalog
-- All content self-authored, MIT-compatible.
BEGIN;
INSERT INTO vvt_process_templates (id, name, description, business_function, purpose_refs, legal_basis_refs, data_subject_refs, data_category_refs, recipient_refs, tom_refs, retention_rule_ref, typical_systems, protection_level, dpia_required, risk_score, tags, sort_order) VALUES
-- HR Templates
('hr-mitarbeiterverwaltung',
'Mitarbeiterverwaltung',
'Verwaltung des Beschaeftigungsverhaeltnisses inkl. Personalakte, Urlaub, Krankmeldungen',
'hr',
'["EMPLOYMENT_ADMIN", "PAYROLL"]',
'["BDSG_26", "ART6_1B"]',
'["EMPLOYEES"]',
'["NAME", "DOB", "ADDRESS", "CONTACT", "SOCIAL_SECURITY", "BANK_ACCOUNT", "EMPLOYMENT_DATA", "HEALTH_DATA"]',
'["INTERNAL_HR", "INTERNAL_FINANCE", "PROCESSOR_PAYROLL", "AUTHORITY_SOZIALVERSICHERUNG", "AUTHORITY_KRANKENKASSE"]',
'["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_ENCRYPTION_REST", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG", "SEP_TENANT_ISOLATION"]',
'HGB_257_10Y',
'["HR-Software", "Personalakte (digital)"]',
'HIGH', TRUE, 3,
'["personal", "pflicht"]',
1),
('hr-gehaltsabrechnung',
'Gehaltsabrechnung',
'Monatliche Lohn- und Gehaltsabrechnung inkl. Steuer- und Sozialversicherungsmeldungen',
'hr',
'["PAYROLL"]',
'["BDSG_26", "ART6_1C"]',
'["EMPLOYEES"]',
'["NAME", "ADDRESS", "SOCIAL_SECURITY", "TAX_ID", "BANK_ACCOUNT", "SALARY_DATA"]',
'["INTERNAL_HR", "INTERNAL_FINANCE", "PROCESSOR_PAYROLL", "AUTHORITY_FINANZAMT", "AUTHORITY_SOZIALVERSICHERUNG"]',
'["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_ENCRYPTION_REST", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG", "INT_FOUR_EYES"]',
'AO_147_10Y',
'["Lohnabrechnungssoftware", "DATEV"]',
'HIGH', FALSE, 3,
'["personal", "finanzen", "pflicht"]',
2),
('hr-bewerbermanagement',
'Bewerbermanagement',
'Durchfuehrung von Bewerbungsverfahren vom Eingang bis zur Zu-/Absage',
'hr',
'["RECRUITING"]',
'["BDSG_26", "ART6_1B"]',
'["APPLICANTS"]',
'["NAME", "DOB", "ADDRESS", "CONTACT", "EDUCATION_DATA", "PHOTO_VIDEO"]',
'["INTERNAL_HR", "INTERNAL_MANAGEMENT"]',
'["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_ENCRYPTION_REST", "CONF_NDA"]',
'AGG_15_6M',
'["Bewerbermanagement-Software", "E-Mail"]',
'MEDIUM', FALSE, 2,
'["personal", "recruiting"]',
3),
('hr-zeiterfassung',
'Zeiterfassung',
'Erfassung und Verwaltung von Arbeitszeiten gemaess ArbZG',
'hr',
'["TIME_TRACKING"]',
'["ART6_1C", "BDSG_26"]',
'["EMPLOYEES"]',
'["NAME", "EMPLOYMENT_DATA"]',
'["INTERNAL_HR", "INTERNAL_MANAGEMENT"]',
'["AC_RBAC", "INT_AUDIT_LOG", "CONF_ENCRYPTION_TRANSIT"]',
'ARBZG_16_2Y',
'["Zeiterfassungssystem", "Stempeluhr"]',
'LOW', FALSE, 1,
'["personal", "pflicht"]',
4),
-- Finance Templates
('finance-buchhaltung',
'Buchhaltung',
'Fuehrung der Handelsbuecher und steuerrechtliche Dokumentation',
'finance',
'["ACCOUNTING", "INVOICING"]',
'["ART6_1C", "ART6_1B"]',
'["CUSTOMERS", "SUPPLIERS", "EMPLOYEES"]',
'["NAME", "ADDRESS", "CONTACT", "BANK_ACCOUNT", "PAYMENT_DATA", "CONTRACT_DATA", "TAX_ID"]',
'["INTERNAL_FINANCE", "AUTHORITY_FINANZAMT", "PROCESSOR_HOSTING"]',
'["AC_RBAC", "INT_AUDIT_LOG", "INT_FOUR_EYES", "CONF_ENCRYPTION_REST", "AVAIL_BACKUP"]',
'HGB_257_10Y',
'["Buchhaltungssoftware", "DATEV", "ERP-System"]',
'HIGH', FALSE, 2,
'["finanzen", "pflicht"]',
5),
('finance-zahlungsverkehr',
'Zahlungsverkehr',
'Verarbeitung und Abwicklung von ein- und ausgehenden Zahlungen',
'finance',
'["PAYMENT_PROCESSING"]',
'["ART6_1B", "ART6_1C"]',
'["CUSTOMERS", "SUPPLIERS"]',
'["NAME", "BANK_ACCOUNT", "PAYMENT_DATA", "CONTRACT_DATA"]',
'["INTERNAL_FINANCE", "PROCESSOR_HOSTING"]',
'["AC_RBAC", "AC_MFA", "CONF_ENCRYPTION_REST", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG"]',
'HGB_257_10Y',
'["Online-Banking", "Payment-Gateway"]',
'HIGH', FALSE, 3,
'["finanzen"]',
6),
-- Sales/CRM Templates
('sales-kundenverwaltung',
'Kundenverwaltung',
'Verwaltung und Pflege der Kundenbeziehungen im CRM-System',
'sales_crm',
'["CRM"]',
'["ART6_1B", "ART6_1F"]',
'["CUSTOMERS", "PROSPECTIVE_CUSTOMERS"]',
'["NAME", "ADDRESS", "CONTACT", "CONTRACT_DATA", "COMMUNICATION_DATA"]',
'["INTERNAL_MARKETING", "INTERNAL_SUPPORT", "PROCESSOR_HOSTING"]',
'["AC_RBAC", "CONF_ENCRYPTION_REST", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG", "SEP_TENANT_ISOLATION"]',
'BGB_195_3Y',
'["CRM-System", "E-Mail-Client"]',
'MEDIUM', FALSE, 2,
'["vertrieb", "kunden"]',
7),
('sales-vertriebssteuerung',
'Vertriebssteuerung',
'Vertriebsanalysen, Forecasting und Berichterstattung',
'sales_crm',
'["SALES_REPORTING"]',
'["ART6_1F"]',
'["CUSTOMERS", "PROSPECTIVE_CUSTOMERS"]',
'["NAME", "CONTACT", "CONTRACT_DATA"]',
'["INTERNAL_MANAGEMENT", "INTERNAL_MARKETING"]',
'["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_PSEUDONYMIZATION"]',
'BGB_195_3Y',
'["CRM-System", "BI-Tool"]',
'LOW', FALSE, 1,
'["vertrieb", "reporting"]',
8),
-- Marketing Templates
('marketing-newsletter',
'Newsletter-Versand',
'Versand von Newslettern und Werbemails an Abonnenten',
'marketing',
'["DIRECT_MARKETING"]',
'["ART6_1A", "UWG_7"]',
'["NEWSLETTER_SUBSCRIBERS", "CUSTOMERS"]',
'["NAME", "CONTACT", "USAGE_DATA"]',
'["INTERNAL_MARKETING", "PROCESSOR_EMAIL"]',
'["AC_RBAC", "CONF_ENCRYPTION_TRANSIT", "SEP_DATA_SEPARATION"]',
'CONSENT_REVOKE',
'["Newsletter-Tool", "E-Mail-Marketing-Plattform"]',
'LOW', FALSE, 1,
'["marketing", "einwilligung"]',
9),
('marketing-website-analytics',
'Website-Analyse',
'Analyse des Nutzerverhaltens auf der Unternehmenswebsite',
'marketing',
'["WEBSITE_ANALYTICS"]',
'["ART6_1A"]',
'["WEBSITE_USERS"]',
'["IP_ADDRESS", "DEVICE_ID", "USAGE_DATA"]',
'["INTERNAL_MARKETING", "PROCESSOR_ANALYTICS"]',
'["CONF_PSEUDONYMIZATION", "CONF_ENCRYPTION_TRANSIT", "SEP_DATA_SEPARATION"]',
'CUSTOM_14M',
'["Web-Analytics-Tool", "Tag-Manager"]',
'LOW', FALSE, 1,
'["marketing", "einwilligung", "tracking"]',
10),
('marketing-social-media',
'Social-Media-Marketing',
'Betrieb und Verwaltung von Social-Media-Praesenzen',
'marketing',
'["SOCIAL_MEDIA"]',
'["ART6_1A", "ART6_1F"]',
'["WEBSITE_USERS", "CUSTOMERS"]',
'["NAME", "CONTACT", "USAGE_DATA", "PHOTO_VIDEO"]',
'["INTERNAL_MARKETING", "PROCESSOR_ANALYTICS"]',
'["AC_RBAC", "CONF_ENCRYPTION_TRANSIT"]',
'PURPOSE_END',
'["Social-Media-Plattformen", "Social-Media-Management-Tool"]',
'LOW', FALSE, 1,
'["marketing", "social-media"]',
11),
-- Support Templates
('support-ticketsystem',
'Ticketsystem / Kundenservice',
'Bearbeitung von Kundenanfragen ueber das Ticketsystem',
'support',
'["CUSTOMER_SUPPORT"]',
'["ART6_1B"]',
'["CUSTOMERS"]',
'["NAME", "CONTACT", "COMMUNICATION_DATA", "CONTRACT_DATA"]',
'["INTERNAL_SUPPORT", "PROCESSOR_HELPDESK"]',
'["AC_RBAC", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG"]',
'BGB_195_3Y',
'["Ticketsystem", "Help-Desk-Software"]',
'MEDIUM', FALSE, 1,
'["support", "kunden"]',
12),
-- IT Templates
('it-systemadministration',
'IT-Systemadministration',
'Verwaltung der IT-Infrastruktur, Benutzerkonten und Berechtigungen',
'it_operations',
'["IT_ADMIN"]',
'["ART6_1F", "ART6_1B"]',
'["EMPLOYEES"]',
'["NAME", "LOGIN_DATA", "IP_ADDRESS", "DEVICE_ID"]',
'["INTERNAL_IT", "PROCESSOR_HOSTING"]',
'["AC_RBAC", "AC_MFA", "AC_PAM", "CONF_ENCRYPTION_REST", "CONF_ENCRYPTION_TRANSIT", "INT_AUDIT_LOG", "SEP_NETWORK_SEG", "SEP_ENV_SEPARATION"]',
'CUSTOM_90D',
'["Active Directory", "LDAP", "IT-Management-Tool"]',
'HIGH', FALSE, 2,
'["it", "infrastruktur"]',
13),
('it-backup',
'Datensicherung und Recovery',
'Regelmaessige Backups und Wiederherstellungsverfahren',
'it_operations',
'["BACKUP_RECOVERY"]',
'["ART6_1F"]',
'["EMPLOYEES", "CUSTOMERS"]',
'["NAME", "ADDRESS", "CONTACT", "CONTRACT_DATA", "LOGIN_DATA"]',
'["INTERNAL_IT", "PROCESSOR_HOSTING"]',
'["AVAIL_BACKUP", "AVAIL_321_RULE", "AVAIL_REDUNDANCY", "CONF_ENCRYPTION_REST", "INT_CHECKSUMS"]',
'CUSTOM_90D',
'["Backup-Software", "Cloud-Backup", "NAS"]',
'HIGH', FALSE, 2,
'["it", "verfuegbarkeit"]',
14),
('it-logging',
'Logging und Sicherheitsueberwachung',
'Protokollierung von System- und Sicherheitsereignissen',
'it_operations',
'["SECURITY_MONITORING"]',
'["ART6_1F"]',
'["EMPLOYEES", "CUSTOMERS", "WEBSITE_USERS"]',
'["IP_ADDRESS", "LOGIN_DATA", "USAGE_DATA", "DEVICE_ID"]',
'["INTERNAL_IT"]',
'["CONF_ENCRYPTION_REST", "INT_AUDIT_LOG", "INT_CHECKSUMS", "AVAIL_MONITORING", "SEP_DATA_SEPARATION"]',
'CUSTOM_90D',
'["SIEM-System", "Log-Management", "Monitoring-Tool"]',
'MEDIUM', FALSE, 2,
'["it", "sicherheit"]',
15),
('it-iam',
'Identitaets- und Zugriffsmanagement',
'Verwaltung von Benutzeridentitaeten, Rollen und Berechtigungen',
'it_operations',
'["IAM"]',
'["ART6_1F", "BDSG_26"]',
'["EMPLOYEES"]',
'["NAME", "LOGIN_DATA", "EMPLOYMENT_DATA"]',
'["INTERNAL_IT", "INTERNAL_HR"]',
'["AC_RBAC", "AC_MFA", "AC_PAM", "AC_NEED_TO_KNOW", "INT_AUDIT_LOG", "CONF_ENCRYPTION_REST"]',
'AGG_15_6M',
'["IAM-System", "SSO-Provider", "Active Directory"]',
'HIGH', FALSE, 2,
'["it", "sicherheit", "zugriffskontrolle"]',
16),
-- Other Templates
('other-videokonferenz',
'Videokonferenz',
'Durchfuehrung von Online-Meetings und Videokonferenzen',
'other',
'["VIDEO_CONFERENCING"]',
'["ART6_1B", "ART6_1F"]',
'["EMPLOYEES", "CUSTOMERS", "BUSINESS_PARTNERS"]',
'["NAME", "CONTACT", "PHOTO_VIDEO", "IP_ADDRESS"]',
'["INTERNAL_IT", "PROCESSOR_HOSTING"]',
'["CONF_ENCRYPTION_TRANSIT", "AC_RBAC"]',
'PURPOSE_END',
'["Videokonferenz-Tool", "Webinar-Plattform"]',
'LOW', FALSE, 1,
'["kommunikation"]',
17),
('other-besuchermanagement',
'Besuchermanagement',
'Erfassung und Verwaltung von Betriebsbesuchern',
'other',
'["VISITOR_MANAGEMENT"]',
'["ART6_1F"]',
'["VISITORS"]',
'["NAME", "CONTACT", "PHOTO_VIDEO"]',
'["INTERNAL_MANAGEMENT"]',
'["AC_RBAC", "CONF_ENCRYPTION_REST"]',
'CUSTOM_30D',
'["Besuchermanagement-System", "Empfangsterminal"]',
'LOW', FALSE, 1,
'["sonstiges", "besucher"]',
18)
ON CONFLICT (id) DO NOTHING;
COMMIT;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,292 @@
# Loeschfristen — Loeschkonzept (Art. 5/17/30 DSGVO)
## Uebersicht
Das Loeschfristen-Modul implementiert ein vollstaendiges, auditfaehiges Loeschkonzept gemaess DSGVO Art. 5 Abs. 1 lit. e (Speicherbegrenzung), Art. 17 (Recht auf Loeschung) und Art. 30 (Dokumentation der Loeschfristen im VVT).
**Kernfunktionen:**
- 3-Level-Loeschlogik (Zweckende → Aufbewahrungspflicht → Legal Hold)
- 25 vordefinierte Baseline-Templates fuer gaengige Datenobjekte
- 4-Schritt-Profiling-Wizard zur automatischen Policy-Generierung
- 11 automatisierte Compliance-Checks
- Druckfertiges Loeschkonzept-Dokument mit 11 Sektionen
- JSON/CSV/Markdown-Export
## 3-Level Loeschlogik
Die Loeschung personenbezogener Daten folgt einer dreistufigen Priorisierung:
```mermaid
graph TD
A[Daten erhoben] --> B{Zweck erfuellt?}
B -->|Ja| C{Aufbewahrungspflicht?}
B -->|Nein| D[Weiter speichern]
C -->|Ja| E{Frist abgelaufen?}
C -->|Nein| F{Legal Hold?}
E -->|Ja| F
E -->|Nein| G[Aufbewahren bis Fristende]
F -->|Ja| H[Speichern bis Legal Hold endet]
F -->|Nein| I[Loeschung durchfuehren]
```
| Level | Trigger | Beschreibung |
|-------|---------|--------------|
| 1 | **Zweckende** | Daten werden geloescht, wenn der Verarbeitungszweck entfaellt |
| 2 | **Aufbewahrungspflicht** | Gesetzliche Aufbewahrungsfristen verlaengern die Speicherung |
| 3 | **Legal Hold** | Aktive Legal Holds setzen die Loeschung aus |
## Frontend — 5-Tab-Aufbau
### Tab 1: Uebersicht
Statistik-Dashboard mit Filterung und Suche:
- Gesamtanzahl Policies, Status-Verteilung, ueberfaellige Pruefungen
- Suche nach Datenobjekt, Tags und Status
- Schnellaktionen: Bearbeiten, Klonen, Archivieren
### Tab 2: Editor
Vollstaendiges Bearbeitungsformular mit 35+ Feldern:
- Stammdaten (Datenobjekt, Beschreibung, Betroffenengruppen, Datenkategorien)
- 3-Level-Loeschlogik (Trigger, Aufbewahrungstreiber, Frist, Startereignis)
- Loeschmethode und -details
- Speicherorte (Typ, Provider, Backup-Kennzeichnung)
- Legal Hold Management (Hinzufuegen, Aktivieren, Aufheben)
- Verantwortlichkeiten und VVT-Verknuepfung
- Status-Workflow (Entwurf → Aktiv → Pruefung erforderlich → Archiviert)
### Tab 3: Generator
4-Schritt-Profiling-Wizard mit 16 Fragen:
1. **Organisation** (4 Fragen): Branche, Mitarbeiterzahl, Geschaeftsmodell, Website
2. **Datenkategorien** (5 Fragen): HR, Buchhaltung, Vertraege, Marketing, Video
3. **Systeme** (4 Fragen): Cloud, Backup, ERP/CRM, Zutrittskontrolle
4. **Spezielle Anforderungen** (3 Fragen): Legal Hold, Langzeitarchivierung, Gesundheitsdaten
Der Wizard generiert automatisch passende Policies aus dem Baseline-Katalog.
### Tab 4: Export & Compliance
- **JSON-Export**: Vollstaendiger Policy-Export als JSON
- **CSV-Export**: Excel-kompatibel mit BOM und Semikolon-Trennung
- **Compliance-Bericht**: Markdown-formatierter Bericht mit Score und Empfehlungen
- **11 Compliance-Checks**: Automatisierte Pruefung aller Policies
### Tab 5: Loeschkonzept-Dokument
Druckfertiges Loeschkonzept mit 11 Sektionen:
| # | Sektion | Inhalt |
|---|---------|--------|
| 0 | Deckblatt | Organisation, DSB, Version, Datum |
| — | Inhaltsverzeichnis | Auto-generiert |
| 1 | Ziel und Zweck | DSGVO Art. 5/17/30 Bezug |
| 2 | Geltungsbereich | Systeme, Speicherorte |
| 3 | Grundprinzipien | 5 Kernprinzipien |
| 4 | Loeschregeln-Uebersicht | Tabelle aller Policies |
| 5 | Detaillierte Loeschregeln | Pro Policy: Alle Felder |
| 6 | VVT-Verknuepfung | Cross-Referenz-Tabelle |
| 7 | Legal Hold Verfahren | Prozedur + aktive Holds |
| 8 | Verantwortlichkeiten | Rollenmatrix |
| 9 | Pruef-/Revisionszyklus | Review-Zeitplan |
| 10 | Compliance-Status | Score, Issues |
| 11 | Aenderungshistorie | Versionstabelle |
**Ausgabe:** HTML-Download oder PDF-Druck via Browser.
## Backend API (7 Endpoints)
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/api/v1/compliance/loeschfristen` | Alle Policies abrufen (mit Pagination) |
| `POST` | `/api/v1/compliance/loeschfristen` | Neue Policy erstellen |
| `GET` | `/api/v1/compliance/loeschfristen/{id}` | Einzelne Policy abrufen |
| `PUT` | `/api/v1/compliance/loeschfristen/{id}` | Policy aktualisieren |
| `DELETE` | `/api/v1/compliance/loeschfristen/{id}` | Policy loeschen |
| `GET` | `/api/v1/compliance/loeschfristen/stats` | Statistik-Uebersicht |
| `PATCH` | `/api/v1/compliance/loeschfristen/{id}/status` | Status aendern |
## Datenbank-Schema
Tabelle: `compliance_loeschfristen`
| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| `id` | UUID (PK) | Datenbank-ID |
| `tenant_id` | UUID | Mandant |
| `project_id` | UUID | Projekt |
| `policy_id` | VARCHAR | Display-ID (LF-2026-001) |
| `data_object_name` | VARCHAR | Datenobjekt |
| `description` | TEXT | Beschreibung |
| `affected_groups` | JSONB | Betroffenengruppen |
| `data_categories` | JSONB | Datenkategorien |
| `primary_purpose` | TEXT | Verarbeitungszweck |
| `deletion_trigger` | VARCHAR | Loeschtrigger |
| `retention_driver` | VARCHAR | Aufbewahrungstreiber |
| `retention_driver_detail` | TEXT | Detail zum Treiber |
| `retention_duration` | INTEGER | Aufbewahrungsdauer |
| `retention_unit` | VARCHAR | Einheit (DAYS/MONTHS/YEARS) |
| `retention_description` | TEXT | Beschreibung der Frist |
| `start_event` | VARCHAR | Startereignis |
| `has_active_legal_hold` | BOOLEAN | Legal Hold aktiv? |
| `legal_holds` | JSONB | Legal Holds (Array) |
| `storage_locations` | JSONB | Speicherorte (Array) |
| `deletion_method` | VARCHAR | Loeschmethode |
| `deletion_method_detail` | TEXT | Detail zur Methode |
| `responsible_role` | VARCHAR | Verantwortliche Rolle |
| `responsible_person` | VARCHAR | Verantwortliche Person |
| `release_process` | TEXT | Freigabeprozess |
| `linked_vvt_activity_ids` | JSONB | VVT-Verknuepfungen |
| `status` | VARCHAR | Status |
| `last_review_date` | TIMESTAMP | Letzte Pruefung |
| `next_review_date` | TIMESTAMP | Naechste Pruefung |
| `review_interval` | VARCHAR | Pruefintervall |
| `tags` | JSONB | Tags |
| `created_at` | TIMESTAMP | Erstellt am |
| `updated_at` | TIMESTAMP | Geaendert am |
Migration: `backend-compliance/migrations/017_loeschfristen.sql`
## Baseline-Katalog (25 Templates)
| # | Template-ID | Datenobjekt | Treiber | Frist | Trigger |
|---|-------------|-------------|---------|-------|---------|
| 1 | `personal-akten` | Personalakten | AO 147 | 10 Jahre | Aufbewahrungspflicht |
| 2 | `buchhaltungsbelege` | Buchhaltungsbelege | HGB 257 | 10 Jahre | Aufbewahrungspflicht |
| 3 | `rechnungen` | Rechnungen | UStG 14b | 10 Jahre | Aufbewahrungspflicht |
| 4 | `geschaeftsbriefe` | Geschaeftsbriefe | HGB 257 | 6 Jahre | Aufbewahrungspflicht |
| 5 | `bewerbungsunterlagen` | Bewerbungsunterlagen | AGG 15 | 6 Monate | Aufbewahrungspflicht |
| 6 | `kundenstammdaten` | Kundenstammdaten | BGB 195 | 3 Jahre | Aufbewahrungspflicht |
| 7 | `newsletter-einwilligungen` | Newsletter-Einwilligungen | — | Bis Widerruf | Zweckende |
| 8 | `webserver-logs` | Webserver-Logs | BSIG | 7 Tage | Aufbewahrungspflicht |
| 9 | `videoueberwachung` | Videoueberwachung | BDSG 35 | 2 Tage | Aufbewahrungspflicht |
| 10 | `gehaltsabrechnungen` | Gehaltsabrechnungen | AO 147 | 10 Jahre | Aufbewahrungspflicht |
| 11 | `vertraege` | Vertraege | HGB 257 | 10 Jahre | Aufbewahrungspflicht |
| 12 | `zeiterfassung` | Zeiterfassungsdaten | ArbZG 16 | 2 Jahre | Aufbewahrungspflicht |
| 13 | `krankmeldungen` | Krankmeldungen | BGB 195 | 3 Jahre | Aufbewahrungspflicht |
| 14 | `steuererklaerungen` | Steuererklaerungen | AO 147 | 10 Jahre | Aufbewahrungspflicht |
| 15 | `protokolle-gesellschafter` | Gesellschafterprotokolle | HGB 257 | 10 Jahre | Aufbewahrungspflicht |
| 16 | `crm-kontakthistorie` | CRM-Kontakthistorie | BGB 195 | 3 Jahre | Aufbewahrungspflicht |
| 17 | `backup-daten` | Backup-Daten | BSIG | 90 Tage | Aufbewahrungspflicht |
| 18 | `cookie-consent-logs` | Cookie-Consent-Nachweise | BGB 195 | 3 Jahre | Aufbewahrungspflicht |
| 19 | `email-archivierung` | E-Mail-Archivierung | HGB 257 | 6 Jahre | Aufbewahrungspflicht |
| 20 | `zutrittsprotokolle` | Zutrittsprotokolle | BSIG | 90 Tage | Aufbewahrungspflicht |
| 21 | `schulungsnachweise` | Schulungsnachweise | Individuell | 3 Jahre | Aufbewahrungspflicht |
| 22 | `betriebsarzt-doku` | Betriebsarzt-Dokumentation | Individuell | 40 Jahre | Aufbewahrungspflicht |
| 23 | `kundenreklamationen` | Kundenreklamationen | BGB 195 | 3 Jahre | Aufbewahrungspflicht |
| 24 | `lieferantenbewertungen` | Lieferantenbewertungen | HGB 257 | 6 Jahre | Aufbewahrungspflicht |
| 25 | `social-media-daten` | Social-Media-Marketingdaten | — | Bis Zweckende | Zweckende |
## 9 Aufbewahrungstreiber
| Treiber | Gesetz | Standard-Frist | Beschreibung |
|---------|--------|----------------|--------------|
| `AO_147` | 147 AO | 10 Jahre | Steuerrelevante Unterlagen |
| `HGB_257` | 257 HGB | 10/6 Jahre | Handelsbuecher, -briefe |
| `USTG_14B` | 14b UStG | 10 Jahre | Rechnungen |
| `BGB_195` | 195 BGB | 3 Jahre | Regelmaessige Verjaehrung |
| `ARBZG_16` | 16 Abs. 2 ArbZG | 2 Jahre | Arbeitszeitaufzeichnungen |
| `AGG_15` | 15 Abs. 4 AGG | 6 Monate | Entschaedigungsansprueche |
| `BDSG_35` | 35 BDSG / Art. 17 DSGVO | Unverzueglich | Zweckwegfall |
| `BSIG` | BSIG / IT-SiG 2.0 | 90 Tage | Sicherheitslogs |
| `CUSTOM` | Individuell | — | Benutzerdefiniert |
## 6 Loeschmethoden
| Methode | Beschreibung |
|---------|--------------|
| `AUTO_DELETE` | Automatische Loeschung durch System-Job |
| `MANUAL_REVIEW_DELETE` | Manuelle Pruefung vor Loeschung |
| `ANONYMIZATION` | Anonymisierung (Statistik bleibt erhalten) |
| `AGGREGATION` | Statistische Verdichtung |
| `CRYPTO_ERASE` | Kryptographische Loeschung (Key Destruction) |
| `PHYSICAL_DESTROY` | Physische Vernichtung (DIN 66399) |
## Profiling Wizard (4 Schritte, 16 Fragen)
Der Profiling-Wizard generiert automatisch passende Loeschrichtlinien basierend auf dem Unternehmensprofil.
| Schritt | Titel | Fragen | Inhalt |
|---------|-------|--------|--------|
| 1 | Organisation | 4 | Branche, Groesse, Geschaeftsmodell, Website |
| 2 | Datenkategorien | 5 | HR, Buchhaltung, Vertraege, Marketing, Video |
| 3 | Systeme & Infrastruktur | 4 | Cloud, Backup, ERP/CRM, Zutrittskontrolle |
| 4 | Spezielle Anforderungen | 3 | Legal Hold, Archivierung, Gesundheitsdaten |
**Regelbeispiele:**
- `data-hr = true` → Personalakten, Gehaltsabrechnungen, Zeiterfassung, Bewerbungen, Krankmeldungen, Schulungsnachweise
- `data-buchhaltung = true` → Buchhaltungsbelege, Rechnungen, Steuererklaerungen
- `data-vertraege = true` → Vertraege, Geschaeftsbriefe, Kundenstammdaten, Kundenreklamationen, Lieferantenbewertungen
- `data-marketing = true` → Newsletter, CRM-Kontakthistorie, Cookie-Consent, Social-Media-Daten
- `sys-zutritt = true` → Zutrittsprotokolle
- `sys-cloud = true` → E-Mail-Archivierung
- `special-gesundheit = true` → Krankmeldungen, Betriebsarzt-Dokumentation
## Compliance Checker (11 Pruefungen)
| # | Check-Typ | Schweregrad | Ausloeser |
|---|-----------|-------------|-----------|
| 1 | `MISSING_TRIGGER` | HIGH | Policy ohne Loeschtrigger |
| 2 | `MISSING_LEGAL_BASIS` | HIGH | Aufbewahrungspflicht ohne Rechtsgrundlage |
| 3 | `OVERDUE_REVIEW` | MEDIUM | Ueberfaellige Pruefung |
| 4 | `NO_RESPONSIBLE` | MEDIUM | Keine Verantwortliche Person/Rolle |
| 5 | `LEGAL_HOLD_CONFLICT` | CRITICAL | Legal Hold + Auto-Delete aktiv |
| 6 | `STALE_DRAFT` | LOW | Entwurf aelter als 90 Tage |
| 7 | `UNCOVERED_VVT_CATEGORY` | MEDIUM | VVT-Datenkategorie ohne Loeschfrist |
| 8 | `MISSING_DELETION_METHOD` | MEDIUM | Aktive Policy ohne Loeschmethoden-Detail |
| 9 | `MISSING_STORAGE_LOCATIONS` | MEDIUM | Aktive Policy ohne Speicherorte |
| 10 | `EXCESSIVE_RETENTION` | HIGH | Frist > 2x gesetzliches Maximum |
| 11 | `MISSING_DATA_CATEGORIES` | LOW | Nicht-Entwurf ohne Datenkategorien |
**Score-Berechnung:** `100 - (CRITICAL*15 + HIGH*10 + MEDIUM*5 + LOW*2)`
## Cross-Modul-Integration
### VVT-Verknuepfung
Jede Loeschregel kann mit Verarbeitungstaetigkeiten aus dem VVT verknuepft werden (`linked_vvt_activity_ids`). Das Loeschkonzept-Dokument generiert automatisch eine Cross-Referenz-Tabelle (Sektion 6).
### Scope Engine Prefill
Der Profiling-Wizard kann Antworten aus der Compliance Scope Engine uebernehmen. 12 Fragen werden automatisch vorausgefuellt:
- `org-branche`, `org-mitarbeiter`, `org-geschaeftsmodell`, `org-website`
- `data-hr`, `data-buchhaltung`, `data-vertraege`, `data-marketing`, `data-video`
- `sys-cloud`, `sys-erp`
### Document Generator Template
Das Loeschkonzept kann als eigenstaendiges Template im Document Generator genutzt werden (Template-Typ: `loeschkonzept`).
## Audit-Faehigkeit
Das Loeschkonzept erfuellt folgende Audit-Kriterien:
1. **Vollstaendigkeit:** Alle Datenobjekte mit Loeschfristen, Rechtsgrundlagen und Methoden dokumentiert
2. **Nachvollziehbarkeit:** Aenderungshistorie mit Versionsnummern und Autoren
3. **Aktualitaet:** Definiertes Pruefintervall mit automatischer Ueberfaelligkeits-Erkennung
4. **Verantwortlichkeit:** Rollenmatrix mit klaren Zustaendigkeiten
5. **Cross-Referenz:** Verknuepfung mit VVT (Art. 30 DSGVO)
6. **Compliance-Nachweis:** Automatisierte 11-Punkt-Pruefung mit Score und Befundprotokoll
7. **Druckfertigkeit:** PDF-taugliches Dokument mit Deckblatt, Inhaltsverzeichnis und rechtlichen Verweisen
## Datei-Uebersicht
| Datei | Beschreibung |
|-------|--------------|
| `admin-compliance/app/sdk/loeschfristen/page.tsx` | Frontend-Seite (5 Tabs) |
| `admin-compliance/lib/sdk/loeschfristen-types.ts` | TypeScript-Typen und Konstanten |
| `admin-compliance/lib/sdk/loeschfristen-baseline-catalog.ts` | 25 Baseline-Templates |
| `admin-compliance/lib/sdk/loeschfristen-profiling.ts` | 4-Schritt-Profiling-Wizard |
| `admin-compliance/lib/sdk/loeschfristen-compliance.ts` | 11 Compliance-Checks |
| `admin-compliance/lib/sdk/loeschfristen-export.ts` | JSON/CSV/Markdown-Export |
| `admin-compliance/lib/sdk/loeschfristen-document.ts` | Loeschkonzept-Dokument-Generator |
| `backend-compliance/compliance/api/loeschfristen_routes.py` | Backend API-Routen |
| `backend-compliance/compliance/db/loeschfristen_models.py` | SQLAlchemy-Modelle |
| `backend-compliance/migrations/017_loeschfristen.sql` | Datenbank-Migration |
| `backend-compliance/tests/test_loeschfristen_routes.py` | Backend-Tests (58+) |

View File

@@ -0,0 +1,759 @@
# VVT — Verzeichnis von Verarbeitungstaetigkeiten (Art. 30 DSGVO)
Das VVT-Modul implementiert das Verarbeitungsverzeichnis gemaess Art. 30 DSGVO mit einer
3-Schichten-Architektur: **Master-Libraries** (globale Stammdaten) + **Prozess-Templates**
(vorgefertigte Verarbeitungsvorlagen) + **Tenant-Instanzen** (individuelle Verarbeitungstaetigkeiten).
**Route:** `/sdk/vvt` | **Backend:** `backend-compliance:8002` | **Migrationen:** `006`, `033`, `035`, `064`-`067`
---
## Uebersicht
| Checkpoint | Reviewer | Rechtsgrundlage | Status |
|-----------|----------|-----------------|--------|
| CP-VVT (REQUIRED) | DSB | Art. 30 DSGVO | Phase 1 abgeschlossen |
**Wann ist ein VVT Pflicht?**
Jeder Verantwortliche und jeder Auftragsverarbeiter muss ein Verzeichnis aller
Verarbeitungstaetigkeiten fuehren (Art. 30 Abs. 1 und Abs. 2 DSGVO). Ausnahmen gelten
nur fuer Unternehmen mit weniger als 250 Beschaeftigten — und auch nur dann, wenn die
Verarbeitung kein Risiko birgt, nur gelegentlich erfolgt und keine besonderen Datenkategorien
(Art. 9/10 DSGVO) betroffen sind. In der Praxis entfaellt die Ausnahme fast nie.
---
## 3-Schichten-Architektur
Das VVT-Modul arbeitet mit drei Abstraktionsebenen:
```mermaid
graph TD
A[Ebene A: Master-Libraries] -->|IDs referenzieren| B[Ebene B: Prozess-Templates]
B -->|Instanziierung| C[Ebene C: Tenant-Aktivitaeten]
A -->|Direkte Auswahl| C
```
### Ebene A — Master-Libraries (Global)
8 globale Referenztabellen, die **ohne `tenant_id`** arbeiten und allen Mandanten zur Verfuegung stehen:
| Library | Tabelle | Eintraege | Beschreibung |
|---------|---------|-----------|-------------|
| Betroffenenkategorien | `vvt_lib_data_subjects` | 15 | Mitarbeiter, Kunden, Bewerber, ... |
| Datenkategorien | `vvt_lib_data_categories` | 35 | Hierarchisch mit Parent/Child (9 Oberkategorien + 26 Unterkategorien) |
| Empfaenger | `vvt_lib_recipients` | 15 | Intern, Auftragsverarbeiter, Behoerden |
| Rechtsgrundlagen | `vvt_lib_legal_bases` | 12 | Art. 6, Art. 9, BDSG, UWG |
| Loeschfristen | `vvt_lib_retention_rules` | 12 | HGB 10J, AO 6J/10J, AGG 6M, ... |
| Transfermechanismen | `vvt_lib_transfer_mechanisms` | 8 | Angemessenheit, SCC, BCR, DPF, ... |
| Verarbeitungszwecke | `vvt_lib_purposes` | 20 | Personalverwaltung, CRM, Analytics, ... |
| TOMs | `vvt_lib_toms` | 20 | RBAC, MFA, Verschluesselung, Backup, ... |
#### Datenkategorien-Hierarchie
Die Datenkategorien sind zweistufig organisiert:
| Oberkategorie | Unterkategorien | Art. 9 |
|--------------|----------------|--------|
| Identifikationsdaten | Name, Geburtsdatum, Ausweisnummer, SV-Nummer | Nein |
| Kontaktdaten | Anschrift, Kontaktinformationen | Nein |
| Finanzdaten | Steuer-ID, Bankverbindung, Zahlungsdaten, Gehaltsdaten | Nein |
| Beschaeftigungsdaten | Arbeitsvertragsdaten, Ausbildungsdaten | Nein |
| Digitale Identitaet | IP-Adresse, Geraete-ID, Zugangsdaten, Nutzungsdaten | Nein |
| Kommunikationsdaten | Korrespondenz, Vertragsdaten | Nein |
| Medien- und Standortdaten | Bild/Video, Standortdaten | Nein |
| Besondere Kategorien (Art. 9) | Gesundheit, Genetik, Biometrie, Ethnische Herkunft, Politische Meinungen, Religion, Gewerkschaft, Sexualleben | **Ja** |
| Strafrechtliche Daten (Art. 10) | Verurteilungen, Straftaten | Art. 10 |
### Ebene B — Prozess-Templates (System + Tenant)
18 vorgefertigte Prozess-Templates, die Library-IDs referenzieren und mit einem Klick
in eine VVT-Aktivitaet instanziiert werden koennen:
| Template-ID | Name | Business Function | Loeschfrist |
|-------------|------|-------------------|-------------|
| `hr-mitarbeiterverwaltung` | Mitarbeiterverwaltung | Personal | 10 Jahre (HGB) |
| `hr-gehaltsabrechnung` | Gehaltsabrechnung | Personal | 10 Jahre (AO) |
| `hr-bewerbermanagement` | Bewerbermanagement | Personal | 6 Monate (AGG) |
| `hr-zeiterfassung` | Zeiterfassung | Personal | 2 Jahre (ArbZG) |
| `finance-buchhaltung` | Buchhaltung | Finanzen | 10 Jahre (HGB) |
| `finance-zahlungsverkehr` | Zahlungsverkehr | Finanzen | 10 Jahre (HGB) |
| `sales-kundenverwaltung` | Kundenverwaltung | Vertrieb | 3 Jahre (BGB) |
| `sales-vertriebssteuerung` | Vertriebssteuerung | Vertrieb | 3 Jahre (BGB) |
| `marketing-newsletter` | Newsletter-Versand | Marketing | Bis Widerruf |
| `marketing-website-analytics` | Website-Analyse | Marketing | 14 Monate |
| `marketing-social-media` | Social-Media-Marketing | Marketing | Bis Zweckerfuellung |
| `support-ticketsystem` | Ticketsystem | Support | 3 Jahre (BGB) |
| `it-systemadministration` | IT-Systemadministration | IT | 90 Tage |
| `it-backup` | Datensicherung | IT | 90 Tage |
| `it-logging` | Logging und Ueberwachung | IT | 90 Tage |
| `it-iam` | Identitaetsmanagement | IT | 6 Monate (AGG) |
| `other-videokonferenz` | Videokonferenz | Sonstiges | Bis Zweckerfuellung |
| `other-besuchermanagement` | Besuchermanagement | Sonstiges | 30 Tage |
### Ebene C — Tenant-Aktivitaeten
Individuelle Verarbeitungstaetigkeiten pro Mandant. Jede Aktivitaet kann sowohl
**Freitext-Felder** (bestehendes Schema) als auch **Library-Referenzen** (neue `*_refs`-Felder)
enthalten. Beide existieren parallel — die Library-Referenzen ergaenzen die Freitext-Felder
fuer strukturierte Auswertungen und Cross-Modul-Verknuepfungen.
---
## Funktionen
- **CRUD-Operationen:** Anlegen, Lesen, Aktualisieren, Loeschen von VVT-Aktivitaeten
- **Organisations-Header:** DSB-Kontakt, VVT-Version, Pruefintervall
- **Template-Instanziierung:** Neue Aktivitaet aus Prozess-Template erstellen (inkl. automatischer Freitext-Befuellung)
- **Library-Referenzen:** Strukturierte Auswahl aus 8 Master-Libraries (parallel zu Freitext)
- **Art. 30 Completeness-Check:** Automatische Bewertung der Pflichtfelder-Vollstaendigkeit
- **Scope-basierte Generierung:** Automatische Erstellung von Aktivitaeten aus Compliance-Scope-Antworten
- **Status-Workflow:** `DRAFT` -> `REVIEW` -> `APPROVED` / `ARCHIVED`
- **Export:** JSON und CSV (Excel-kompatibel mit BOM, Semikolon-getrennt)
- **Audit-Log:** Protokollierung aller Aktionen (CREATE / UPDATE / DELETE / EXPORT)
- **Statistiken:** Zaehler nach Status, Geschaeftsbereich, DSFA-Pflicht, Drittlandtransfers
- **Cross-Modul-Links:** Verknuepfung zu Loeschfristen und TOM-Massnahmen (Phase 2)
---
## API-Endpoints
### Aktivitaeten (CRUD)
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/vvt/activities` | Liste (Filter: status, business_function, search, review_overdue) |
| `POST` | `/vvt/activities` | Neue Aktivitaet anlegen -> HTTP 201 |
| `GET` | `/vvt/activities/{id}` | Einzelne Aktivitaet abrufen |
| `PUT` | `/vvt/activities/{id}` | Aktivitaet aktualisieren |
| `DELETE` | `/vvt/activities/{id}` | Aktivitaet loeschen |
| `GET` | `/vvt/activities/{id}/completeness` | Art. 30 Vollstaendigkeits-Check |
| `GET` | `/vvt/activities/{id}/versions` | Versionshistorie |
### Organisation
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/vvt/organization` | Organisations-Header laden |
| `PUT` | `/vvt/organization` | Organisations-Header speichern (Upsert) |
### Export & Statistik
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/vvt/export?format=json` | JSON-Export aller Aktivitaeten |
| `GET` | `/vvt/export?format=csv` | CSV-Export (Semikolon, UTF-8 BOM) |
| `GET` | `/vvt/stats` | Statistik-Zusammenfassung |
| `GET` | `/vvt/audit-log` | Audit-Trail (limit, offset) |
### Master-Libraries (read-only, global)
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/vvt/libraries` | Uebersicht: alle 8 Library-Typen mit Anzahl |
| `GET` | `/vvt/libraries/data-subjects` | Betroffenenkategorien (Filter: `typical_for`) |
| `GET` | `/vvt/libraries/data-categories` | Datenkategorien — hierarchisch oder `?flat=true` (Filter: `parent_id`, `is_art9`) |
| `GET` | `/vvt/libraries/recipients` | Empfaenger (Filter: `type`) |
| `GET` | `/vvt/libraries/legal-bases` | Rechtsgrundlagen (Filter: `is_art9`, `type`) |
| `GET` | `/vvt/libraries/retention-rules` | Loeschfristen mit Dauer, Einheit, Rechtsgrundlage |
| `GET` | `/vvt/libraries/transfer-mechanisms` | Uebermittlungsmechanismen |
| `GET` | `/vvt/libraries/purposes` | Verarbeitungszwecke (Filter: `typical_for`) |
| `GET` | `/vvt/libraries/toms` | Technisch-Organisatorische Massnahmen (Filter: `category`) |
### Prozess-Templates
| Methode | Pfad | Beschreibung |
|---------|------|--------------|
| `GET` | `/vvt/templates` | Alle Templates (Filter: `business_function`, `search`) |
| `GET` | `/vvt/templates/{id}` | Einzelnes Template mit aufgeloesten Library-Labels |
| `POST` | `/vvt/templates/{id}/instantiate` | VVT-Aktivitaet aus Template erstellen -> HTTP 201 |
!!! info "Proxy-Route (Frontend)"
Das Admin-Frontend ruft die Endpoints ueber den Next.js-Proxy auf:
`/api/sdk/v1/compliance/vvt/**` -> `backend-compliance:8002/api/compliance/vvt/**`
---
## Datenfluss: Template-Instanziierung
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend
participant DB as PostgreSQL
F->>B: POST /vvt/templates/hr-mitarbeiterverwaltung/instantiate
B->>DB: SELECT FROM vvt_process_templates WHERE id = 'hr-mitarbeiterverwaltung'
DB-->>B: Template mit Library-Referenzen
Note over B: Fuer jeden ref-Typ:<br/>Library-IDs -> Labels aufloesen
B->>DB: SELECT FROM vvt_lib_purposes WHERE id IN ('EMPLOYMENT_ADMIN', 'PAYROLL')
DB-->>B: Labels: "Personalverwaltung", "Gehaltsabrechnung"
B->>DB: SELECT FROM vvt_lib_retention_rules WHERE id = 'HGB_257_10Y'
DB-->>B: "10 Jahre (HGB § 257)"
Note over B: Neue VVT-Aktivitaet erstellen:<br/>- Freitext-Felder mit Labels befuellt<br/>- *_refs-Felder mit Library-IDs
B->>DB: INSERT INTO compliance_vvt_activities (...)
B->>DB: INSERT INTO compliance_vvt_audit_log (action='CREATE', ...)
DB-->>B: Neue Aktivitaet mit UUID
B-->>F: 201 Created + VVTActivityResponse
F->>F: Wechsel zu Editor-Tab
```
---
## Datenmodell
### VVT-Aktivitaet (Response) — vollstaendig
```json
{
"id": "uuid",
"vvt_id": "VVT-0001",
"name": "Mitarbeiterverwaltung",
"description": "Verwaltung des Beschaeftigungsverhaeltnisses",
"status": "DRAFT",
"business_function": "hr",
"purposes": ["Personalverwaltung", "Gehaltsabrechnung"],
"legal_bases": [{"type": "BDSG_26", "description": "Beschaeftigtenverhaeltnis"}],
"data_subject_categories": ["Beschaeftigte"],
"personal_data_categories": ["Name", "Geburtsdatum", "Bankverbindung"],
"recipient_categories": [{"type": "INTERNAL", "name": "Personalabteilung"}],
"third_country_transfers": [],
"retention_period": {
"description": "10 Jahre (HGB § 257)",
"legalBasis": "HGB § 257",
"deletionProcedure": "Vernichtung",
"duration": 10,
"durationUnit": "YEARS"
},
"tom_description": "Rollenbasierte Zugriffskontrolle, Verschluesselung",
"structured_toms": {
"accessControl": ["RBAC", "Need-to-Know"],
"confidentiality": ["Verschluesselung ruhender Daten"],
"integrity": ["Audit-Logging"],
"availability": [],
"separation": ["Mandantentrennung"]
},
"systems": [{"systemId": "HR-Software", "name": "HR-Software"}],
"deployment_model": "cloud",
"protection_level": "HIGH",
"dpia_required": true,
"responsible": "Max Mustermann",
"owner": "Personalabteilung",
"purpose_refs": ["EMPLOYMENT_ADMIN", "PAYROLL"],
"legal_basis_refs": ["BDSG_26", "ART6_1B"],
"data_subject_refs": ["EMPLOYEES"],
"data_category_refs": ["NAME", "DOB", "ADDRESS", "BANK_ACCOUNT", "EMPLOYMENT_DATA"],
"recipient_refs": ["INTERNAL_HR", "INTERNAL_FINANCE", "PROCESSOR_PAYROLL"],
"retention_rule_ref": "HGB_257_10Y",
"transfer_mechanism_refs": null,
"tom_refs": ["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_ENCRYPTION_REST", "INT_AUDIT_LOG"],
"source_template_id": "hr-mitarbeiterverwaltung",
"risk_score": 3,
"linked_loeschfristen_ids": null,
"linked_tom_measure_ids": null,
"art30_completeness": {
"score": 90,
"missing": ["responsible"],
"warnings": [],
"passed": 9,
"total": 10
},
"created_at": "2026-03-19T10:00:00Z",
"updated_at": "2026-03-19T12:30:00Z"
}
```
!!! warning "Dual-Schema: Freitext + Library-Referenzen"
Jede Aktivitaet hat **zwei parallele Repraesentationen** fuer Datenkategorien,
Betroffene, Zwecke etc.: Die bestehenden Freitext-Felder (`purposes`, `legal_bases`, ...)
und die neuen Library-Referenz-Felder (`purpose_refs`, `legal_basis_refs`, ...).
Beide existieren parallel. Bei Template-Instanziierung werden beide automatisch befuellt.
Bestehende Aktivitaeten ohne `*_refs` bleiben voll funktionsfaehig.
### Art. 30 Completeness-Check
Der Completeness-Endpoint prueft 10 Pflichtfelder nach Art. 30 Abs. 1 DSGVO:
| Nr. | Pruefung | Quelle |
|-----|---------|--------|
| 1 | Name der Verarbeitung | `name` |
| 2 | Verarbeitungszweck(e) | `purposes` ODER `purpose_refs` |
| 3 | Rechtsgrundlage | `legal_bases` ODER `legal_basis_refs` |
| 4 | Betroffenenkategorien | `data_subject_categories` ODER `data_subject_refs` |
| 5 | Datenkategorien | `personal_data_categories` ODER `data_category_refs` |
| 6 | Empfaenger | `recipient_categories` ODER `recipient_refs` |
| 7 | Drittland-Uebermittlung | Immer bestanden (kein Transfer ist valide) |
| 8 | Loeschfristen | `retention_period.description` ODER `retention_rule_ref` |
| 9 | TOM-Beschreibung | `tom_description` ODER `tom_refs` ODER `structured_toms` |
| 10 | Verantwortlicher | `responsible` |
**Warnings** (nicht im Score, aber angezeigt):
- `dpia_required_but_no_dsfa_linked` — DSFA erforderlich, aber keine DSFA verknuepft
- `third_country_transfer_without_mechanism` — Drittlandtransfer ohne Transfermechanismus
```json
{
"score": 80,
"missing": ["retention_period", "responsible"],
"warnings": ["dpia_required_but_no_dsfa_linked"],
"passed": 8,
"total": 10
}
```
---
## Library-Datenmodell
### Master-Library-Eintrag (allgemein)
```json
{
"id": "EMPLOYEES",
"label_de": "Beschaeftigte",
"description_de": "Aktuelle Mitarbeiterinnen und Mitarbeiter",
"sort_order": 1
}
```
### Datenkategorie (hierarchisch)
```json
{
"id": "IDENTIFICATION",
"label_de": "Identifikationsdaten",
"children": [
{
"id": "NAME",
"parent_id": "IDENTIFICATION",
"label_de": "Name",
"is_art9": false,
"risk_weight": 1
},
{
"id": "DOB",
"parent_id": "IDENTIFICATION",
"label_de": "Geburtsdatum",
"is_art9": false,
"risk_weight": 2
}
]
}
```
### Loeschfrist (Retention Rule)
```json
{
"id": "HGB_257_10Y",
"label_de": "10 Jahre (HGB § 257)",
"legal_basis": "HGB § 257",
"duration": 10,
"duration_unit": "YEARS",
"start_event": "Ende des Kalenderjahres",
"deletion_procedure": "Vernichtung nach Ablauf der Aufbewahrungsfrist"
}
```
### Prozess-Template
```json
{
"id": "hr-mitarbeiterverwaltung",
"name": "Mitarbeiterverwaltung",
"business_function": "hr",
"purpose_refs": ["EMPLOYMENT_ADMIN", "PAYROLL"],
"legal_basis_refs": ["BDSG_26", "ART6_1B"],
"data_subject_refs": ["EMPLOYEES"],
"data_category_refs": ["NAME", "DOB", "ADDRESS", "CONTACT", "SOCIAL_SECURITY", "BANK_ACCOUNT", "EMPLOYMENT_DATA", "HEALTH_DATA"],
"recipient_refs": ["INTERNAL_HR", "INTERNAL_FINANCE", "PROCESSOR_PAYROLL"],
"tom_refs": ["AC_RBAC", "AC_NEED_TO_KNOW", "CONF_ENCRYPTION_REST", "INT_AUDIT_LOG"],
"retention_rule_ref": "HGB_257_10Y",
"typical_systems": ["HR-Software", "Personalakte (digital)"],
"protection_level": "HIGH",
"dpia_required": true,
"risk_score": 3,
"tags": ["personal", "pflicht"]
}
```
---
## Status-Workflow
```mermaid
stateDiagram-v2
[*] --> DRAFT : Neue Aktivitaet
DRAFT --> REVIEW : Zur Pruefung
REVIEW --> APPROVED : Genehmigt
REVIEW --> DRAFT : Zurueck zu Entwurf
APPROVED --> REVIEW : Erneute Pruefung
APPROVED --> ARCHIVED : Archivieren
ARCHIVED --> DRAFT : Reaktivieren
```
---
## DB-Tabellen
### Migrationshistorie
| Migration | Datei | Inhalt |
|-----------|-------|--------|
| 006 | `006_vvt.sql` | Initiale VVT-Tabellen (organization, activities, audit_log) |
| 033 | `033_vvt_consolidation.sql` | Schema-Konsolidierung |
| 035 | `035_vvt_tenant_isolation.sql` | Tenant-Isolation |
| **064** | `064_vvt_master_libraries.sql` | 8 Library-Tabellen (global) |
| **065** | `065_vvt_library_seed.sql` | Seed-Daten (~150 Eintraege) |
| **066** | `066_vvt_process_templates.sql` | Template-Tabelle + 13 neue Spalten auf `compliance_vvt_activities` |
| **067** | `067_vvt_process_templates_seed.sql` | 18 Prozess-Templates |
### Tabellen-Uebersicht
| Tabelle | Typ | Tenant-scoped | Eintraege |
|---------|-----|--------------|-----------|
| `compliance_vvt_organization` | Daten | Ja | 1 pro Tenant |
| `compliance_vvt_activities` | Daten | Ja | n pro Tenant |
| `compliance_vvt_audit_log` | Audit | Ja | Unbegrenzt |
| `vvt_lib_data_subjects` | Library | **Nein** (global) | 15 |
| `vvt_lib_data_categories` | Library | **Nein** (global) | 35 |
| `vvt_lib_recipients` | Library | **Nein** (global) | 15 |
| `vvt_lib_legal_bases` | Library | **Nein** (global) | 12 |
| `vvt_lib_retention_rules` | Library | **Nein** (global) | 12 |
| `vvt_lib_transfer_mechanisms` | Library | **Nein** (global) | 8 |
| `vvt_lib_purposes` | Library | **Nein** (global) | 20 |
| `vvt_lib_toms` | Library | **Nein** (global) | 20 |
| `vvt_process_templates` | Hybrid | System + Tenant | 18 (System) |
---
## Datei-Uebersicht
### Backend
| Datei | Beschreibung |
|-------|-------------|
| `compliance/db/vvt_models.py` | SQLAlchemy Models: Organization, Activity, AuditLog |
| `compliance/db/vvt_library_models.py` | SQLAlchemy Models: 8 Libraries + ProcessTemplate |
| `compliance/api/vvt_routes.py` | Activity CRUD, Export, Stats, Completeness |
| `compliance/api/vvt_library_routes.py` | Library GET-Endpoints, Template CRUD + Instantiate |
| `compliance/api/schemas.py` | Pydantic Schemas (VVTActivityCreate/Update/Response) |
### Frontend
| Datei | Beschreibung |
|-------|-------------|
| `admin-compliance/app/sdk/vvt/page.tsx` | VVT-Seite (3 Tabs: Verzeichnis, Editor, Export) |
| `admin-compliance/lib/sdk/vvt-types.ts` | TypeScript-Typen, Library-Interfaces, Helper |
| `admin-compliance/lib/sdk/vvt-profiling.ts` | Scope-basierte Generierung |
### Migrationen
| Datei | Beschreibung |
|-------|-------------|
| `migrations/064_vvt_master_libraries.sql` | 8 Library-Tabellen |
| `migrations/065_vvt_library_seed.sql` | Seed: ~150 Eintraege |
| `migrations/066_vvt_process_templates.sql` | Template-Tabelle + 13 Spalten auf Activities |
| `migrations/067_vvt_process_templates_seed.sql` | Seed: 18 Prozess-Templates |
---
## Tests
```bash
# Backend: Alle VVT-Tests (Library + Routes)
cd backend-compliance && python3 -m pytest tests/test_vvt_library_routes.py tests/test_vvt_routes.py -v
# Backend: Nur Library-Tests (54 Tests)
python3 -m pytest tests/test_vvt_library_routes.py -v
# Backend: Nur bestehende VVT-Tests (49 Tests)
python3 -m pytest tests/test_vvt_routes.py -v
# Frontend: Scope → VVT Integration (35 Tests)
cd admin-compliance && npx vitest run lib/sdk/__tests__/vvt-scope-integration.test.ts
```
### Testabdeckung
| Testdatei | Tests | Abdeckung |
|-----------|-------|-----------|
| `test_vvt_library_routes.py` | 54 | Models, Helpers, Endpoints, Completeness, Schema-Compat |
| `test_vvt_routes.py` | 49 | CRUD, Export, Stats, Audit, Organization |
| `vvt-scope-integration.test.ts` | 35 | Profile→Scope Prefill, Scope→VVT Export, Generator, Full Pipeline, Dept. Data Categories, Block 9 Mapping |
| **Gesamt** | **138** | |
---
## Frontend: 5-Tab-Aufbau
### Tab 1: Verzeichnis
- **Statistik-Kacheln:** Gesamt, Genehmigt, Entwurf, Drittland, Art. 9
- **Filter:** Status, Drittland, Art. 9
- **Suche:** VVT-ID, Name, Beschreibung
- **Sortierung:** Name, Datum, Status
- **"Aus Vorlage erstellen":** Template-Picker-Modal mit 18 Templates, filterbar nach Geschaeftsbereich
- **"Neue Verarbeitung":** Leere Aktivitaet erstellen
- **"Aus Scope generieren":** Automatische Generierung aus Compliance-Scope-Antworten
### Tab 2: Editor
- **Formular-Sections:** Grunddaten, Zwecke, Rechtsgrundlagen, Betroffene, Datenkategorien, Empfaenger, Drittlandtransfers, Loeschfristen, TOMs, Systeme, Datenquellen, Datenfluesse
- **Library-Unterstuetzung:** Dropdown-Auswahl aus Master-Libraries fuer alle strukturierten Felder
- **Freitext-Fallback:** Manuelle Eingabe bleibt immer moeglich
### Tab 3: Export & Compliance
- **Compliance-Check:** Pruefung aller Pflichtfelder nach Art. 30 DSGVO
- **Art. 30 Vollstaendigkeit:** Pro-Aktivitaet-Fortschrittsbalken
- **Organisations-Header:** DSB-Kontakt, Version, Pruefintervall
- **JSON-Export:** Vollstaendiger Export aller Aktivitaeten + Metadaten
- **CSV-Export:** Excel-kompatibel (Semikolon, UTF-8 BOM)
- **Statistik:** Zaehler nach Geschaeftsbereichen und Datenkategorien
### Tab 4: VVT-Dokument (Druckansicht)
Generiert ein vollstaendiges, druckbares VVT-Dokument gemaess Art. 30 Abs. 1 DSGVO:
- **Deckblatt:** Organisation, DSB-Kontakt, Versionierung, Pruefintervall, Erstellungsdatum
- **Inhaltsverzeichnis:** Automatisch nummerierte Eintraege aller Verarbeitungstaetigkeiten
- **Aktivitaeten-Tabellen:** Pro Aktivitaet eine zweispaltige Tabelle mit allen Pflichtfeldern:
- Zweck der Verarbeitung, Rechtsgrundlagen, Betroffene Personen
- Datenkategorien (mit Art. 9/10 Kennzeichnung), Empfaengerkategorien
- Drittlandtransfers mit Transfermechanismus
- Loeschfristen, TOMs, Systeme, Schutzbedarfsstufe
- **Library-Aufloesung:** IDs werden automatisch zu deutschen Labels aufgeloest (DATA_SUBJECT_CATEGORY_META, PERSONAL_DATA_CATEGORY_META, LEGAL_BASIS_META, TRANSFER_MECHANISM_META)
- **PDF-Druck:** Via `window.open()` + `window.print()` — eigenstaendiges HTML mit Inline-CSS und `@media print`-Regeln fuer Seitenumbrueche
- **HTML-Download:** Vollstaendiges Dokument als HTML-Datei speicherbar
### Tab 5: Auftragsverarbeiter (Art. 30 Abs. 2)
Eigenstaendiges Verzeichnis fuer Auftragsverarbeiter-Taetigkeiten gemaess Art. 30 Abs. 2 DSGVO:
- **Pflichtfelder (Art. 30 Abs. 2):**
- Name und Kontakt des Auftragsverarbeiters
- Kategorien der Verarbeitungen (fuer jeden Verantwortlichen)
- Unterauftragsverarbeiter-Kette (Name, Zweck, Land, Drittland-Kennzeichnung)
- Drittlandtransfers mit Transfermechanismen
- Technisch-Organisatorische Massnahmen (TOMs)
- **Editor:** Vollstaendiges Formular mit FormSection/FormField-Komponenten
- **Listenansicht:** Karten mit Auftraggeber, Anzahl Kategorien, Unterauftragnehmer, Status-Badge
- **Status-Workflow:** DRAFT → REVIEW → APPROVED → ARCHIVED
- **PDF-Druck:** Eigene Druckfunktion fuer das Auftragsverarbeiter-Verzeichnis
- **Rechtshinweis:** Infobox mit Erlaeuterung der Art. 30 Abs. 2 Anforderungen
!!! note "Datenhaltung"
Auftragsverarbeiter-Eintraege werden aktuell im Frontend-State verwaltet.
Backend-Persistenz (eigene DB-Tabelle + API-Endpoints) ist fuer Phase 2 geplant.
---
## Compliance-Kontext
| Verknuepftes Modul | Beziehung |
|-------------------|-----------|
| **Company Profile** | Stammdaten (DSB, Branche, Mitarbeiter, Angebote) fliessen via Scope in VVT |
| **Compliance Scope** | Scope-Antworten (Block 8+9) generieren VVT-Aktivitaeten per Knopfdruck |
| **DSFA** (Art. 35) | VVT-Aktivitaet referenziert DSFA ueber `dsfa_id` |
| **Loeschfristen** | Cross-Modul-Link ueber `linked_loeschfristen_ids` (Phase 2) |
| **TOM** (Art. 32) | Cross-Modul-Link ueber `linked_tom_measure_ids` (Phase 2) |
---
## Datenfluss: Company Profile → Compliance Scope → VVT Generator
Das VVT-Modul bezieht seine Daten aus einer **3-stufigen Pipeline**, in der keine Daten doppelt
abgefragt werden. Das Company Profile (`/sdk/company-profile`) dient als Single Source of Truth
fuer Stammdaten. Der Compliance Scope (`/sdk/compliance-scope`) fungiert als impliziter
**Daten-Verteilungs-Agent** — jede Scope-Frage deklariert per `mapsToVVTQuestion`-Eigenschaft,
welche VVT-Frage sie befuellt.
### Gesamtablauf
```mermaid
sequenceDiagram
participant CP as Company Profile
participant SE as Compliance Scope<br/>(Block 1-9)
participant VG as VVT Generator
participant DB as PostgreSQL
Note over CP: Stammdaten:<br/>DSB, Branche, Mitarbeiter,<br/>Angebote, Geschaeftsmodell
CP->>SE: prefillFromCompanyProfile(profile)<br/>+ getAutoFilledScoringAnswers(profile)
Note over SE: Auto-Prefill:<br/>dpoName → org_has_dsb<br/>offerings → prod_type<br/>employeeCount → org_employee_count
SE->>SE: Block 8: Abteilungswahl<br/>(personal, finanzen, marketing, ...)
SE->>SE: Block 9: Datenkategorien pro Abteilung<br/>(dk_dept_hr → NAME, SALARY_DATA, ...)
SE->>VG: exportToVVTAnswers(scopeAnswers)<br/>→ mapsToVVTQuestion-Mapping
Note over VG: prefillFromScopeAnswers():<br/>Scope-Antworten → ProfilingAnswers
VG->>VG: generateActivities(profilingAnswers)<br/>→ Template-Triggering + Enrichment
VG->>DB: POST /vvt/activities (je Aktivitaet)
Note over DB: VVT-Aktivitaeten gespeichert<br/>mit Library-Refs + Freitext
```
### Stufe 1: Company Profile → Scope (automatisches Prefill)
Beim Oeffnen des Compliance-Scope-Moduls werden Stammdaten aus dem Company Profile automatisch
in Scope-Antworten ueberfuehrt — der Nutzer muss diese Daten nicht erneut eingeben.
**Funktion:** `prefillFromCompanyProfile()` (`compliance-scope-profiling.ts:764`)
| Company Profile Feld | Scope-Frage | Mapping |
|----------------------|-------------|---------|
| `dpoName` (nicht leer) | `org_has_dsb` | `true` |
| `offerings` (WebApp) | `prod_type` | `['webapp']` |
| `offerings` (SaaS) | `prod_type` | `['saas']` |
| `offerings` (Webshop) | `prod_webshop` | `true` |
**Funktion:** `getAutoFilledScoringAnswers()` (`compliance-scope-profiling.ts:844`)
| Company Profile Feld | Scope-Frage | Zweck |
|----------------------|-------------|-------|
| `employeeCount` | `org_employee_count` | Scoring + Art. 30 Abs. 5 Pruefung |
| `annualRevenue` | `org_annual_revenue` | Scoring |
| `industry` | `org_industry` | Scoring + Branchenkontext |
| `businessModel` | `org_business_model` | Scoring |
### Stufe 2: Compliance Scope — Block 8 + Block 9
#### Block 8: Verarbeitungstaetigkeiten (Abteilungswahl)
Die Frage `vvt_departments` (Multi-Choice) bestimmt, welche Abteilungen personenbezogene Daten verarbeiten:
| Auswahl-Wert | Department-Key(s) | Beschreibung |
|--------------|-------------------|-------------|
| `personal` | `dept_hr`, `dept_recruiting` | Personal + Bewerbermanagement |
| `finanzen` | `dept_finance` | Finanzen & Buchhaltung |
| `vertrieb` | `dept_sales` | Vertrieb & CRM |
| `marketing` | `dept_marketing` | Marketing |
| `it` | `dept_it` | IT / Administration |
| `recht` | `dept_recht` | Recht / Compliance |
| `kundenservice` | `dept_support` | Kundenservice / Support |
| `produktion` | `dept_produktion` | Produktion / Fertigung |
| `logistik` | `dept_logistik` | Logistik / Versand |
| `einkauf` | `dept_einkauf` | Einkauf / Beschaffung |
| `facility` | `dept_facility` | Facility Management |
Das Mapping erfolgt in `ScopeWizardTab.tsx` ueber `DEPT_VALUE_TO_KEY`.
#### Block 9: Datenkategorien pro Abteilung
Fuer jede in Block 8 gewaehlte Abteilung wird eine eigene Frage mit spezifischen Datenkategorien angezeigt.
Die Datenkategorien stammen aus `DEPARTMENT_DATA_CATEGORIES` (`vvt-profiling.ts:306`).
**12 Abteilungen mit insgesamt ~80 Datenkategorien:**
| Abteilung | Scope-Frage | VVT-Mapping | Kategorien (Auswahl) | Art. 9 |
|-----------|-------------|-------------|---------------------|--------|
| Personal (HR) | `dk_dept_hr` | `dept_hr_categories` | Stammdaten, Gehalt, SV-Nr., Bankverbindung, Gesundheit, Religion | Gesundheit, Religion |
| Recruiting | `dk_dept_recruiting` | `dept_recruiting_categories` | Bewerberstammdaten, Bewerbungsunterlagen, Qualifikationen | Gesundheit |
| Finanzen | `dk_dept_finance` | `dept_finance_categories` | Kunden-/Lieferantendaten, Bankverbindungen, Steuer-IDs, Rechnungen | — |
| Vertrieb | `dk_dept_sales` | `dept_sales_categories` | Kontaktdaten, CRM-Daten, Kommunikation, Vertragsdaten | — |
| Marketing | `dk_dept_marketing` | `dept_marketing_categories` | E-Mail, Tracking, Consent, Social-Media, Interessenprofil | — |
| Support | `dk_dept_support` | `dept_support_categories` | Kundenstammdaten, Tickets, Kommunikation, Vertragsdaten | — |
| IT | `dk_dept_it` | `dept_it_categories` | Benutzerkonten, Logs, Geraete, Netzwerk, E-Mail, Backups | — |
| Recht | `dk_dept_recht` | `dept_recht_categories` | Vertraege, Compliance-Daten, Vorfaelle, Strafrechtliche Daten | Strafrechtlich |
| Produktion | `dk_dept_produktion` | `dept_produktion_categories` | Schichtplaene, Mitarbeiterdaten, Arbeitsschutz, Zugang | Gesundheit |
| Logistik | `dk_dept_logistik` | `dept_logistik_categories` | Empfaenger, Versandadressen, Sendungsverfolgung, Fahrer | — |
| Einkauf | `dk_dept_einkauf` | `dept_einkauf_categories` | Lieferantenkontakte, Vertraege, Bankverbindungen | — |
| Facility | `dk_dept_facility` | `dept_facility_categories` | Zutrittsdaten, Dienstleister, Videoueberwachung, Besucher | Gesundheit |
**Smart Auto-Prefill:** Beim erstmaligen Aufklappen einer Abteilungskarte werden automatisch
alle Kategorien mit `isTypical: true` vorausgewaehlt. Art.-9-Kategorien werden orange hervorgehoben.
### Stufe 3: Scope → VVT Generator
**Funktion:** `exportToVVTAnswers()` (`compliance-scope-profiling.ts:987`)
Jede Scope-Frage, die ein `mapsToVVTQuestion`-Attribut traegt, wird in das VVT-Antwort-Format
ueberfuehrt. Das Mapping ist **deklarativ** — kein imperativer Verteilungscode.
**Funktion:** `generateActivities()` (`vvt-profiling.ts:459`)
1. **Template-Triggering:** Jede VVT-Profiling-Frage hat ein `triggersTemplates`-Array. Wird eine Boolean-Frage mit `true` beantwortet, werden alle referenzierten Templates aktiviert.
2. **IT-Baseline:** Die 4 IT-Templates (Systemadministration, Backup, Logging, IAM) werden **immer** generiert.
3. **Enrichment:** `enrichActivityFromAnswers()` reichert Aktivitaeten an:
- `transfer_cloud_us = true` → US-Drittlandtransfer mit SCC + TIA auf jede Aktivitaet
- `data_health = true``HEALTH_DATA` + Art. 9 Rechtsgrundlage auf HR-Aktivitaeten
- `data_minors = true``MINORS` als Betroffenenkategorie auf Support/Engineering
- `special_ai / special_video_surveillance``dpiaRequired = true`
4. **Art. 30 Abs. 5 Pruefung:** `< 250 Mitarbeiter UND keine besonderen Kategorien → Ausnahme moeglich`
### 16 vorbefuellte Fragen (SCOPE_PREFILLED_VVT_QUESTIONS)
Diese VVT-Profiling-Fragen werden automatisch aus Scope-Antworten befuellt,
sodass der Nutzer sie nicht doppelt beantworten muss:
| VVT-Frage | Quelle (Scope-Block) |
|-----------|---------------------|
| `org_industry` | Block 1 (Organisation) |
| `org_employees` | Company Profile |
| `org_b2b_b2c` | Block 1 |
| `dept_hr` | Block 2/8 (Abteilungen) |
| `dept_finance` | Block 2/8 |
| `dept_marketing` | Block 2/8 |
| `data_health` | Block 2/4 (Datenkategorien) |
| `data_minors` | Block 2/4 |
| `data_biometric` | Block 2/4 |
| `data_criminal` | Block 2/4 |
| `special_ai` | Block 3/7 (Besondere Verarbeitungen) |
| `special_video_surveillance` | Block 3/4 |
| `special_tracking` | Block 3/4 |
| `transfer_cloud_us` | Block 4 (Drittland) |
| `transfer_subprocessor` | Block 4 |
| `transfer_support_non_eu` | Block 4 |
### Cross-Modul-Datenverteilung
Dasselbe deklarative Muster wird auch fuer andere Module verwendet:
| Modul | Export-Funktion | Mapping-Attribut |
|-------|----------------|-----------------|
| VVT | `exportToVVTAnswers()` | `mapsToVVTQuestion` |
| Loeschfristen | `exportToLoeschfristenAnswers()` | `mapsToLFQuestion` |
| TOM Generator | `exportToTOMProfile()` | (direkte Ableitung) |
**Bidirektionale Mappings:** `prefillFromVVTAnswers()` und `prefillFromLoeschfristenAnswers()`
ermoeglichen auch den Rueckfluss: Wenn ein Nutzer zuerst das VVT-Modul verwendet, koennen
diese Antworten in den Scope uebernommen werden.
---
## Datei-Uebersicht: Scope-Integration
| Datei | Beschreibung |
|-------|-------------|
| `lib/sdk/compliance-scope-profiling.ts` | Scope Engine: 9 Bloecke, Export-Funktionen, Company-Profile-Prefill |
| `lib/sdk/vvt-profiling.ts` | VVT-Profiling: 25 Fragen, 12 Abteilungs-Datenkategorien, Generator |
| `lib/sdk/vvt-baseline-catalog.ts` | 18 Baseline-Templates mit Freitext-Feldern |
| `components/sdk/compliance-scope/ScopeWizardTab.tsx` | Block-9-UI: Accordion, Auto-Prefill, Art.-9-Badges |
| `lib/sdk/__tests__/vvt-scope-integration.test.ts` | 35 Integrationstests fuer die gesamte Pipeline |
---
## Phase 2 — Geplante Erweiterungen
| Feature | Beschreibung |
|---------|-------------|
| Processor Records Backend | DB-Tabelle + API-Endpoints fuer Art. 30 Abs. 2 Auftragsverarbeiter |
| Link-Tabellen | M:N-Verknuepfungen VVT <-> Loeschfristen und VVT <-> TOM |
| Bidirektionaler Sync | Link-Operation aktualisiert auch Ziel-Modul |
| VVT Generator Service | Backend-basierte Auto-Fill Engine mit Company Profile + Templates |
| LibraryAutocomplete-Komponente | Wiederverwendbare Frontend-Komponente fuer alle Library-Felder |
| Word/DOCX-Export | Ergaenzung des PDF-Drucks um nativen Word-Export |