/** * VVT Profiling — Generator Logic, Enrichment & Helpers * * Generates baseline VVT activities from profiling answers. */ import { VVT_BASELINE_CATALOG, templateToActivity } from './vvt-baseline-catalog' import { generateVVTId } from './vvt-types' import type { VVTActivity } from './vvt-types' import { PROFILING_QUESTIONS, type ProfilingAnswers, type ProfilingResult, } from './vvt-profiling-data' // ============================================================================= // GENERATOR LOGIC // ============================================================================= export function generateActivities(answers: ProfilingAnswers): ProfilingResult { const triggeredIds = new Set() for (const question of PROFILING_QUESTIONS) { const answer = answers[question.id] if (!answer) continue if (question.type === 'boolean' && answer === true) { question.triggersTemplates.forEach(id => triggeredIds.add(id)) } } // Always add IT baseline templates triggeredIds.add('it-systemadministration') triggeredIds.add('it-backup') triggeredIds.add('it-logging') triggeredIds.add('it-iam') const existingIds: string[] = [] const activities: VVTActivity[] = [] for (const templateId of triggeredIds) { const template = VVT_BASELINE_CATALOG.find(t => t.templateId === templateId) if (!template) continue const vvtId = generateVVTId(existingIds) existingIds.push(vvtId) const activity = templateToActivity(template, vvtId) enrichActivityFromAnswers(activity, answers) activities.push(activity) } // Calculate coverage score const totalFields = activities.length * 12 let filledFields = 0 for (const a of activities) { if (a.name) filledFields++ if (a.description) filledFields++ if (a.purposes.length > 0) filledFields++ if (a.legalBases.length > 0) filledFields++ if (a.dataSubjectCategories.length > 0) filledFields++ if (a.personalDataCategories.length > 0) filledFields++ if (a.recipientCategories.length > 0) filledFields++ if (a.retentionPeriod.description) filledFields++ if (a.tomDescription) filledFields++ if (a.businessFunction !== 'other') filledFields++ if (a.structuredToms.accessControl.length > 0) filledFields++ if (a.responsible || a.owner) filledFields++ } const coverageScore = totalFields > 0 ? Math.round((filledFields / totalFields) * 100) : 0 const employeeCount = typeof answers.org_employees === 'number' ? answers.org_employees : 0 const hasSpecialCategories = answers.data_health === true || answers.data_biometric === true || answers.data_criminal === true const art30Abs5Exempt = employeeCount < 250 && !hasSpecialCategories return { answers, generatedActivities: activities, coverageScore, art30Abs5Exempt, } } // ============================================================================= // ENRICHMENT // ============================================================================= function enrichActivityFromAnswers(activity: VVTActivity, answers: ProfilingAnswers): void { if (answers.transfer_cloud_us === true) { activity.thirdCountryTransfers.push({ country: 'US', recipient: 'Cloud-Dienstleister (USA)', transferMechanism: 'SCC_PROCESSOR', additionalMeasures: ['Verschluesselung at-rest', 'Transfer Impact Assessment'], }) } if (answers.data_health === true) { if (!activity.personalDataCategories.includes('HEALTH_DATA')) { if (activity.businessFunction === 'hr') { activity.personalDataCategories.push('HEALTH_DATA') if (!activity.legalBases.some(lb => lb.type.startsWith('ART9_'))) { activity.legalBases.push({ type: 'ART9_EMPLOYMENT', description: 'Arbeitsrechtliche Verarbeitung', reference: 'Art. 9 Abs. 2 lit. b DSGVO', }) } } } } if (answers.data_minors === true) { if (!activity.dataSubjectCategories.includes('MINORS')) { if (activity.businessFunction === 'support' || activity.businessFunction === 'product_engineering') { activity.dataSubjectCategories.push('MINORS') } } } if (answers.special_ai === true || answers.special_video_surveillance === true || answers.special_tracking === true) { if (answers.special_ai === true && activity.businessFunction === 'product_engineering') { activity.dpiaRequired = true } } } // ============================================================================= // HELPERS // ============================================================================= export function getQuestionsForStep(step: number) { return PROFILING_QUESTIONS.filter(q => q.step === step) } export function getStepProgress(answers: ProfilingAnswers, step: number): number { const questions = getQuestionsForStep(step) if (questions.length === 0) return 100 const answered = questions.filter(q => { const a = answers[q.id] return a !== undefined && a !== null && a !== '' }).length return Math.round((answered / questions.length) * 100) } export function getTotalProgress(answers: ProfilingAnswers): number { const total = PROFILING_QUESTIONS.length if (total === 0) return 100 const answered = PROFILING_QUESTIONS.filter(q => { const a = answers[q.id] return a !== undefined && a !== null && a !== '' }).length return Math.round((answered / total) * 100) } // ============================================================================= // COMPLIANCE SCOPE INTEGRATION // ============================================================================= export function prefillFromScopeAnswers( scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] ): ProfilingAnswers { const { exportToVVTAnswers } = require('./compliance-scope-profiling') const exported = exportToVVTAnswers(scopeAnswers) as Record const prefilled: ProfilingAnswers = {} for (const [key, value] of Object.entries(exported)) { if (value !== undefined && value !== null) { prefilled[key] = value as string | string[] | number | boolean } } return prefilled } export const SCOPE_PREFILLED_VVT_QUESTIONS = [ 'org_industry', 'org_employees', 'org_b2b_b2c', 'dept_hr', 'dept_finance', 'dept_marketing', 'data_health', 'data_minors', 'data_biometric', 'data_criminal', 'special_ai', 'special_video_surveillance', 'special_tracking', 'transfer_cloud_us', 'transfer_subprocessor', 'transfer_support_non_eu', ]