refactor(admin-compliance): split 11 oversized files under 500 LOC hard cap (batch 2)
Barrel-split pattern: each original becomes a thin re-export barrel; logic moved to sibling files so no consumer imports need updating. Files split: - loeschfristen-profiling.ts → profiling-data.ts + profiling-generator.ts - vendor-compliance/catalog/vendor-templates.ts → vendor-country-profiles.ts - vendor-compliance/catalog/legal-basis.ts → legal-basis-retention.ts - dsfa/eu-legal-frameworks.ts → eu-legal-frameworks-national.ts - compliance-scope-types/document-scope-matrix-core.ts → core-part2.ts - compliance-scope-types/document-scope-matrix-extended.ts → extended-part2.ts - app/sdk/document-generator/contextBridge.ts → contextBridge-helpers.ts - app/api/sdk/drafting-engine/draft/route.ts → draft-helpers.ts + draft-helpers-v2.ts All files ≤ 500 LOC. Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
290
admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts
Normal file
290
admin-compliance/lib/sdk/loeschfristen-profiling-generator.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
// =============================================================================
|
||||
// Loeschfristen Module - Policy Generator
|
||||
// Generates deletion policies from profiling answers
|
||||
// =============================================================================
|
||||
|
||||
import type { LoeschfristPolicy, StorageLocation } from './loeschfristen-types'
|
||||
import { BASELINE_TEMPLATES, type BaselineTemplate, templateToPolicy } from './loeschfristen-baseline-catalog'
|
||||
import type { ProfilingAnswer, ProfilingResult } from './loeschfristen-profiling-data'
|
||||
import { PROFILING_STEPS } from './loeschfristen-profiling-data'
|
||||
|
||||
// Re-export ProfilingResult so callers can type-annotate
|
||||
export type { ProfilingResult }
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Retrieve the value of a specific answer by question ID.
|
||||
*/
|
||||
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: import('./loeschfristen-profiling-data').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
|
||||
|
||||
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>()
|
||||
|
||||
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')
|
||||
matchedTemplateIds.add('schulungsnachweise')
|
||||
}
|
||||
|
||||
// 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')
|
||||
matchedTemplateIds.add('kundenreklamationen')
|
||||
matchedTemplateIds.add('lieferantenbewertungen')
|
||||
}
|
||||
|
||||
// Marketing (data-marketing = true)
|
||||
if (getBool('data-marketing')) {
|
||||
matchedTemplateIds.add('newsletter-einwilligungen')
|
||||
matchedTemplateIds.add('crm-kontakthistorie')
|
||||
matchedTemplateIds.add('cookie-consent-logs')
|
||||
matchedTemplateIds.add('social-media-daten')
|
||||
}
|
||||
|
||||
// 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')
|
||||
}
|
||||
|
||||
// Cloud (sys-cloud = true) → E-Mail-Archivierung
|
||||
if (getBool('sys-cloud')) {
|
||||
matchedTemplateIds.add('email-archivierung')
|
||||
}
|
||||
|
||||
// Zutritt (sys-zutritt = true)
|
||||
if (getBool('sys-zutritt')) {
|
||||
matchedTemplateIds.add('zutrittsprotokolle')
|
||||
}
|
||||
|
||||
// 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')) {
|
||||
matchedTemplateIds.add('krankmeldungen')
|
||||
matchedTemplateIds.add('betriebsarzt-doku')
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
for (const policy of generatedPolicies) {
|
||||
policy.storageLocations.push({ ...backupLocation, id: crypto.randomUUID() })
|
||||
}
|
||||
}
|
||||
|
||||
// Legal Hold
|
||||
const hasLegalHoldRequirement = getBool('special-legal-hold')
|
||||
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',
|
||||
]
|
||||
Reference in New Issue
Block a user