/** * Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten * in den Dokumentengenerator. * * Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf * den vom Benutzer ausgewählten Datenpunkten. */ import { DataPoint, DataPointCategory, LegalBasis, RetentionPeriod, RiskLevel, CATEGORY_METADATA, LEGAL_BASIS_INFO, RETENTION_PERIOD_INFO, RISK_LEVEL_STYLING, LocalizedText, SupportedLanguage } from '@/lib/sdk/einwilligungen/types' // ============================================================================= // TYPES // ============================================================================= /** * Sprach-Option für alle Helper-Funktionen */ export type Language = SupportedLanguage /** * Generierte Platzhalter-Map für den Dokumentengenerator */ export interface DataPointPlaceholders { '[DATENPUNKTE_COUNT]': string '[DATENPUNKTE_LIST]': string '[DATENPUNKTE_TABLE]': string '[VERARBEITUNGSZWECKE]': string '[RECHTSGRUNDLAGEN]': string '[SPEICHERFRISTEN]': string '[EMPFAENGER]': string '[BESONDERE_KATEGORIEN]': string '[DRITTLAND_TRANSFERS]': string '[RISIKO_ZUSAMMENFASSUNG]': string } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= /** * Extrahiert Text aus LocalizedText basierend auf Sprache */ function getText(text: LocalizedText, lang: Language): string { return text[lang] || text.de } /** * Generiert eine Markdown-Tabelle der Datenpunkte * * @param dataPoints - Liste der ausgewählten Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Markdown-Tabelle als String */ export function generateDataPointsTable( dataPoints: DataPoint[], lang: Language = 'de' ): string { if (dataPoints.length === 0) { return lang === 'de' ? '*Keine Datenpunkte ausgewählt.*' : '*No data points selected.*' } const header = lang === 'de' ? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |' : '| Data Point | Category | Purpose | Legal Basis | Retention Period |' const separator = '|------------|-----------|-------|-----------------|---------------|' const rows = dataPoints.map(dp => { const category = CATEGORY_METADATA[dp.category] const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis] const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod] const name = getText(dp.name, lang) const categoryName = getText(category.name, lang) const purpose = getText(dp.purpose, lang) const legalBasisName = getText(legalBasis.name, lang) const retentionLabel = getText(retention.label, lang) // Truncate long texts for table readability const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |` }).join('\n') return `${header}\n${separator}\n${rows}` } /** * Gruppiert Datenpunkte nach Speicherfrist * * @param dataPoints - Liste der Datenpunkte * @returns Record mit Speicherfrist als Key und Datenpunkten als Value */ export function groupByRetention( dataPoints: DataPoint[] ): Record { return dataPoints.reduce((acc, dp) => { const key = dp.retentionPeriod if (!acc[key]) { acc[key] = [] } acc[key].push(dp) return acc }, {} as Record) } /** * Gruppiert Datenpunkte nach Kategorie * * @param dataPoints - Liste der Datenpunkte * @returns Record mit Kategorie als Key und Datenpunkten als Value */ export function groupByCategory( dataPoints: DataPoint[] ): Record { return dataPoints.reduce((acc, dp) => { const key = dp.category if (!acc[key]) { acc[key] = [] } acc[key].push(dp) return acc }, {} as Record) } /** * Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO) * * @param dataPoints - Liste der Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten) */ export function generateSpecialCategorySection( dataPoints: DataPoint[], lang: Language = 'de' ): string { const special = dataPoints.filter(dp => dp.isSpecialCategory) if (special.length === 0) { return '' } if (lang === 'de') { const items = special.map(dp => `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` ).join('\n') return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO) Wir verarbeiten folgende besondere Kategorien personenbezogener Daten: ${items} Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.` } else { const items = special.map(dp => `- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}` ).join('\n') return `## Processing of Special Categories of Personal Data (Art. 9 GDPR) We process the following special categories of personal data: ${items} Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.` } } /** * Generiert Liste aller eindeutigen Verarbeitungszwecke * * @param dataPoints - Liste der Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Kommaseparierte Liste der Zwecke */ export function generatePurposesList( dataPoints: DataPoint[], lang: Language = 'de' ): string { const purposes = new Set() dataPoints.forEach(dp => { purposes.add(getText(dp.purpose, lang)) }) return [...purposes].join(', ') } /** * Generiert Liste aller verwendeten Rechtsgrundlagen * * @param dataPoints - Liste der Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Formatierte Liste der Rechtsgrundlagen */ export function generateLegalBasisList( dataPoints: DataPoint[], lang: Language = 'de' ): string { const bases = new Set() dataPoints.forEach(dp => { bases.add(dp.legalBasis) }) return [...bases].map(basis => { const info = LEGAL_BASIS_INFO[basis] return `${info.article} (${getText(info.name, lang)})` }).join(', ') } /** * Generiert Liste aller Speicherfristen gruppiert * * @param dataPoints - Liste der Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien */ export function generateRetentionList( dataPoints: DataPoint[], lang: Language = 'de' ): string { const grouped = groupByRetention(dataPoints) const entries: string[] = [] for (const [period, points] of Object.entries(grouped)) { const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod] const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))] entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`) } return entries.join('; ') } /** * Generiert Liste aller Empfänger/Drittparteien * * @param dataPoints - Liste der Datenpunkte * @returns Kommaseparierte Liste der Empfänger */ export function generateRecipientsList(dataPoints: DataPoint[]): string { const recipients = new Set() dataPoints.forEach(dp => { dp.thirdPartyRecipients?.forEach(r => recipients.add(r)) }) if (recipients.size === 0) { return '' } return [...recipients].join(', ') } /** * Generiert Abschnitt für Drittland-Übermittlungen * * @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true * @param lang - Sprache für die Ausgabe * @returns Markdown-Abschnitt als String */ export function generateThirdCountrySection( dataPoints: DataPoint[], lang: Language = 'de' ): string { // Note: We assume dataPoints have been filtered for thirdCountryTransfer // The actual flag would need to be added to the DataPoint interface // For now, we check if any thirdPartyRecipients suggest third country const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare'] const thirdCountryPoints = dataPoints.filter(dp => dp.thirdPartyRecipients?.some(r => thirdCountryIndicators.some(indicator => r.toLowerCase().includes(indicator.toLowerCase()) ) ) ) if (thirdCountryPoints.length === 0) { return '' } const recipients = new Set() thirdCountryPoints.forEach(dp => { dp.thirdPartyRecipients?.forEach(r => { if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) { recipients.add(r) } }) }) if (lang === 'de') { return `## Übermittlung in Drittländer Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR): ${[...recipients].map(r => `- ${r}`).join('\n')} Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).` } else { return `## Transfers to Third Countries We transfer personal data to the following recipients in third countries (outside the EU/EEA): ${[...recipients].map(r => `- ${r}`).join('\n')} The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).` } } /** * Generiert Risiko-Zusammenfassung * * @param dataPoints - Liste der Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Formatierte Risiko-Zusammenfassung */ export function generateRiskSummary( dataPoints: DataPoint[], lang: Language = 'de' ): string { const riskCounts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 } dataPoints.forEach(dp => { riskCounts[dp.riskLevel]++ }) const parts = Object.entries(riskCounts) .filter(([, count]) => count > 0) .map(([level, count]) => { const styling = RISK_LEVEL_STYLING[level as RiskLevel] return `${count} ${getText(styling.label, lang).toLowerCase()}` }) return parts.join(', ') } /** * Generiert alle Platzhalter für den Dokumentengenerator * * @param dataPoints - Liste der ausgewählten Datenpunkte * @param lang - Sprache für die Ausgabe * @returns Objekt mit allen Platzhaltern */ export function generateAllPlaceholders( dataPoints: DataPoint[], lang: Language = 'de' ): DataPointPlaceholders { return { '[DATENPUNKTE_COUNT]': String(dataPoints.length), '[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '), '[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang), '[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang), '[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang), '[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang), '[EMPFAENGER]': generateRecipientsList(dataPoints), '[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang), '[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang), '[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang) } } // ============================================================================= // VALIDATION HELPERS // ============================================================================= /** * Validierungswarnung für den Dokumentengenerator */ export interface ValidationWarning { type: 'error' | 'warning' | 'info' code: string message: string suggestion: string affectedDataPoints?: DataPoint[] } /** * Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt * * @param dataPoints - Liste der Datenpunkte * @param documentContent - Der generierte Dokumentinhalt * @param lang - Sprache * @returns ValidationWarning oder null */ export function checkSpecialCategoriesWarning( dataPoints: DataPoint[], documentContent: string, lang: Language = 'de' ): ValidationWarning | null { const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory) if (specialCategories.length === 0) { return null } const hasSection = lang === 'de' ? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie') : documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor') if (!hasSection) { return { type: 'error', code: 'MISSING_ART9_SECTION', message: lang === 'de' ? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.` : `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`, suggestion: lang === 'de' ? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.' : 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.', affectedDataPoints: specialCategories } } return null } /** * Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden * * @param dataPoints - Liste der Datenpunkte * @param documentContent - Der generierte Dokumentinhalt * @param lang - Sprache * @returns ValidationWarning oder null */ export function checkThirdCountryWarning( dataPoints: DataPoint[], documentContent: string, lang: Language = 'de' ): ValidationWarning | null { const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US'] const thirdCountryPoints = dataPoints.filter(dp => dp.thirdPartyRecipients?.some(r => thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase())) ) ) if (thirdCountryPoints.length === 0) { return null } const hasSCCMention = lang === 'de' ? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46') : documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46') if (!hasSCCMention) { return { type: 'warning', code: 'MISSING_SCC_SECTION', message: lang === 'de' ? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.` : `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`, suggestion: lang === 'de' ? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.' : 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.', affectedDataPoints: thirdCountryPoints } } return null } /** * Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden * * @param dataPoints - Liste der Datenpunkte * @param documentContent - Der generierte Dokumentinhalt * @param lang - Sprache * @returns ValidationWarning oder null */ export function checkExplicitConsentWarning( dataPoints: DataPoint[], documentContent: string, lang: Language = 'de' ): ValidationWarning | null { const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent) if (explicitConsentPoints.length === 0) { return null } const hasConsentSection = lang === 'de' ? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7') : documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7') if (!hasConsentSection) { return { type: 'warning', code: 'MISSING_CONSENT_SECTION', message: lang === 'de' ? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.` : `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`, suggestion: lang === 'de' ? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.' : 'Add a section about the right to withdraw consent.', affectedDataPoints: explicitConsentPoints } } return null } /** * Führt alle Validierungsprüfungen durch * * @param dataPoints - Liste der Datenpunkte * @param documentContent - Der generierte Dokumentinhalt * @param lang - Sprache * @returns Array aller Warnungen */ export function validateDocument( dataPoints: DataPoint[], documentContent: string, lang: Language = 'de' ): ValidationWarning[] { const warnings: ValidationWarning[] = [] const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang) if (specialCatWarning) warnings.push(specialCatWarning) const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang) if (thirdCountryWarning) warnings.push(thirdCountryWarning) const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang) if (consentWarning) warnings.push(consentWarning) return warnings }