Initial commit: breakpilot-lehrer - Lehrer KI Platform

Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
/**
* Export Utilities
*
* Functions for generating compliance reports and exports:
* - VVT Export (Art. 30 DSGVO)
* - RoPA Export (Art. 30(2) DSGVO)
* - Vendor Audit Pack
* - Management Summary
*/
// ==========================================
// VVT EXPORT
// ==========================================
export {
// Types
type VVTExportOptions,
type VVTExportResult,
type VVTRow,
// Functions
transformToVVTRows,
generateVVTJson,
generateVVTCsv,
getLocalizedText,
formatDataSubjects,
formatPersonalData,
formatLegalBasis,
formatRecipients,
formatTransfers,
formatRetention,
hasSpecialCategoryData,
hasThirdCountryTransfers,
generateComplianceSummary,
} from './vvt-export'
// ==========================================
// VENDOR AUDIT PACK
// ==========================================
export {
// Types
type VendorAuditPackOptions,
type VendorAuditSection,
type VendorAuditPackResult,
// Functions
generateVendorOverview,
generateContactsSection,
generateLocationsSection,
generateTransferSection,
generateCertificationsSection,
generateContractsSection,
generateFindingsSection,
generateControlStatusSection,
generateRiskSection,
generateReviewScheduleSection,
generateVendorAuditPack,
generateVendorAuditJson,
} from './vendor-audit-pack'
// ==========================================
// ROPA EXPORT
// ==========================================
export {
// Types
type RoPAExportOptions,
type RoPARow,
type RoPAExportResult,
// Functions
transformToRoPARows,
generateRoPAJson,
generateRoPACsv,
generateProcessorSummary,
validateRoPACompleteness,
} from './ropa-export'

View File

@@ -0,0 +1,356 @@
/**
* RoPA (Records of Processing Activities) Export Utilities
*
* Functions for generating Art. 30(2) DSGVO compliant
* processor-perspective records.
*/
import type {
ProcessingActivity,
Vendor,
Organization,
LocalizedText,
ThirdCountryTransfer,
} from '../types'
// ==========================================
// TYPES
// ==========================================
export interface RoPAExportOptions {
activities: ProcessingActivity[]
vendors: Vendor[]
organization: Organization
language: 'de' | 'en'
format: 'PDF' | 'DOCX' | 'XLSX'
includeSubProcessors?: boolean
includeTransfers?: boolean
}
export interface RoPARow {
// Art. 30(2)(a) - Controller info
controllerName: string
controllerAddress: string
controllerDPO: string
// Art. 30(2)(b) - Processing categories
processingCategories: string[]
// Art. 30(2)(c) - Third country transfers
thirdCountryTransfers: string[]
transferMechanisms: string[]
// Art. 30(2)(d) - Technical measures (general description)
technicalMeasures: string[]
// Additional info
subProcessors: string[]
status: string
}
export interface RoPAExportResult {
success: boolean
filename: string
mimeType: string
content: string
metadata: {
controllerCount: number
processingCount: number
generatedAt: Date
language: string
processorName: string
}
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================
function getLocalizedText(text: LocalizedText | undefined, lang: 'de' | 'en'): string {
if (!text) return ''
return text[lang] || text.de || ''
}
function formatTransfers(transfers: ThirdCountryTransfer[], lang: 'de' | 'en'): string[] {
const mechanismLabels: Record<string, LocalizedText> = {
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
BCR: { de: 'Binding Corporate Rules', en: 'Binding Corporate Rules' },
DEROGATION_CONSENT: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
DEROGATION_CONTRACT: { de: 'Vertragserfüllung', en: 'Contract Performance' },
CERTIFICATION: { de: 'Zertifizierung', en: 'Certification' },
}
return transfers.map((t) => {
const mechanism = mechanismLabels[t.transferMechanism]?.[lang] || t.transferMechanism
return `${t.country}: ${t.recipient} (${mechanism})`
})
}
function formatAddress(address: { street?: string; city?: string; postalCode?: string; country?: string }): string {
if (!address) return '-'
const parts = [address.street, address.postalCode, address.city, address.country].filter(Boolean)
return parts.join(', ') || '-'
}
// ==========================================
// EXPORT FUNCTIONS
// ==========================================
/**
* Transform activities to RoPA rows from processor perspective
* Groups by controller (responsible party)
*/
export function transformToRoPARows(
activities: ProcessingActivity[],
vendors: Vendor[],
lang: 'de' | 'en'
): RoPARow[] {
// Group activities by controller
const byController = new Map<string, ProcessingActivity[]>()
for (const activity of activities) {
const controllerName = activity.responsible.organizationName
if (!byController.has(controllerName)) {
byController.set(controllerName, [])
}
byController.get(controllerName)!.push(activity)
}
// Transform to RoPA rows
const rows: RoPARow[] = []
for (const [controllerName, controllerActivities] of byController) {
const firstActivity = controllerActivities[0]
// Collect all processing categories
const processingCategories = controllerActivities.map((a) =>
getLocalizedText(a.name, lang)
)
// Collect all third country transfers
const allTransfers = controllerActivities.flatMap((a) => a.thirdCountryTransfers)
const uniqueTransfers = formatTransfers(
allTransfers.filter(
(t, i, arr) => arr.findIndex((x) => x.country === t.country && x.recipient === t.recipient) === i
),
lang
)
// Collect unique transfer mechanisms
const uniqueMechanisms = [...new Set(allTransfers.map((t) => t.transferMechanism))]
// Collect all TOM references
const allTOM = [...new Set(controllerActivities.flatMap((a) => a.technicalMeasures))]
// Collect sub-processors from vendors
const subProcessorIds = [...new Set(controllerActivities.flatMap((a) => a.subProcessors))]
const subProcessorNames = subProcessorIds
.map((id) => vendors.find((v) => v.id === id)?.name)
.filter((name): name is string => !!name)
// DPO contact
const dpoContact = firstActivity.dpoContact
? `${firstActivity.dpoContact.name} (${firstActivity.dpoContact.email})`
: firstActivity.responsible.contact
? `${firstActivity.responsible.contact.name} (${firstActivity.responsible.contact.email})`
: '-'
rows.push({
controllerName,
controllerAddress: formatAddress(firstActivity.responsible.address),
controllerDPO: dpoContact,
processingCategories,
thirdCountryTransfers: uniqueTransfers,
transferMechanisms: uniqueMechanisms,
technicalMeasures: allTOM,
subProcessors: subProcessorNames,
status: controllerActivities.every((a) => a.status === 'APPROVED') ? 'APPROVED' : 'PENDING',
})
}
return rows
}
/**
* Generate RoPA as JSON
*/
export function generateRoPAJson(options: RoPAExportOptions): RoPAExportResult {
const rows = transformToRoPARows(options.activities, options.vendors, options.language)
const exportData = {
metadata: {
processor: {
name: options.organization.name,
address: formatAddress(options.organization.address),
dpo: options.organization.dpoContact
? `${options.organization.dpoContact.name} (${options.organization.dpoContact.email})`
: '-',
},
generatedAt: new Date().toISOString(),
language: options.language,
gdprArticle: 'Art. 30(2) DSGVO',
version: '1.0',
},
records: rows.map((row, index) => ({
recordNumber: index + 1,
...row,
})),
summary: {
controllerCount: rows.length,
totalProcessingCategories: rows.reduce((sum, r) => sum + r.processingCategories.length, 0),
withThirdCountryTransfers: rows.filter((r) => r.thirdCountryTransfers.length > 0).length,
uniqueSubProcessors: [...new Set(rows.flatMap((r) => r.subProcessors))].length,
},
}
const content = JSON.stringify(exportData, null, 2)
return {
success: true,
filename: `RoPA_${options.organization.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.json`,
mimeType: 'application/json',
content,
metadata: {
controllerCount: rows.length,
processingCount: options.activities.length,
generatedAt: new Date(),
language: options.language,
processorName: options.organization.name,
},
}
}
/**
* Generate RoPA as CSV
*/
export function generateRoPACsv(options: RoPAExportOptions): string {
const rows = transformToRoPARows(options.activities, options.vendors, options.language)
const lang = options.language
const headers =
lang === 'de'
? [
'Nr.',
'Verantwortlicher',
'Anschrift',
'DSB',
'Verarbeitungskategorien',
'Drittlandtransfers',
'Transfermechanismen',
'TOM',
'Unterauftragnehmer',
'Status',
]
: [
'No.',
'Controller',
'Address',
'DPO',
'Processing Categories',
'Third Country Transfers',
'Transfer Mechanisms',
'Technical Measures',
'Sub-Processors',
'Status',
]
const csvRows = rows.map((row, index) => [
(index + 1).toString(),
row.controllerName,
row.controllerAddress,
row.controllerDPO,
row.processingCategories.join('; '),
row.thirdCountryTransfers.join('; '),
row.transferMechanisms.join('; '),
row.technicalMeasures.join('; '),
row.subProcessors.join('; '),
row.status,
])
const escape = (val: string) => `"${val.replace(/"/g, '""')}"`
return [
headers.map(escape).join(','),
...csvRows.map((row) => row.map(escape).join(',')),
].join('\n')
}
/**
* Generate processor summary for RoPA
*/
export function generateProcessorSummary(
activities: ProcessingActivity[],
vendors: Vendor[],
lang: 'de' | 'en'
): {
totalControllers: number
totalCategories: number
withTransfers: number
subProcessorCount: number
pendingApproval: number
} {
const rows = transformToRoPARows(activities, vendors, lang)
return {
totalControllers: rows.length,
totalCategories: rows.reduce((sum, r) => sum + r.processingCategories.length, 0),
withTransfers: rows.filter((r) => r.thirdCountryTransfers.length > 0).length,
subProcessorCount: [...new Set(rows.flatMap((r) => r.subProcessors))].length,
pendingApproval: rows.filter((r) => r.status !== 'APPROVED').length,
}
}
/**
* Validate RoPA completeness
*/
export function validateRoPACompleteness(
rows: RoPARow[],
lang: 'de' | 'en'
): { isComplete: boolean; issues: string[] } {
const issues: string[] = []
for (const row of rows) {
// Art. 30(2)(a) - Controller info
if (!row.controllerName) {
issues.push(
lang === 'de'
? 'Name des Verantwortlichen fehlt'
: 'Controller name missing'
)
}
// Art. 30(2)(b) - Processing categories
if (row.processingCategories.length === 0) {
issues.push(
lang === 'de'
? `${row.controllerName}: Keine Verarbeitungskategorien angegeben`
: `${row.controllerName}: No processing categories specified`
)
}
// Art. 30(2)(c) - Transfers without mechanism
if (row.thirdCountryTransfers.length > 0 && row.transferMechanisms.length === 0) {
issues.push(
lang === 'de'
? `${row.controllerName}: Drittlandtransfer ohne Rechtsgrundlage`
: `${row.controllerName}: Third country transfer without legal basis`
)
}
// Art. 30(2)(d) - TOM
if (row.technicalMeasures.length === 0) {
issues.push(
lang === 'de'
? `${row.controllerName}: Keine TOM angegeben`
: `${row.controllerName}: No technical measures specified`
)
}
}
return {
isComplete: issues.length === 0,
issues,
}
}

View File

@@ -0,0 +1,489 @@
/**
* Vendor Audit Pack Export Utilities
*
* Functions for generating comprehensive vendor audit documentation.
*/
import type {
Vendor,
ContractDocument,
Finding,
ControlInstance,
RiskAssessment,
LocalizedText,
} from '../types'
// ==========================================
// TYPES
// ==========================================
export interface VendorAuditPackOptions {
vendor: Vendor
contracts: ContractDocument[]
findings: Finding[]
controlInstances: ControlInstance[]
riskAssessment?: RiskAssessment
language: 'de' | 'en'
format: 'PDF' | 'DOCX'
includeContracts?: boolean
includeFindings?: boolean
includeControlStatus?: boolean
includeRiskAssessment?: boolean
}
export interface VendorAuditSection {
title: string
content: string | Record<string, unknown>
level: 1 | 2 | 3
}
export interface VendorAuditPackResult {
success: boolean
filename: string
sections: VendorAuditSection[]
metadata: {
vendorName: string
generatedAt: Date
language: string
contractCount: number
findingCount: number
openFindingCount: number
riskLevel: string
}
}
// ==========================================
// CONSTANTS
// ==========================================
const VENDOR_ROLE_LABELS: Record<string, LocalizedText> = {
PROCESSOR: { de: 'Auftragsverarbeiter', en: 'Processor' },
JOINT_CONTROLLER: { de: 'Gemeinsam Verantwortlicher', en: 'Joint Controller' },
CONTROLLER: { de: 'Verantwortlicher', en: 'Controller' },
SUB_PROCESSOR: { de: 'Unterauftragnehmer', en: 'Sub-Processor' },
THIRD_PARTY: { de: 'Dritter', en: 'Third Party' },
}
const SERVICE_CATEGORY_LABELS: Record<string, LocalizedText> = {
HOSTING: { de: 'Hosting', en: 'Hosting' },
CLOUD_INFRASTRUCTURE: { de: 'Cloud-Infrastruktur', en: 'Cloud Infrastructure' },
ANALYTICS: { de: 'Analytics', en: 'Analytics' },
CRM: { de: 'CRM-System', en: 'CRM System' },
ERP: { de: 'ERP-System', en: 'ERP System' },
HR_SOFTWARE: { de: 'HR-Software', en: 'HR Software' },
PAYMENT: { de: 'Zahlungsabwicklung', en: 'Payment Processing' },
EMAIL: { de: 'E-Mail-Dienst', en: 'Email Service' },
MARKETING: { de: 'Marketing', en: 'Marketing' },
SUPPORT: { de: 'Support', en: 'Support' },
SECURITY: { de: 'Sicherheit', en: 'Security' },
INTEGRATION: { de: 'Integration', en: 'Integration' },
CONSULTING: { de: 'Beratung', en: 'Consulting' },
LEGAL: { de: 'Recht', en: 'Legal' },
ACCOUNTING: { de: 'Buchhaltung', en: 'Accounting' },
COMMUNICATION: { de: 'Kommunikation', en: 'Communication' },
STORAGE: { de: 'Speicher', en: 'Storage' },
OTHER: { de: 'Sonstiges', en: 'Other' },
}
const DATA_ACCESS_LABELS: Record<string, LocalizedText> = {
NONE: { de: 'Kein Zugriff', en: 'No Access' },
POTENTIAL: { de: 'Potentieller Zugriff', en: 'Potential Access' },
ADMINISTRATIVE: { de: 'Administrativer Zugriff', en: 'Administrative Access' },
CONTENT: { de: 'Inhaltlicher Zugriff', en: 'Content Access' },
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================
function getLabel(labels: Record<string, LocalizedText>, key: string, lang: 'de' | 'en'): string {
return labels[key]?.[lang] || key
}
function formatDate(date: Date | string | undefined, lang: 'de' | 'en'): string {
if (!date) return lang === 'de' ? 'Nicht angegeben' : 'Not specified'
const d = new Date(date)
return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
function getRiskLevelLabel(score: number, lang: 'de' | 'en'): string {
if (score >= 70) return lang === 'de' ? 'KRITISCH' : 'CRITICAL'
if (score >= 50) return lang === 'de' ? 'HOCH' : 'HIGH'
if (score >= 30) return lang === 'de' ? 'MITTEL' : 'MEDIUM'
return lang === 'de' ? 'NIEDRIG' : 'LOW'
}
// ==========================================
// SECTION GENERATORS
// ==========================================
/**
* Generate vendor overview section
*/
export function generateVendorOverview(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Vendor-Übersicht' : 'Vendor Overview'
const content = {
name: vendor.name,
legalForm: vendor.legalForm || '-',
country: vendor.country,
address: vendor.address
? `${vendor.address.street}, ${vendor.address.postalCode} ${vendor.address.city}`
: '-',
website: vendor.website || '-',
role: getLabel(VENDOR_ROLE_LABELS, vendor.role, lang),
serviceCategory: getLabel(SERVICE_CATEGORY_LABELS, vendor.serviceCategory, lang),
serviceDescription: vendor.serviceDescription,
dataAccessLevel: getLabel(DATA_ACCESS_LABELS, vendor.dataAccessLevel, lang),
status: vendor.status,
createdAt: formatDate(vendor.createdAt, lang),
}
return { title, content, level: 1 }
}
/**
* Generate contacts section
*/
export function generateContactsSection(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Kontaktdaten' : 'Contact Information'
const content = {
primaryContact: {
name: vendor.primaryContact.name,
email: vendor.primaryContact.email,
phone: vendor.primaryContact.phone || '-',
department: vendor.primaryContact.department || '-',
role: vendor.primaryContact.role || '-',
},
dpoContact: vendor.dpoContact
? {
name: vendor.dpoContact.name,
email: vendor.dpoContact.email,
phone: vendor.dpoContact.phone || '-',
}
: null,
}
return { title, content, level: 2 }
}
/**
* Generate processing locations section
*/
export function generateLocationsSection(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Verarbeitungsstandorte' : 'Processing Locations'
const locations = vendor.processingLocations.map((loc) => ({
country: loc.country,
region: loc.region || '-',
city: loc.city || '-',
dataCenter: loc.dataCenter || '-',
isEU: loc.isEU,
isAdequate: loc.isAdequate,
}))
return { title, content: { locations }, level: 2 }
}
/**
* Generate transfer mechanisms section
*/
export function generateTransferSection(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Drittlandtransfers' : 'Third Country Transfers'
const mechanismLabels: Record<string, LocalizedText> = {
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
BCR: { de: 'Binding Corporate Rules', en: 'Binding Corporate Rules' },
DEROGATION_CONSENT: { de: 'Ausdrückliche Einwilligung', en: 'Explicit Consent' },
DEROGATION_CONTRACT: { de: 'Vertragserfüllung', en: 'Contract Performance' },
CERTIFICATION: { de: 'Zertifizierung', en: 'Certification' },
CODE_OF_CONDUCT: { de: 'Verhaltensregeln', en: 'Code of Conduct' },
}
const mechanisms = vendor.transferMechanisms.map((tm) => ({
type: tm,
label: mechanismLabels[tm]?.[lang] || tm,
}))
return { title, content: { mechanisms }, level: 2 }
}
/**
* Generate certifications section
*/
export function generateCertificationsSection(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Zertifizierungen' : 'Certifications'
const certifications = vendor.certifications.map((cert) => ({
type: cert.type,
issuer: cert.issuer || '-',
issuedDate: cert.issuedDate ? formatDate(cert.issuedDate, lang) : '-',
expirationDate: cert.expirationDate ? formatDate(cert.expirationDate, lang) : '-',
scope: cert.scope || '-',
certificateNumber: cert.certificateNumber || '-',
}))
return { title, content: { certifications }, level: 2 }
}
/**
* Generate contracts section
*/
export function generateContractsSection(
contracts: ContractDocument[],
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Verträge' : 'Contracts'
const contractList = contracts.map((contract) => ({
type: contract.documentType,
fileName: contract.originalName,
version: contract.version,
effectiveDate: contract.effectiveDate ? formatDate(contract.effectiveDate, lang) : '-',
expirationDate: contract.expirationDate ? formatDate(contract.expirationDate, lang) : '-',
status: contract.status,
reviewStatus: contract.reviewStatus,
complianceScore: contract.complianceScore !== undefined ? `${contract.complianceScore}%` : '-',
}))
return { title, content: { contracts: contractList }, level: 1 }
}
/**
* Generate findings section
*/
export function generateFindingsSection(
findings: Finding[],
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Findings' : 'Findings'
const summary = {
total: findings.length,
open: findings.filter((f) => f.status === 'OPEN').length,
inProgress: findings.filter((f) => f.status === 'IN_PROGRESS').length,
resolved: findings.filter((f) => f.status === 'RESOLVED').length,
critical: findings.filter((f) => f.severity === 'CRITICAL').length,
high: findings.filter((f) => f.severity === 'HIGH').length,
medium: findings.filter((f) => f.severity === 'MEDIUM').length,
low: findings.filter((f) => f.severity === 'LOW').length,
}
const findingList = findings.map((finding) => ({
id: finding.id.slice(0, 8),
type: finding.type,
category: finding.category,
severity: finding.severity,
title: finding.title[lang] || finding.title.de,
description: finding.description[lang] || finding.description.de,
recommendation: finding.recommendation
? finding.recommendation[lang] || finding.recommendation.de
: '-',
status: finding.status,
affectedRequirement: finding.affectedRequirement || '-',
createdAt: formatDate(finding.createdAt, lang),
resolvedAt: finding.resolvedAt ? formatDate(finding.resolvedAt, lang) : '-',
}))
return { title, content: { summary, findings: findingList }, level: 1 }
}
/**
* Generate control status section
*/
export function generateControlStatusSection(
controlInstances: ControlInstance[],
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Control-Status' : 'Control Status'
const summary = {
total: controlInstances.length,
pass: controlInstances.filter((c) => c.status === 'PASS').length,
partial: controlInstances.filter((c) => c.status === 'PARTIAL').length,
fail: controlInstances.filter((c) => c.status === 'FAIL').length,
notApplicable: controlInstances.filter((c) => c.status === 'NOT_APPLICABLE').length,
planned: controlInstances.filter((c) => c.status === 'PLANNED').length,
}
const passRate =
summary.total > 0
? Math.round((summary.pass / (summary.total - summary.notApplicable)) * 100)
: 0
const controlList = controlInstances.map((ci) => ({
controlId: ci.controlId,
status: ci.status,
lastAssessedAt: formatDate(ci.lastAssessedAt, lang),
nextAssessmentDate: formatDate(ci.nextAssessmentDate, lang),
notes: ci.notes || '-',
}))
return {
title,
content: { summary, passRate: `${passRate}%`, controls: controlList },
level: 1,
}
}
/**
* Generate risk assessment section
*/
export function generateRiskSection(
vendor: Vendor,
riskAssessment: RiskAssessment | undefined,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Risikobewertung' : 'Risk Assessment'
const content = {
inherentRiskScore: vendor.inherentRiskScore,
inherentRiskLevel: getRiskLevelLabel(vendor.inherentRiskScore, lang),
residualRiskScore: vendor.residualRiskScore,
residualRiskLevel: getRiskLevelLabel(vendor.residualRiskScore, lang),
manualAdjustment: vendor.manualRiskAdjustment || 0,
justification: vendor.riskJustification || '-',
assessment: riskAssessment
? {
assessedBy: riskAssessment.assessedBy,
assessedAt: formatDate(riskAssessment.assessedAt, lang),
approvedBy: riskAssessment.approvedBy || '-',
approvedAt: riskAssessment.approvedAt
? formatDate(riskAssessment.approvedAt, lang)
: '-',
nextAssessmentDate: formatDate(riskAssessment.nextAssessmentDate, lang),
riskFactors: riskAssessment.riskFactors.map((rf) => ({
name: rf.name[lang] || rf.name.de,
category: rf.category,
value: rf.value,
weight: rf.weight,
rationale: rf.rationale || '-',
})),
}
: null,
}
return { title, content, level: 1 }
}
/**
* Generate review schedule section
*/
export function generateReviewScheduleSection(
vendor: Vendor,
lang: 'de' | 'en'
): VendorAuditSection {
const title = lang === 'de' ? 'Review-Zeitplan' : 'Review Schedule'
const frequencyLabels: Record<string, LocalizedText> = {
QUARTERLY: { de: 'Vierteljährlich', en: 'Quarterly' },
SEMI_ANNUAL: { de: 'Halbjährlich', en: 'Semi-Annual' },
ANNUAL: { de: 'Jährlich', en: 'Annual' },
BIENNIAL: { de: 'Alle 2 Jahre', en: 'Biennial' },
}
const content = {
reviewFrequency: getLabel(frequencyLabels, vendor.reviewFrequency, lang),
lastReviewDate: vendor.lastReviewDate ? formatDate(vendor.lastReviewDate, lang) : '-',
nextReviewDate: vendor.nextReviewDate ? formatDate(vendor.nextReviewDate, lang) : '-',
isOverdue:
vendor.nextReviewDate && new Date(vendor.nextReviewDate) < new Date()
? lang === 'de'
? 'Ja'
: 'Yes'
: lang === 'de'
? 'Nein'
: 'No',
}
return { title, content, level: 2 }
}
// ==========================================
// MAIN EXPORT FUNCTION
// ==========================================
/**
* Generate complete vendor audit pack
*/
export function generateVendorAuditPack(options: VendorAuditPackOptions): VendorAuditPackResult {
const { vendor, contracts, findings, controlInstances, riskAssessment, language } = options
const sections: VendorAuditSection[] = []
// Always include vendor overview
sections.push(generateVendorOverview(vendor, language))
sections.push(generateContactsSection(vendor, language))
sections.push(generateLocationsSection(vendor, language))
sections.push(generateTransferSection(vendor, language))
sections.push(generateCertificationsSection(vendor, language))
sections.push(generateReviewScheduleSection(vendor, language))
// Contracts (optional)
if (options.includeContracts !== false && contracts.length > 0) {
sections.push(generateContractsSection(contracts, language))
}
// Findings (optional)
if (options.includeFindings !== false && findings.length > 0) {
sections.push(generateFindingsSection(findings, language))
}
// Control status (optional)
if (options.includeControlStatus !== false && controlInstances.length > 0) {
sections.push(generateControlStatusSection(controlInstances, language))
}
// Risk assessment (optional)
if (options.includeRiskAssessment !== false) {
sections.push(generateRiskSection(vendor, riskAssessment, language))
}
// Calculate metadata
const openFindings = findings.filter((f) => f.status === 'OPEN').length
return {
success: true,
filename: `Vendor_Audit_${vendor.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.${options.format.toLowerCase()}`,
sections,
metadata: {
vendorName: vendor.name,
generatedAt: new Date(),
language,
contractCount: contracts.length,
findingCount: findings.length,
openFindingCount: openFindings,
riskLevel: getRiskLevelLabel(vendor.inherentRiskScore, language),
},
}
}
/**
* Generate vendor audit pack as JSON
*/
export function generateVendorAuditJson(options: VendorAuditPackOptions): string {
const result = generateVendorAuditPack(options)
return JSON.stringify(result, null, 2)
}

View File

@@ -0,0 +1,444 @@
/**
* VVT Export Utilities
*
* Functions for generating Art. 30 DSGVO compliant
* Verarbeitungsverzeichnis (VVT) exports.
*/
import type {
ProcessingActivity,
Organization,
LocalizedText,
LegalBasis,
DataSubjectCategory,
PersonalDataCategory,
RecipientCategory,
ThirdCountryTransfer,
RetentionPeriod,
} from '../types'
// ==========================================
// TYPES
// ==========================================
export interface VVTExportOptions {
activities: ProcessingActivity[]
organization: Organization
language: 'de' | 'en'
format: 'PDF' | 'DOCX' | 'XLSX'
includeAnnexes?: boolean
includeRiskAssessment?: boolean
watermark?: string
}
export interface VVTExportResult {
success: boolean
filename: string
mimeType: string
content: Uint8Array | string
metadata: {
activityCount: number
generatedAt: Date
language: string
organization: string
}
}
export interface VVTRow {
vvtId: string
name: string
responsible: string
purposes: string[]
legalBasis: string[]
dataSubjects: string[]
personalData: string[]
recipients: string[]
thirdCountryTransfers: string[]
retentionPeriod: string
technicalMeasures: string[]
dpiaRequired: string
status: string
}
// ==========================================
// CONSTANTS
// ==========================================
const DATA_SUBJECT_LABELS: Record<DataSubjectCategory, LocalizedText> = {
EMPLOYEES: { de: 'Beschäftigte', en: 'Employees' },
APPLICANTS: { de: 'Bewerber', en: 'Job Applicants' },
CUSTOMERS: { de: 'Kunden', en: 'Customers' },
PROSPECTIVE_CUSTOMERS: { de: 'Interessenten', en: 'Prospective Customers' },
SUPPLIERS: { de: 'Lieferanten', en: 'Suppliers' },
BUSINESS_PARTNERS: { de: 'Geschäftspartner', en: 'Business Partners' },
VISITORS: { de: 'Besucher', en: 'Visitors' },
WEBSITE_USERS: { de: 'Website-Nutzer', en: 'Website Users' },
APP_USERS: { de: 'App-Nutzer', en: 'App Users' },
NEWSLETTER_SUBSCRIBERS: { de: 'Newsletter-Abonnenten', en: 'Newsletter Subscribers' },
MEMBERS: { de: 'Mitglieder', en: 'Members' },
PATIENTS: { de: 'Patienten', en: 'Patients' },
STUDENTS: { de: 'Schüler/Studenten', en: 'Students' },
MINORS: { de: 'Minderjährige', en: 'Minors' },
OTHER: { de: 'Sonstige', en: 'Other' },
}
const PERSONAL_DATA_LABELS: Record<PersonalDataCategory, LocalizedText> = {
NAME: { de: 'Name', en: 'Name' },
CONTACT: { de: 'Kontaktdaten', en: 'Contact Data' },
ADDRESS: { de: 'Adressdaten', en: 'Address' },
DOB: { de: 'Geburtsdatum', en: 'Date of Birth' },
ID_NUMBER: { de: 'Ausweisnummern', en: 'ID Numbers' },
SOCIAL_SECURITY: { de: 'Sozialversicherungsnummer', en: 'Social Security Number' },
TAX_ID: { de: 'Steuer-ID', en: 'Tax ID' },
BANK_ACCOUNT: { de: 'Bankverbindung', en: 'Bank Account' },
PAYMENT_DATA: { de: 'Zahlungsdaten', en: 'Payment Data' },
EMPLOYMENT_DATA: { de: 'Beschäftigungsdaten', en: 'Employment Data' },
SALARY_DATA: { de: 'Gehaltsdaten', en: 'Salary Data' },
EDUCATION_DATA: { de: 'Bildungsdaten', en: 'Education Data' },
PHOTO_VIDEO: { de: 'Fotos/Videos', en: 'Photos/Videos' },
IP_ADDRESS: { de: 'IP-Adressen', en: 'IP Addresses' },
DEVICE_ID: { de: 'Geräte-Kennungen', en: 'Device IDs' },
LOCATION_DATA: { de: 'Standortdaten', en: 'Location Data' },
USAGE_DATA: { de: 'Nutzungsdaten', en: 'Usage Data' },
COMMUNICATION_DATA: { de: 'Kommunikationsdaten', en: 'Communication Data' },
CONTRACT_DATA: { de: 'Vertragsdaten', en: 'Contract Data' },
LOGIN_DATA: { de: 'Login-Daten', en: 'Login Data' },
HEALTH_DATA: { de: 'Gesundheitsdaten (Art. 9)', en: 'Health Data (Art. 9)' },
GENETIC_DATA: { de: 'Genetische Daten (Art. 9)', en: 'Genetic Data (Art. 9)' },
BIOMETRIC_DATA: { de: 'Biometrische Daten (Art. 9)', en: 'Biometric Data (Art. 9)' },
RACIAL_ETHNIC: { de: 'Rassische/Ethnische Herkunft (Art. 9)', en: 'Racial/Ethnic Origin (Art. 9)' },
POLITICAL_OPINIONS: { de: 'Politische Meinungen (Art. 9)', en: 'Political Opinions (Art. 9)' },
RELIGIOUS_BELIEFS: { de: 'Religiöse Überzeugungen (Art. 9)', en: 'Religious Beliefs (Art. 9)' },
TRADE_UNION: { de: 'Gewerkschaftszugehörigkeit (Art. 9)', en: 'Trade Union Membership (Art. 9)' },
SEX_LIFE: { de: 'Sexualleben/Orientierung (Art. 9)', en: 'Sex Life/Orientation (Art. 9)' },
CRIMINAL_DATA: { de: 'Strafrechtliche Daten (Art. 10)', en: 'Criminal Data (Art. 10)' },
OTHER: { de: 'Sonstige', en: 'Other' },
}
const LEGAL_BASIS_LABELS: Record<string, LocalizedText> = {
CONSENT: { de: 'Einwilligung (Art. 6 Abs. 1 lit. a)', en: 'Consent (Art. 6(1)(a))' },
CONTRACT: { de: 'Vertragserfüllung (Art. 6 Abs. 1 lit. b)', en: 'Contract (Art. 6(1)(b))' },
LEGAL_OBLIGATION: { de: 'Rechtliche Verpflichtung (Art. 6 Abs. 1 lit. c)', en: 'Legal Obligation (Art. 6(1)(c))' },
VITAL_INTEREST: { de: 'Lebenswichtige Interessen (Art. 6 Abs. 1 lit. d)', en: 'Vital Interests (Art. 6(1)(d))' },
PUBLIC_TASK: { de: 'Öffentliche Aufgabe (Art. 6 Abs. 1 lit. e)', en: 'Public Task (Art. 6(1)(e))' },
LEGITIMATE_INTEREST: { de: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f)', en: 'Legitimate Interest (Art. 6(1)(f))' },
}
// ==========================================
// HELPER FUNCTIONS
// ==========================================
export function getLocalizedText(text: LocalizedText | undefined, lang: 'de' | 'en'): string {
if (!text) return ''
return text[lang] || text.de || ''
}
export function formatDataSubjects(categories: DataSubjectCategory[], lang: 'de' | 'en'): string[] {
return categories.map((cat) => DATA_SUBJECT_LABELS[cat]?.[lang] || cat)
}
export function formatPersonalData(categories: PersonalDataCategory[], lang: 'de' | 'en'): string[] {
return categories.map((cat) => PERSONAL_DATA_LABELS[cat]?.[lang] || cat)
}
export function formatLegalBasis(bases: LegalBasis[], lang: 'de' | 'en'): string[] {
return bases.map((basis) => {
const label = LEGAL_BASIS_LABELS[basis.type]?.[lang] || basis.type
return basis.description ? `${label}: ${basis.description}` : label
})
}
export function formatRecipients(recipients: RecipientCategory[], lang: 'de' | 'en'): string[] {
return recipients.map((r) => {
const suffix = r.isThirdCountry && r.country ? ` (${r.country})` : ''
return r.name + suffix
})
}
export function formatTransfers(transfers: ThirdCountryTransfer[], lang: 'de' | 'en'): string[] {
const labels: Record<string, LocalizedText> = {
ADEQUACY_DECISION: { de: 'Angemessenheitsbeschluss', en: 'Adequacy Decision' },
SCC_CONTROLLER: { de: 'SCC (C2C)', en: 'SCC (C2C)' },
SCC_PROCESSOR: { de: 'SCC (C2P)', en: 'SCC (C2P)' },
BCR: { de: 'BCR', en: 'BCR' },
}
return transfers.map((t) => {
const mechanism = labels[t.transferMechanism]?.[lang] || t.transferMechanism
return `${t.country}: ${t.recipient} (${mechanism})`
})
}
export function formatRetention(retention: RetentionPeriod | undefined, lang: 'de' | 'en'): string {
if (!retention) return lang === 'de' ? 'Nicht festgelegt' : 'Not specified'
// If description is available, use it
if (retention.description) {
const desc = retention.description[lang] || retention.description.de
if (desc) return desc
}
// Otherwise build from duration
if (!retention.duration) {
return lang === 'de' ? 'Nicht festgelegt' : 'Not specified'
}
const periodLabels: Record<string, LocalizedText> = {
DAYS: { de: 'Tage', en: 'days' },
MONTHS: { de: 'Monate', en: 'months' },
YEARS: { de: 'Jahre', en: 'years' },
}
const unit = periodLabels[retention.durationUnit || 'YEARS'][lang]
let result = `${retention.duration} ${unit}`
if (retention.legalBasis) {
result += ` (${retention.legalBasis})`
}
return result
}
// ==========================================
// EXPORT FUNCTIONS
// ==========================================
/**
* Transform processing activities to VVT rows for export
*/
export function transformToVVTRows(
activities: ProcessingActivity[],
lang: 'de' | 'en'
): VVTRow[] {
return activities.map((activity) => ({
vvtId: activity.vvtId,
name: getLocalizedText(activity.name, lang),
responsible: activity.responsible.organizationName,
purposes: activity.purposes.map((p) => getLocalizedText(p, lang)),
legalBasis: formatLegalBasis(activity.legalBasis, lang),
dataSubjects: formatDataSubjects(activity.dataSubjectCategories, lang),
personalData: formatPersonalData(activity.personalDataCategories, lang),
recipients: formatRecipients(activity.recipientCategories, lang),
thirdCountryTransfers: formatTransfers(activity.thirdCountryTransfers, lang),
retentionPeriod: formatRetention(activity.retentionPeriod, lang),
technicalMeasures: activity.technicalMeasures,
dpiaRequired: activity.dpiaRequired
? lang === 'de'
? 'Ja'
: 'Yes'
: lang === 'de'
? 'Nein'
: 'No',
status: activity.status,
}))
}
/**
* Generate VVT as JSON (for further processing)
*/
export function generateVVTJson(options: VVTExportOptions): VVTExportResult {
const rows = transformToVVTRows(options.activities, options.language)
const exportData = {
metadata: {
organization: options.organization.name,
generatedAt: new Date().toISOString(),
language: options.language,
activityCount: rows.length,
version: '1.0',
gdprArticle: 'Art. 30 DSGVO',
},
responsible: {
name: options.organization.name,
legalForm: options.organization.legalForm,
address: options.organization.address,
dpo: options.organization.dpoContact,
},
activities: rows,
}
const content = JSON.stringify(exportData, null, 2)
return {
success: true,
filename: `VVT_${options.organization.name.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.json`,
mimeType: 'application/json',
content,
metadata: {
activityCount: rows.length,
generatedAt: new Date(),
language: options.language,
organization: options.organization.name,
},
}
}
/**
* Generate VVT CSV content (for Excel compatibility)
*/
export function generateVVTCsv(options: VVTExportOptions): string {
const rows = transformToVVTRows(options.activities, options.language)
const lang = options.language
const headers = lang === 'de'
? [
'VVT-Nr.',
'Bezeichnung',
'Verantwortlicher',
'Zwecke',
'Rechtsgrundlage',
'Betroffene',
'Datenkategorien',
'Empfänger',
'Drittlandtransfers',
'Löschfristen',
'TOM',
'DSFA erforderlich',
'Status',
]
: [
'VVT ID',
'Name',
'Responsible',
'Purposes',
'Legal Basis',
'Data Subjects',
'Data Categories',
'Recipients',
'Third Country Transfers',
'Retention Period',
'Technical Measures',
'DPIA Required',
'Status',
]
const csvRows = rows.map((row) => [
row.vvtId,
row.name,
row.responsible,
row.purposes.join('; '),
row.legalBasis.join('; '),
row.dataSubjects.join('; '),
row.personalData.join('; '),
row.recipients.join('; '),
row.thirdCountryTransfers.join('; '),
row.retentionPeriod,
row.technicalMeasures.join('; '),
row.dpiaRequired,
row.status,
])
const escape = (val: string) => `"${val.replace(/"/g, '""')}"`
return [
headers.map(escape).join(','),
...csvRows.map((row) => row.map(escape).join(',')),
].join('\n')
}
/**
* Check if activities have special category data (Art. 9)
*/
export function hasSpecialCategoryData(activities: ProcessingActivity[]): boolean {
const specialCategories: PersonalDataCategory[] = [
'HEALTH_DATA',
'GENETIC_DATA',
'BIOMETRIC_DATA',
'RACIAL_ETHNIC',
'POLITICAL_OPINIONS',
'RELIGIOUS_BELIEFS',
'TRADE_UNION',
'SEX_LIFE',
]
return activities.some((activity) =>
activity.personalDataCategories.some((cat) => specialCategories.includes(cat))
)
}
/**
* Check if activities have third country transfers
*/
export function hasThirdCountryTransfers(activities: ProcessingActivity[]): boolean {
return activities.some((activity) => activity.thirdCountryTransfers.length > 0)
}
/**
* Generate compliance summary for VVT
*/
export function generateComplianceSummary(
activities: ProcessingActivity[],
lang: 'de' | 'en'
): {
totalActivities: number
byStatus: Record<string, number>
withSpecialCategories: number
withThirdCountryTransfers: number
dpiaRequired: number
issues: string[]
} {
const byStatus: Record<string, number> = {}
let withSpecialCategories = 0
let withThirdCountryTransfers = 0
let dpiaRequired = 0
const issues: string[] = []
for (const activity of activities) {
// Count by status
byStatus[activity.status] = (byStatus[activity.status] || 0) + 1
// Check for special categories
if (
activity.personalDataCategories.some((cat) =>
[
'HEALTH_DATA',
'GENETIC_DATA',
'BIOMETRIC_DATA',
'RACIAL_ETHNIC',
'POLITICAL_OPINIONS',
'RELIGIOUS_BELIEFS',
'TRADE_UNION',
'SEX_LIFE',
].includes(cat)
)
) {
withSpecialCategories++
}
// Check for third country transfers
if (activity.thirdCountryTransfers.length > 0) {
withThirdCountryTransfers++
}
// Check DPIA
if (activity.dpiaRequired) {
dpiaRequired++
}
// Check for issues
if (activity.legalBasis.length === 0) {
issues.push(
lang === 'de'
? `${activity.vvtId}: Keine Rechtsgrundlage angegeben`
: `${activity.vvtId}: No legal basis specified`
)
}
if (!activity.retentionPeriod) {
issues.push(
lang === 'de'
? `${activity.vvtId}: Keine Löschfrist angegeben`
: `${activity.vvtId}: No retention period specified`
)
}
}
return {
totalActivities: activities.length,
byStatus,
withSpecialCategories,
withThirdCountryTransfers,
dpiaRequired,
issues,
}
}