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:
@@ -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
510
admin-compliance/lib/sdk/__tests__/vvt-scope-integration.test.ts
Normal file
510
admin-compliance/lib/sdk/__tests__/vvt-scope-integration.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
@@ -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'],
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -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) {
|
||||
|
||||
827
admin-compliance/lib/sdk/loeschfristen-document.ts
Normal file
827
admin-compliance/lib/sdk/loeschfristen-document.ts
Normal 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 („Recht auf Vergessenwerden“) — 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
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 '-'
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
427
backend-compliance/compliance/api/vvt_library_routes.py
Normal file
427
backend-compliance/compliance/api/vvt_library_routes.py
Normal 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]
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
164
backend-compliance/compliance/db/vvt_library_models.py
Normal file
164
backend-compliance/compliance/db/vvt_library_models.py
Normal 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'),
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
105
backend-compliance/migrations/064_vvt_master_libraries.sql
Normal file
105
backend-compliance/migrations/064_vvt_master_libraries.sql
Normal 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;
|
||||
200
backend-compliance/migrations/065_vvt_library_seed.sql
Normal file
200
backend-compliance/migrations/065_vvt_library_seed.sql
Normal 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;
|
||||
54
backend-compliance/migrations/066_vvt_process_templates.sql
Normal file
54
backend-compliance/migrations/066_vvt_process_templates.sql
Normal 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;
|
||||
305
backend-compliance/migrations/067_vvt_process_templates_seed.sql
Normal file
305
backend-compliance/migrations/067_vvt_process_templates_seed.sql
Normal 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;
|
||||
1099
backend-compliance/tests/test_vvt_library_routes.py
Normal file
1099
backend-compliance/tests/test_vvt_library_routes.py
Normal file
File diff suppressed because it is too large
Load Diff
292
docs-src/services/sdk-modules/loeschfristen.md
Normal file
292
docs-src/services/sdk-modules/loeschfristen.md
Normal 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+) |
|
||||
759
docs-src/services/sdk-modules/vvt.md
Normal file
759
docs-src/services/sdk-modules/vvt.md
Normal 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 |
|
||||
Reference in New Issue
Block a user