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>
323 lines
10 KiB
TypeScript
323 lines
10 KiB
TypeScript
/**
|
|
* Privacy Policy Renderers & Main Generator
|
|
*
|
|
* Cookies section, changes section, rendering (HTML/Markdown),
|
|
* and the main generatePrivacyPolicy entry point.
|
|
*/
|
|
|
|
import {
|
|
DataPoint,
|
|
CompanyInfo,
|
|
PrivacyPolicySection,
|
|
GeneratedPrivacyPolicy,
|
|
SupportedLanguage,
|
|
ExportFormat,
|
|
LocalizedText,
|
|
} from '../types'
|
|
import { RETENTION_MATRIX } from '../catalog/loader'
|
|
|
|
import {
|
|
formatDate,
|
|
generateControllerSection,
|
|
generateDataCollectionSection,
|
|
generatePurposesSection,
|
|
generateLegalBasisSection,
|
|
generateRecipientsSection,
|
|
generateRetentionSection,
|
|
generateSpecialCategoriesSection,
|
|
generateRightsSection,
|
|
} from './privacy-policy-sections'
|
|
|
|
// =============================================================================
|
|
// HELPER
|
|
// =============================================================================
|
|
|
|
function t(text: LocalizedText, language: SupportedLanguage): string {
|
|
return text[language]
|
|
}
|
|
|
|
// =============================================================================
|
|
// SECTION GENERATORS (cookies + changes)
|
|
// =============================================================================
|
|
|
|
export function generateCookiesSection(
|
|
dataPoints: DataPoint[],
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '8. Cookies und aehnliche Technologien',
|
|
en: '8. Cookies and Similar Technologies',
|
|
}
|
|
|
|
const cookieDataPoints = dataPoints.filter((dp) => dp.cookieCategory !== null)
|
|
|
|
if (cookieDataPoints.length === 0) {
|
|
const content: LocalizedText = {
|
|
de: 'Wir verwenden auf dieser Website keine Cookies.',
|
|
en: 'We do not use cookies on this website.',
|
|
}
|
|
return {
|
|
id: 'cookies',
|
|
order: 8,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: false,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
const essential = cookieDataPoints.filter((dp) => dp.cookieCategory === 'ESSENTIAL')
|
|
const performance = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERFORMANCE')
|
|
const personalization = cookieDataPoints.filter((dp) => dp.cookieCategory === 'PERSONALIZATION')
|
|
const externalMedia = cookieDataPoints.filter((dp) => dp.cookieCategory === 'EXTERNAL_MEDIA')
|
|
|
|
const sections: string[] = []
|
|
|
|
if (essential.length > 0) {
|
|
const list = essential.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Technisch notwendige Cookies\n\nDiese Cookies sind fuer den Betrieb der Website erforderlich und koennen nicht deaktiviert werden.\n\n${list}`
|
|
: `### Essential Cookies\n\nThese cookies are required for the website to function and cannot be disabled.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (performance.length > 0) {
|
|
const list = performance.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Analyse- und Performance-Cookies\n\nDiese Cookies helfen uns, die Nutzung der Website zu verstehen und zu verbessern.\n\n${list}`
|
|
: `### Analytics and Performance Cookies\n\nThese cookies help us understand and improve website usage.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (personalization.length > 0) {
|
|
const list = personalization.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Personalisierungs-Cookies\n\nDiese Cookies ermoeglichen personalisierte Werbung und Inhalte.\n\n${list}`
|
|
: `### Personalization Cookies\n\nThese cookies enable personalized advertising and content.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
if (externalMedia.length > 0) {
|
|
const list = externalMedia.map((dp) => `- **${t(dp.name, language)}**: ${t(dp.purpose, language)}`).join('\n')
|
|
sections.push(
|
|
language === 'de'
|
|
? `### Cookies fuer externe Medien\n\nDiese Cookies erlauben die Einbindung externer Medien wie Videos und Karten.\n\n${list}`
|
|
: `### External Media Cookies\n\nThese cookies allow embedding external media like videos and maps.\n\n${list}`
|
|
)
|
|
}
|
|
|
|
const intro: LocalizedText = {
|
|
de: `Wir verwenden Cookies und aehnliche Technologien, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen. Sie koennen Ihre Cookie-Einstellungen jederzeit ueber unseren Cookie-Banner anpassen.`,
|
|
en: `We use cookies and similar technologies to provide you with the best possible experience on our website. You can adjust your cookie settings at any time through our cookie banner.`,
|
|
}
|
|
|
|
const content: LocalizedText = {
|
|
de: `${intro.de}\n\n${sections.join('\n\n')}`,
|
|
en: `${intro.en}\n\n${sections.join('\n\n')}`,
|
|
}
|
|
|
|
return {
|
|
id: 'cookies',
|
|
order: 8,
|
|
title,
|
|
content,
|
|
dataPointIds: cookieDataPoints.map((dp) => dp.id),
|
|
isRequired: true,
|
|
isGenerated: true,
|
|
}
|
|
}
|
|
|
|
export function generateChangesSection(
|
|
version: string,
|
|
date: Date,
|
|
language: SupportedLanguage
|
|
): PrivacyPolicySection {
|
|
const title: LocalizedText = {
|
|
de: '9. Aenderungen dieser Datenschutzerklaerung',
|
|
en: '9. Changes to this Privacy Policy',
|
|
}
|
|
|
|
const formattedDate = formatDate(date, language)
|
|
|
|
const content: LocalizedText = {
|
|
de: `Diese Datenschutzerklaerung ist aktuell gueltig und hat den Stand: **${formattedDate}** (Version ${version}).
|
|
|
|
Wir behalten uns vor, diese Datenschutzerklaerung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um Aenderungen unserer Leistungen umzusetzen.
|
|
|
|
Fuer Ihren erneuten Besuch gilt dann die neue Datenschutzerklaerung.`,
|
|
en: `This privacy policy is currently valid and was last updated: **${formattedDate}** (Version ${version}).
|
|
|
|
We reserve the right to amend this privacy policy to ensure it always complies with current legal requirements or to implement changes to our services.
|
|
|
|
The new privacy policy will then apply for your next visit.`,
|
|
}
|
|
|
|
return {
|
|
id: 'changes',
|
|
order: 9,
|
|
title,
|
|
content,
|
|
dataPointIds: [],
|
|
isRequired: true,
|
|
isGenerated: false,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN GENERATOR FUNCTIONS
|
|
// =============================================================================
|
|
|
|
export function generatePrivacyPolicySections(
|
|
dataPoints: DataPoint[],
|
|
companyInfo: CompanyInfo,
|
|
language: SupportedLanguage,
|
|
version: string = '1.0.0'
|
|
): PrivacyPolicySection[] {
|
|
const now = new Date()
|
|
|
|
const sections: PrivacyPolicySection[] = [
|
|
generateControllerSection(companyInfo, language),
|
|
generateDataCollectionSection(dataPoints, language),
|
|
generatePurposesSection(dataPoints, language),
|
|
generateLegalBasisSection(dataPoints, language),
|
|
generateRecipientsSection(dataPoints, language),
|
|
generateRetentionSection(dataPoints, RETENTION_MATRIX, language),
|
|
]
|
|
|
|
const specialCategoriesSection = generateSpecialCategoriesSection(dataPoints, language)
|
|
if (specialCategoriesSection) {
|
|
sections.push(specialCategoriesSection)
|
|
}
|
|
|
|
sections.push(
|
|
generateRightsSection(language),
|
|
generateCookiesSection(dataPoints, language),
|
|
generateChangesSection(version, now, language)
|
|
)
|
|
|
|
sections.forEach((section, index) => {
|
|
section.order = index + 1
|
|
const titleDe = section.title.de
|
|
const titleEn = section.title.en
|
|
if (titleDe.match(/^\d+[a-z]?\./)) {
|
|
section.title.de = titleDe.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
|
}
|
|
if (titleEn.match(/^\d+[a-z]?\./)) {
|
|
section.title.en = titleEn.replace(/^\d+[a-z]?\./, `${index + 1}.`)
|
|
}
|
|
})
|
|
|
|
return sections
|
|
}
|
|
|
|
export function generatePrivacyPolicy(
|
|
tenantId: string,
|
|
dataPoints: DataPoint[],
|
|
companyInfo: CompanyInfo,
|
|
language: SupportedLanguage,
|
|
format: ExportFormat = 'HTML'
|
|
): GeneratedPrivacyPolicy {
|
|
const version = '1.0.0'
|
|
const sections = generatePrivacyPolicySections(dataPoints, companyInfo, language, version)
|
|
const content = renderPrivacyPolicy(sections, language, format)
|
|
|
|
return {
|
|
id: `privacy-policy-${tenantId}-${Date.now()}`,
|
|
tenantId,
|
|
language,
|
|
sections,
|
|
companyInfo,
|
|
generatedAt: new Date(),
|
|
version,
|
|
format,
|
|
content,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// RENDERERS
|
|
// =============================================================================
|
|
|
|
function renderPrivacyPolicy(
|
|
sections: PrivacyPolicySection[],
|
|
language: SupportedLanguage,
|
|
format: ExportFormat
|
|
): string {
|
|
switch (format) {
|
|
case 'HTML':
|
|
return renderAsHTML(sections, language)
|
|
case 'MARKDOWN':
|
|
return renderAsMarkdown(sections, language)
|
|
default:
|
|
return renderAsMarkdown(sections, language)
|
|
}
|
|
}
|
|
|
|
export function renderAsHTML(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
|
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
|
|
|
const sectionsHTML = sections
|
|
.map((section) => {
|
|
const content = t(section.content, language)
|
|
.replace(/\n\n/g, '</p><p>')
|
|
.replace(/\n/g, '<br>')
|
|
.replace(/### (.+)/g, '<h3>$1</h3>')
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/- (.+)(?:<br>|$)/g, '<li>$1</li>')
|
|
|
|
return `
|
|
<section id="${section.id}">
|
|
<h2>${t(section.title, language)}</h2>
|
|
<p>${content}</p>
|
|
</section>
|
|
`
|
|
})
|
|
.join('\n')
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="${language}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
line-height: 1.6;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
color: #333;
|
|
}
|
|
h1 { font-size: 2rem; margin-bottom: 2rem; }
|
|
h2 { font-size: 1.5rem; margin-top: 2rem; color: #1a1a1a; }
|
|
h3 { font-size: 1.25rem; margin-top: 1.5rem; color: #333; }
|
|
p { margin: 1rem 0; }
|
|
ul, ol { margin: 1rem 0; padding-left: 2rem; }
|
|
li { margin: 0.5rem 0; }
|
|
strong { font-weight: 600; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>${title}</h1>
|
|
${sectionsHTML}
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
export function renderAsMarkdown(sections: PrivacyPolicySection[], language: SupportedLanguage): string {
|
|
const title = language === 'de' ? 'Datenschutzerklaerung' : 'Privacy Policy'
|
|
|
|
const sectionsMarkdown = sections
|
|
.map((section) => {
|
|
return `## ${t(section.title, language)}\n\n${t(section.content, language)}`
|
|
})
|
|
.join('\n\n---\n\n')
|
|
|
|
return `# ${title}\n\n${sectionsMarkdown}`
|
|
}
|