Files
breakpilot-compliance/admin-compliance/lib/sdk/vvt-profiling-logic.ts
Sharang Parnerkar 528abc86ab refactor(admin): split 8 oversized lib/ files into focused modules under 500 LOC
Split these files that exceeded the 500-line hard cap:
- privacy-policy.ts (965 LOC) -> sections + renderers
- academy/api.ts (787 LOC) -> courses + mock-data
- whistleblower/api.ts (755 LOC) -> operations + mock-data
- vvt-profiling.ts (659 LOC) -> data + logic
- cookie-banner.ts (595 LOC) -> config + embed
- dsr/types.ts (581 LOC) -> core + api types
- tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis
- datapoint-helpers.ts (548 LOC) -> generators + validators

Each original file becomes a barrel re-export for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:05:59 +02:00

188 lines
6.4 KiB
TypeScript

/**
* 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<string>()
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<string, unknown>
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',
]