Files
breakpilot-compliance/admin-compliance/lib/sdk/loeschfristen-profiling.ts
Sharang Parnerkar ff775517a2 refactor(admin): split loeschfristen-profiling.ts (538 LOC) into data + logic
Types and PROFILING_STEPS data (242 LOC) extracted to
loeschfristen-profiling-data.ts. Functions remain in
loeschfristen-profiling.ts (306 LOC). Both under 500.

Barrel re-exports in the logic file so existing imports work unchanged.

next build passes.

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

307 lines
11 KiB
TypeScript

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