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:
72
admin-lehrer/lib/sdk/vendor-compliance/export/index.ts
Normal file
72
admin-lehrer/lib/sdk/vendor-compliance/export/index.ts
Normal 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'
|
||||
356
admin-lehrer/lib/sdk/vendor-compliance/export/ropa-export.ts
Normal file
356
admin-lehrer/lib/sdk/vendor-compliance/export/ropa-export.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
444
admin-lehrer/lib/sdk/vendor-compliance/export/vvt-export.ts
Normal file
444
admin-lehrer/lib/sdk/vendor-compliance/export/vvt-export.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user