// ============================================================================= // Obligations Module - Compliance Check Engine // Prueft Pflichten auf Vollstaendigkeit, Konsistenz und Auditfaehigkeit // ============================================================================= // ============================================================================= // TYPES // ============================================================================= export interface Obligation { id: string title: string description: string source: string source_article: string deadline: string | null status: 'pending' | 'in-progress' | 'completed' | 'overdue' priority: 'critical' | 'high' | 'medium' | 'low' responsible: string linked_systems: string[] linked_vendor_ids?: string[] assessment_id?: string rule_code?: string notes?: string created_at?: string updated_at?: string evidence?: string[] review_date?: string category?: string } export type ObligationComplianceIssueType = | 'MISSING_RESPONSIBLE' | 'OVERDUE_DEADLINE' | 'MISSING_EVIDENCE' | 'MISSING_DESCRIPTION' | 'NO_LEGAL_REFERENCE' | 'INCOMPLETE_REGULATION' | 'HIGH_PRIORITY_NOT_STARTED' | 'STALE_PENDING' | 'MISSING_LINKED_SYSTEMS' | 'NO_REVIEW_PROCESS' | 'CRITICAL_WITHOUT_EVIDENCE' | 'MISSING_VENDOR_LINK' export type ObligationComplianceIssueSeverity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' export interface ObligationComplianceIssue { type: ObligationComplianceIssueType severity: ObligationComplianceIssueSeverity message: string affectedObligations: string[] recommendation: string } export interface ObligationComplianceCheckResult { score: number issues: ObligationComplianceIssue[] summary: { total: number; critical: number; high: number; medium: number; low: number } checkedAt: string } // ============================================================================= // CONSTANTS // ============================================================================= export const OBLIGATION_SEVERITY_LABELS_DE: Record = { CRITICAL: 'Kritisch', HIGH: 'Hoch', MEDIUM: 'Mittel', LOW: 'Niedrig', } export const OBLIGATION_SEVERITY_COLORS: Record = { CRITICAL: '#dc2626', HIGH: '#ea580c', MEDIUM: '#d97706', LOW: '#6b7280', } // ============================================================================= // HELPERS // ============================================================================= function daysBetween(date: Date, now: Date): number { const diffMs = now.getTime() - date.getTime() return Math.floor(diffMs / (1000 * 60 * 60 * 24)) } // ============================================================================= // PER-OBLIGATION CHECKS (1-5, 9, 11) // ============================================================================= /** * Check 1: MISSING_RESPONSIBLE (MEDIUM) * Pflicht ohne verantwortliche Person/Abteilung. */ function checkMissingResponsible(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => !o.responsible || o.responsible.trim() === '') if (affected.length === 0) return null return { type: 'MISSING_RESPONSIBLE', severity: 'MEDIUM', message: `${affected.length} Pflicht(en) ohne verantwortliche Person oder Abteilung. Ohne klare Zustaendigkeit koennen Pflichten nicht zuverlaessig umgesetzt werden.`, affectedObligations: affected.map(o => o.id), recommendation: 'Weisen Sie jeder Pflicht eine verantwortliche Person oder Abteilung zu.', } } /** * Check 2: OVERDUE_DEADLINE (HIGH) * Pflicht mit Deadline in der Vergangenheit + Status != completed. */ function checkOverdueDeadline(obligations: Obligation[]): ObligationComplianceIssue | null { const now = new Date() const affected = obligations.filter(o => { if (!o.deadline || o.status === 'completed') return false return new Date(o.deadline) < now }) if (affected.length === 0) return null return { type: 'OVERDUE_DEADLINE', severity: 'HIGH', message: `${affected.length} Pflicht(en) mit ueberschrittener Frist. Ueberfaellige Pflichten stellen ein Compliance-Risiko dar und koennen zu Bussgeldern fuehren.`, affectedObligations: affected.map(o => o.id), recommendation: 'Bearbeiten Sie ueberfaellige Pflichten umgehend oder passen Sie die Fristen an.', } } /** * Check 3: MISSING_EVIDENCE (HIGH) * Completed-Pflicht ohne Evidence. */ function checkMissingEvidence(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => o.status === 'completed' && (!o.evidence || o.evidence.length === 0) ) if (affected.length === 0) return null return { type: 'MISSING_EVIDENCE', severity: 'HIGH', message: `${affected.length} abgeschlossene Pflicht(en) ohne Nachweis. Ohne Nachweise ist die Erfuellung im Audit nicht belegbar.`, affectedObligations: affected.map(o => o.id), recommendation: 'Hinterlegen Sie Nachweisdokumente fuer alle abgeschlossenen Pflichten.', } } /** * Check 4: MISSING_DESCRIPTION (MEDIUM) * Pflicht ohne Beschreibung. */ function checkMissingDescription(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => !o.description || o.description.trim() === '') if (affected.length === 0) return null return { type: 'MISSING_DESCRIPTION', severity: 'MEDIUM', message: `${affected.length} Pflicht(en) ohne Beschreibung. Eine fehlende Beschreibung erschwert die Nachvollziehbarkeit und Umsetzung.`, affectedObligations: affected.map(o => o.id), recommendation: 'Ergaenzen Sie eine Beschreibung fuer jede Pflicht, die den Inhalt und die Anforderungen erlaeutert.', } } /** * Check 5: NO_LEGAL_REFERENCE (HIGH) * Pflicht ohne source_article (kein Artikel-Bezug). */ function checkNoLegalReference(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => !o.source_article || o.source_article.trim() === '') if (affected.length === 0) return null return { type: 'NO_LEGAL_REFERENCE', severity: 'HIGH', message: `${affected.length} Pflicht(en) ohne Artikel-/Paragraphen-Referenz. Ohne Rechtsbezug ist die Pflicht im Audit nicht nachvollziehbar.`, affectedObligations: affected.map(o => o.id), recommendation: 'Ergaenzen Sie die Rechtsgrundlage (z.B. Art. 32 DSGVO) fuer jede Pflicht.', } } /** * Check 9: MISSING_LINKED_SYSTEMS (MEDIUM) * Pflicht ohne verknuepfte Systeme/Verarbeitungen. */ function checkMissingLinkedSystems(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => !o.linked_systems || o.linked_systems.length === 0) if (affected.length === 0) return null return { type: 'MISSING_LINKED_SYSTEMS', severity: 'MEDIUM', message: `${affected.length} Pflicht(en) ohne verknuepfte Systeme oder Verarbeitungstaetigkeiten. Ohne Systemzuordnung fehlt der operative Bezug.`, affectedObligations: affected.map(o => o.id), recommendation: 'Ordnen Sie jeder Pflicht die betroffenen IT-Systeme oder Verarbeitungstaetigkeiten zu.', } } /** * Check 11: CRITICAL_WITHOUT_EVIDENCE (CRITICAL) * Critical-Pflicht ohne Evidence. */ function checkCriticalWithoutEvidence(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => o.priority === 'critical' && (!o.evidence || o.evidence.length === 0) ) if (affected.length === 0) return null return { type: 'CRITICAL_WITHOUT_EVIDENCE', severity: 'CRITICAL', message: `${affected.length} kritische Pflicht(en) ohne Nachweis. Kritische Pflichten erfordern zwingend eine Dokumentation der Erfuellung.`, affectedObligations: affected.map(o => o.id), recommendation: 'Hinterlegen Sie umgehend Nachweise fuer alle kritischen Pflichten.', } } /** * Check 12: MISSING_VENDOR_LINK (MEDIUM) * Art.-28-Pflicht ohne verknuepften Auftragsverarbeiter. */ function checkMissingVendorLink(obligations: Obligation[]): ObligationComplianceIssue | null { const affected = obligations.filter(o => o.source_article?.includes('Art. 28') && (!o.linked_vendor_ids || o.linked_vendor_ids.length === 0) ) if (affected.length === 0) return null return { type: 'MISSING_VENDOR_LINK', severity: 'MEDIUM', message: `${affected.length} Art.-28-Pflicht(en) ohne verknuepften Auftragsverarbeiter.`, affectedObligations: affected.map(o => o.id), recommendation: 'Verknuepfen Sie Art.-28-Pflichten mit den betroffenen Auftragsverarbeitern im Vendor Register.', } } // ============================================================================= // AGGREGATE CHECKS (6-8, 10) // ============================================================================= /** * Check 6: INCOMPLETE_REGULATION (HIGH) * Regulierung, bei der alle Pflichten pending/overdue sind. */ function checkIncompleteRegulation(obligations: Obligation[]): ObligationComplianceIssue | null { const bySource = new Map() for (const o of obligations) { const src = o.source || 'Unbekannt' if (!bySource.has(src)) bySource.set(src, []) bySource.get(src)!.push(o) } const incompleteRegs: string[] = [] const affectedIds: string[] = [] for (const [source, obls] of bySource.entries()) { if (obls.length < 2) continue // Skip single-obligation regulations const allStalled = obls.every(o => o.status === 'pending' || o.status === 'overdue') if (allStalled) { incompleteRegs.push(source) affectedIds.push(...obls.map(o => o.id)) } } if (incompleteRegs.length === 0) return null return { type: 'INCOMPLETE_REGULATION', severity: 'HIGH', message: `${incompleteRegs.length} Regulierung(en) vollstaendig ohne Umsetzung: ${incompleteRegs.join(', ')}. Alle Pflichten sind ausstehend oder ueberfaellig.`, affectedObligations: affectedIds, recommendation: 'Beginnen Sie mit der Umsetzung der wichtigsten Pflichten in den betroffenen Regulierungen.', } } /** * Check 7: HIGH_PRIORITY_NOT_STARTED (CRITICAL) * Critical/High-Pflicht seit > 30 Tagen pending. */ function checkHighPriorityNotStarted(obligations: Obligation[]): ObligationComplianceIssue | null { const now = new Date() const affected = obligations.filter(o => { if (o.status !== 'pending') return false if (o.priority !== 'critical' && o.priority !== 'high') return false if (!o.created_at) return false return daysBetween(new Date(o.created_at), now) > 30 }) if (affected.length === 0) return null return { type: 'HIGH_PRIORITY_NOT_STARTED', severity: 'CRITICAL', message: `${affected.length} hochprioritaere Pflicht(en) seit ueber 30 Tagen nicht begonnen. Dies deutet auf organisatorische Blockaden oder fehlende Priorisierung hin.`, affectedObligations: affected.map(o => o.id), recommendation: 'Starten Sie umgehend mit der Bearbeitung dieser kritischen/hohen Pflichten und erstellen Sie einen Umsetzungsplan.', } } /** * Check 8: STALE_PENDING (LOW) * Pflicht seit > 90 Tagen pending. */ function checkStalePending(obligations: Obligation[]): ObligationComplianceIssue | null { const now = new Date() const affected = obligations.filter(o => { if (o.status !== 'pending') return false if (!o.created_at) return false return daysBetween(new Date(o.created_at), now) > 90 }) if (affected.length === 0) return null return { type: 'STALE_PENDING', severity: 'LOW', message: `${affected.length} Pflicht(en) seit ueber 90 Tagen ausstehend. Langfristig unbearbeitete Pflichten sollten priorisiert oder als nicht relevant markiert werden.`, affectedObligations: affected.map(o => o.id), recommendation: 'Pruefen Sie, ob die Pflichten weiterhin relevant sind, und setzen Sie Prioritaeten fuer die Umsetzung.', } } /** * Check 10: NO_REVIEW_PROCESS (MEDIUM) * Keine einzige Pflicht hat review_date. */ function checkNoReviewProcess(obligations: Obligation[]): ObligationComplianceIssue | null { if (obligations.length === 0) return null const hasAnyReview = obligations.some(o => o.review_date) if (hasAnyReview) return null return { type: 'NO_REVIEW_PROCESS', severity: 'MEDIUM', message: 'Keine Pflicht hat ein Pruefungsdatum (review_date). Ohne regelmaessige Ueberpruefung ist die Aktualitaet des Pflichtenregisters nicht gewaehrleistet.', affectedObligations: [], recommendation: 'Fuehren Sie ein Pruefintervall ein und setzen Sie review_date fuer alle Pflichten.', } } // ============================================================================= // MAIN COMPLIANCE CHECK // ============================================================================= /** * Fuehrt einen vollstaendigen Compliance-Check ueber alle Pflichten durch. */ export function runObligationComplianceCheck(obligations: Obligation[]): ObligationComplianceCheckResult { const issues: ObligationComplianceIssue[] = [] const checks = [ checkMissingResponsible(obligations), checkOverdueDeadline(obligations), checkMissingEvidence(obligations), checkMissingDescription(obligations), checkNoLegalReference(obligations), checkIncompleteRegulation(obligations), checkHighPriorityNotStarted(obligations), checkStalePending(obligations), checkMissingLinkedSystems(obligations), checkNoReviewProcess(obligations), checkCriticalWithoutEvidence(obligations), checkMissingVendorLink(obligations), ] for (const issue of checks) { if (issue !== null) { issues.push(issue) } } // Calculate score const summary = { total: issues.length, critical: 0, high: 0, medium: 0, low: 0 } for (const issue of issues) { switch (issue.severity) { case 'CRITICAL': summary.critical++; break case 'HIGH': summary.high++; break case 'MEDIUM': summary.medium++; break case 'LOW': summary.low++; break } } const rawScore = 100 - (summary.critical * 15 + summary.high * 10 + summary.medium * 5 + summary.low * 2) const score = Math.max(0, rawScore) return { score, issues, summary, checkedAt: new Date().toISOString(), } }