Split these files that exceeded the 500-line hard cap: - privacy-policy.ts (965 LOC) -> sections + renderers - academy/api.ts (787 LOC) -> courses + mock-data - whistleblower/api.ts (755 LOC) -> operations + mock-data - vvt-profiling.ts (659 LOC) -> data + logic - cookie-banner.ts (595 LOC) -> config + embed - dsr/types.ts (581 LOC) -> core + api types - tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis - datapoint-helpers.ts (548 LOC) -> generators + validators Each original file becomes a barrel re-export for backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
266 lines
8.5 KiB
TypeScript
266 lines
8.5 KiB
TypeScript
/**
|
|
* Datapoint Helpers — Generation Functions
|
|
*
|
|
* Functions that generate DSGVO-compliant text blocks from data points
|
|
* for the document generator.
|
|
*/
|
|
|
|
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
|
|
// =============================================================================
|
|
|
|
export type Language = SupportedLanguage
|
|
|
|
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
|
|
// =============================================================================
|
|
|
|
function getText(text: LocalizedText, lang: Language): string {
|
|
return text[lang] || text.de
|
|
}
|
|
|
|
export function groupByRetention(
|
|
dataPoints: DataPoint[]
|
|
): Record<RetentionPeriod, DataPoint[]> {
|
|
return dataPoints.reduce((acc, dp) => {
|
|
const key = dp.retentionPeriod
|
|
if (!acc[key]) acc[key] = []
|
|
acc[key].push(dp)
|
|
return acc
|
|
}, {} as Record<RetentionPeriod, DataPoint[]>)
|
|
}
|
|
|
|
export function groupByCategory(
|
|
dataPoints: DataPoint[]
|
|
): Record<DataPointCategory, DataPoint[]> {
|
|
return dataPoints.reduce((acc, dp) => {
|
|
const key = dp.category
|
|
if (!acc[key]) acc[key] = []
|
|
acc[key].push(dp)
|
|
return acc
|
|
}, {} as Record<DataPointCategory, DataPoint[]>)
|
|
}
|
|
|
|
// =============================================================================
|
|
// GENERATOR FUNCTIONS
|
|
// =============================================================================
|
|
|
|
export function generateDataPointsTable(
|
|
dataPoints: DataPoint[],
|
|
lang: Language = 'de'
|
|
): string {
|
|
if (dataPoints.length === 0) {
|
|
return lang === 'de' ? '*Keine Datenpunkte ausgewaehlt.*' : '*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)
|
|
|
|
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
|
|
|
|
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
|
|
}).join('\n')
|
|
|
|
return `${header}\n${separator}\n${rows}`
|
|
}
|
|
|
|
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 ausdruecklichen Einwilligung gemaess Art. 9 Abs. 2 lit. a DSGVO. Sie koennen Ihre Einwilligung jederzeit mit Wirkung fuer 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.`
|
|
}
|
|
}
|
|
|
|
export function generatePurposesList(
|
|
dataPoints: DataPoint[],
|
|
lang: Language = 'de'
|
|
): string {
|
|
const purposes = new Set<string>()
|
|
dataPoints.forEach(dp => purposes.add(getText(dp.purpose, lang)))
|
|
return [...purposes].join(', ')
|
|
}
|
|
|
|
export function generateLegalBasisList(
|
|
dataPoints: DataPoint[],
|
|
lang: Language = 'de'
|
|
): string {
|
|
const bases = new Set<LegalBasis>()
|
|
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(', ')
|
|
}
|
|
|
|
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('; ')
|
|
}
|
|
|
|
export function generateRecipientsList(dataPoints: DataPoint[]): string {
|
|
const recipients = new Set<string>()
|
|
dataPoints.forEach(dp => {
|
|
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
|
})
|
|
if (recipients.size === 0) return ''
|
|
return [...recipients].join(', ')
|
|
}
|
|
|
|
export function generateThirdCountrySection(
|
|
dataPoints: DataPoint[],
|
|
lang: Language = 'de'
|
|
): string {
|
|
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<string>()
|
|
thirdCountryPoints.forEach(dp => {
|
|
dp.thirdPartyRecipients?.forEach(r => {
|
|
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
|
|
recipients.add(r)
|
|
}
|
|
})
|
|
})
|
|
|
|
if (lang === 'de') {
|
|
return `## Uebermittlung in Drittlaender
|
|
|
|
Wir uebermitteln personenbezogene Daten an folgende Empfaenger in Drittlaendern (ausserhalb der EU/des EWR):
|
|
|
|
${[...recipients].map(r => `- ${r}`).join('\n')}
|
|
|
|
Die Uebermittlung 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).`
|
|
}
|
|
}
|
|
|
|
export function generateRiskSummary(
|
|
dataPoints: DataPoint[],
|
|
lang: Language = 'de'
|
|
): string {
|
|
const riskCounts: Record<RiskLevel, number> = { 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(', ')
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|