// Loeschfristen Profiling — utility functions // Data (types + PROFILING_STEPS) lives in loeschfristen-profiling-data.ts import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types' import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog' import type { ProfilingAnswer, ProfilingStepId, ProfilingResult } from './loeschfristen-profiling-data' // Re-export types + data so existing imports work unchanged export { type ProfilingStepId, type ProfilingQuestion, type ProfilingAnswer, type ProfilingStep, type ProfilingResult, PROFILING_STEPS } from './loeschfristen-profiling-data' export function getAnswerValue(answers: ProfilingAnswer[], questionId: string): unknown { const answer = answers.find(a => a.questionId === questionId) return answer?.value ?? undefined } /** * Check whether all required questions in a given step have been answered. */ export function isStepComplete(answers: ProfilingAnswer[], stepId: ProfilingStepId): boolean { const step = PROFILING_STEPS.find(s => s.id === stepId) if (!step) return false return step.questions .filter(q => q.required) .every(q => { const answer = answers.find(a => a.questionId === q.id) if (!answer) return false // Check that the value is not empty const val = answer.value if (val === undefined || val === null) return false if (typeof val === 'string' && val.trim() === '') return false if (Array.isArray(val) && val.length === 0) return false return true }) } /** * Calculate overall profiling progress as a percentage (0-100). */ export function getProfilingProgress(answers: ProfilingAnswer[]): number { const totalRequired = PROFILING_STEPS.reduce( (sum, step) => sum + step.questions.filter(q => q.required).length, 0 ) if (totalRequired === 0) return 100 const answeredRequired = PROFILING_STEPS.reduce((sum, step) => { return ( sum + step.questions.filter(q => q.required).filter(q => { const answer = answers.find(a => a.questionId === q.id) if (!answer) return false const val = answer.value if (val === undefined || val === null) return false if (typeof val === 'string' && val.trim() === '') return false if (Array.isArray(val) && val.length === 0) return false return true }).length ) }, 0) return Math.round((answeredRequired / totalRequired) * 100) } // ============================================================================= // CORE GENERATOR // ============================================================================= /** * Generate deletion policies based on the profiling answers. * * Logic: * - Match baseline templates based on boolean and categorical answers * - Deduplicate matched templates by templateId * - Convert matched templates to full LoeschfristPolicy objects * - Add additional storage locations (Cloud, Backup) if applicable * - Detect legal hold requirements */ export function generatePoliciesFromProfile(answers: ProfilingAnswer[]): ProfilingResult { const matchedTemplateIds = new Set() // ------------------------------------------------------------------------- // Helper to get a boolean answer // ------------------------------------------------------------------------- const getBool = (questionId: string): boolean => { const val = getAnswerValue(answers, questionId) return val === true } const getString = (questionId: string): string => { const val = getAnswerValue(answers, questionId) return typeof val === 'string' ? val : '' } // ------------------------------------------------------------------------- // Always-included templates (universally recommended) // ------------------------------------------------------------------------- matchedTemplateIds.add('protokolle-gesellschafter') // ------------------------------------------------------------------------- // HR data (data-hr = true) // ------------------------------------------------------------------------- if (getBool('data-hr')) { matchedTemplateIds.add('personal-akten') matchedTemplateIds.add('gehaltsabrechnungen') matchedTemplateIds.add('zeiterfassung') matchedTemplateIds.add('bewerbungsunterlagen') matchedTemplateIds.add('krankmeldungen') } // ------------------------------------------------------------------------- // Buchhaltung (data-buchhaltung = true) // ------------------------------------------------------------------------- if (getBool('data-buchhaltung')) { matchedTemplateIds.add('buchhaltungsbelege') matchedTemplateIds.add('rechnungen') matchedTemplateIds.add('steuererklaerungen') } // ------------------------------------------------------------------------- // Vertraege (data-vertraege = true) // ------------------------------------------------------------------------- if (getBool('data-vertraege')) { matchedTemplateIds.add('vertraege') matchedTemplateIds.add('geschaeftsbriefe') matchedTemplateIds.add('kundenstammdaten') } // ------------------------------------------------------------------------- // Marketing (data-marketing = true) // ------------------------------------------------------------------------- if (getBool('data-marketing')) { matchedTemplateIds.add('newsletter-einwilligungen') matchedTemplateIds.add('crm-kontakthistorie') matchedTemplateIds.add('cookie-consent-logs') } // ------------------------------------------------------------------------- // Video (data-video = true) // ------------------------------------------------------------------------- if (getBool('data-video')) { matchedTemplateIds.add('videoueberwachung') } // ------------------------------------------------------------------------- // Website (org-website = true) // ------------------------------------------------------------------------- if (getBool('org-website')) { matchedTemplateIds.add('webserver-logs') matchedTemplateIds.add('cookie-consent-logs') } // ------------------------------------------------------------------------- // ERP/CRM (sys-erp = true) // ------------------------------------------------------------------------- if (getBool('sys-erp')) { matchedTemplateIds.add('kundenstammdaten') matchedTemplateIds.add('crm-kontakthistorie') } // ------------------------------------------------------------------------- // Backup (sys-backup = true) // ------------------------------------------------------------------------- if (getBool('sys-backup')) { matchedTemplateIds.add('backup-daten') } // ------------------------------------------------------------------------- // Gesundheitsdaten (special-gesundheit = true) // ------------------------------------------------------------------------- if (getBool('special-gesundheit')) { // Ensure krankmeldungen is included even without full HR data matchedTemplateIds.add('krankmeldungen') } // ------------------------------------------------------------------------- // Resolve matched templates from catalog // ------------------------------------------------------------------------- const matchedTemplates: BaselineTemplate[] = [] for (const templateId of matchedTemplateIds) { const template = BASELINE_TEMPLATES.find(t => t.templateId === templateId) if (template) { matchedTemplates.push(template) } } // ------------------------------------------------------------------------- // Convert to policies // ------------------------------------------------------------------------- const generatedPolicies: LoeschfristPolicy[] = matchedTemplates.map(template => templateToPolicy(template) ) // ------------------------------------------------------------------------- // Additional storage locations // ------------------------------------------------------------------------- const additionalStorageLocations: StorageLocation[] = [] if (getBool('sys-cloud')) { const cloudLocation: StorageLocation = { id: crypto.randomUUID(), name: 'Cloud-Speicher', type: 'CLOUD', isBackup: false, provider: null, deletionCapable: true, } additionalStorageLocations.push(cloudLocation) // Add Cloud storage location to all generated policies for (const policy of generatedPolicies) { policy.storageLocations.push({ ...cloudLocation, id: crypto.randomUUID() }) } } if (getBool('sys-backup')) { const backupLocation: StorageLocation = { id: crypto.randomUUID(), name: 'Backup-System', type: 'BACKUP', isBackup: true, provider: null, deletionCapable: true, } additionalStorageLocations.push(backupLocation) // Add Backup storage location to all generated policies for (const policy of generatedPolicies) { policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() }) } } // ------------------------------------------------------------------------- // Legal Hold // ------------------------------------------------------------------------- const hasLegalHoldRequirement = getBool('special-legal-hold') // If legal hold is active, mark all generated policies accordingly if (hasLegalHoldRequirement) { for (const policy of generatedPolicies) { policy.hasActiveLegalHold = true policy.deletionTrigger = 'LEGAL_HOLD' } } // ------------------------------------------------------------------------- // Tag policies with profiling metadata // ------------------------------------------------------------------------- const branche = getString('org-branche') const mitarbeiter = getString('org-mitarbeiter') for (const policy of generatedPolicies) { policy.tags = [ ...policy.tags, 'profiling-generated', ...(branche ? [`branche:${branche}`] : []), ...(mitarbeiter ? [`groesse:${mitarbeiter}`] : []), ] } return { matchedTemplates, generatedPolicies, additionalStorageLocations, hasLegalHoldRequirement, } } // ============================================================================= // COMPLIANCE SCOPE INTEGRATION // ============================================================================= /** * Prefill Loeschfristen profiling answers from Compliance Scope Engine answers. * The Scope Engine acts as the "Single Source of Truth" for organizational questions. */ export function prefillFromScopeAnswers( scopeAnswers: import('./compliance-scope-types').ScopeProfilingAnswer[] ): ProfilingAnswer[] { const { exportToLoeschfristenAnswers } = require('./compliance-scope-profiling') const exported = exportToLoeschfristenAnswers(scopeAnswers) as Array<{ questionId: string; value: unknown }> return exported.map(item => ({ questionId: item.questionId, value: item.value as string | string[] | boolean | number, })) } /** * Get the list of Loeschfristen question IDs that are prefilled from Scope answers. * These questions should show "Aus Scope-Analyse uebernommen" hint. */ export const SCOPE_PREFILLED_LF_QUESTIONS = [ 'org-branche', 'org-mitarbeiter', 'org-geschaeftsmodell', 'org-website', 'data-hr', 'data-buchhaltung', 'data-vertraege', 'data-marketing', 'data-video', 'sys-cloud', 'sys-erp', ]