bae59e2ce0
Complete overhaul of document generator templates based on paragraph-by-paragraph legal review of attorney-drafted templates (TOM, AVV, AGB, DSI, Community Guidelines, Nutzungsbedingungen, Widerrufsbelehrung, Cookie-Richtlinie). Templates (11 migrations 087-097): - 087: TOM-Dokumentation v2 (11 categories incl. Trennungskontrolle) - 088: AVV Art. 28 DSGVO (complete, §§ 1-11, 3 annexes) - 089: Cross-document updates (Löschkonzept DIN 66399, VVT recipients) - 090: AGB SaaS/Shop v2 (18 §§, B2B/B2C, IoT, physical goods, IP protection) - 091: Community Guidelines v2 (3 tones, 11 modular categories, DSA-compliant) - 092: Media & Content modules (MStV, AI Act Art. 50, UWG, Pressekodex) - 093: DSI/Privacy Policy v2 (Art. 13 complete, shop+corporate modules) - 094: Nutzungsbedingungen (Terms of Use, UGC, tipping, wallet, CC licenses) - 095: Widerrufsbelehrung (SaaS + physical + IoT bundle + combo) - 096: Social Media DSI (Facebook, YouTube, LinkedIn, TikTok, Meta Pixel) - 097: Cookie-Richtlinie v2 (TDDDG § 25, consent banner, browser links) Frontend (generator): - scopeDefaults.ts: L1-L4 scope-based defaults from Compliance Scope Engine - contextBridge.ts: TOMCtx + DPACtx interfaces (70+ new fields) - contextBridge-helpers.ts: 35+ placeholder mappings for TOM/DPA/AGB - _constants.ts: 120+ new generator fields (TOM, DPA, AGB, community, media, social, nutzungsbedingungen, widerruf, cookie, shop, IoT) - page.tsx: Auto-prefill TOM/DPA from scope engine decision Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
324 lines
18 KiB
TypeScript
324 lines
18 KiB
TypeScript
/**
|
|
* Template-Spec v1 — Context Bridge Helpers
|
|
*
|
|
* Runtime functions: contextToPlaceholders, computeFlags, section relevance,
|
|
* dot-path accessors. Split from contextBridge.ts for the 500 LOC hard cap.
|
|
*/
|
|
|
|
import type {
|
|
TemplateContext,
|
|
ProviderCtx,
|
|
ComputedFlags,
|
|
TOMCtx,
|
|
DPACtx,
|
|
} from './contextBridge'
|
|
|
|
// =============================================================================
|
|
// Bridge: context → flat placeholder map
|
|
// =============================================================================
|
|
|
|
function str(v: unknown): string {
|
|
if (v === null || v === undefined || v === '') return ''
|
|
if (Array.isArray(v)) return v.join(', ')
|
|
return String(v)
|
|
}
|
|
|
|
/** Compute compound address from PROVIDER fields */
|
|
function providerAddress(p: ProviderCtx): string {
|
|
const parts = [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' ')].filter(Boolean)
|
|
return parts.join(', ')
|
|
}
|
|
|
|
/**
|
|
* Maps a TemplateContext to a flat Record<string, string> of placeholder values.
|
|
* Keys are the {{PLACEHOLDER}} strings used in templates.
|
|
*/
|
|
export function contextToPlaceholders(ctx: TemplateContext): Record<string, string> {
|
|
const p = ctx.PROVIDER
|
|
const c = ctx.CUSTOMER
|
|
const s = ctx.SERVICE
|
|
const l = ctx.LEGAL
|
|
const prv = ctx.PRIVACY
|
|
const sla = ctx.SLA
|
|
const pay = ctx.PAYMENTS
|
|
const sec = ctx.SECURITY
|
|
const nda = ctx.NDA
|
|
const con = ctx.CONSENT
|
|
const h = ctx.HOSTING
|
|
const f = ctx.FEATURES
|
|
const tom = ctx.TOM
|
|
const dpa = ctx.DPA
|
|
|
|
const address = providerAddress(p)
|
|
|
|
return {
|
|
// --- PROVIDER ---
|
|
'{{COMPANY_NAME}}': str(p.LEGAL_NAME),
|
|
'{{PROVIDER_NAME}}': str(p.LEGAL_NAME),
|
|
'{{DISCLOSING_PARTY}}': str(p.LEGAL_NAME), // NDA: provider is disclosing party
|
|
'{{SERVICE_PROVIDER}}': str(p.LEGAL_NAME), // SLA
|
|
'{{COMPANY_ADDRESS}}': address,
|
|
'{{PROVIDER_ADDRESS}}': address,
|
|
'{{CONTACT_EMAIL}}': str(p.EMAIL),
|
|
'{{PROVIDER_EMAIL}}': str(p.EMAIL),
|
|
'{{ABUSE_EMAIL}}': str(p.EMAIL), // AUP abuse contact
|
|
'{{REPORT_EMAIL}}': prv.CONTACT_EMAIL ? str(prv.CONTACT_EMAIL) : str(p.EMAIL), // community moderation
|
|
|
|
// --- CUSTOMER ---
|
|
'{{CUSTOMER_NAME}}': str(c.LEGAL_NAME),
|
|
'{{RECEIVING_PARTY}}': str(c.LEGAL_NAME), // NDA
|
|
'{{CUSTOMER}}': str(c.LEGAL_NAME), // SLA
|
|
|
|
// --- SERVICE ---
|
|
'{{SERVICE_NAME}}': str(s.NAME),
|
|
'{{PLATFORM_NAME}}': str(s.NAME), // community / copyright
|
|
'{{SERVICE_DESCRIPTION}}': str(s.DESCRIPTION),
|
|
'{{SERVICE_MODEL}}': str(s.MODEL),
|
|
'{{SERVICE_TIER}}': str(s.TIER),
|
|
'{{DATA_LOCATION}}': str(s.DATA_LOCATION),
|
|
'{{EXPORT_FORMATS}}': Array.isArray(s.EXPORT_FORMATS) ? s.EXPORT_FORMATS.join(', ') : '',
|
|
'{{EXPORT_WINDOW_DAYS}}': str(s.EXPORT_WINDOW_DAYS),
|
|
'{{MIN_TERM_MONTHS}}': str(s.MIN_TERM_MONTHS),
|
|
'{{TERMINATION_NOTICE_DAYS}}': str(s.TERMINATION_NOTICE_DAYS),
|
|
|
|
// --- LEGAL ---
|
|
'{{GOVERNING_LAW}}': str(l.GOVERNING_LAW),
|
|
'{{JURISDICTION_CITY}}': str(l.JURISDICTION_CITY),
|
|
'{{VERSION_DATE}}': str(l.VERSION_DATE),
|
|
'{{EFFECTIVE_DATE}}': str(l.EFFECTIVE_DATE),
|
|
'{{START_DATE}}': str(l.EFFECTIVE_DATE), // cloud contract start date
|
|
|
|
// --- PRIVACY ---
|
|
'{{PRIVACY_CONTACT_EMAIL}}': str(prv.CONTACT_EMAIL),
|
|
'{{DPO_NAME}}': str(prv.DPO_NAME),
|
|
'{{DPO_EMAIL}}': str(prv.DPO_EMAIL),
|
|
'{{COPYRIGHT_CONTACT_NAME}}': str(prv.DPO_NAME), // copyright policy
|
|
'{{COPYRIGHT_EMAIL}}': str(prv.DPO_EMAIL),
|
|
'{{PRIVACY_POLICY_URL}}': str(prv.PRIVACY_POLICY_URL),
|
|
'{{COOKIE_POLICY_URL}}': str(prv.COOKIE_POLICY_URL),
|
|
'{{ANALYTICS_RETENTION_MONTHS}}': str(prv.ANALYTICS_RETENTION_MONTHS),
|
|
'{{DATA_TRANSFER_THIRD_COUNTRIES}}': str(prv.DATA_TRANSFER_THIRD_COUNTRIES),
|
|
|
|
// --- SLA ---
|
|
'{{AVAILABILITY_PERCENT}}': str(sla.AVAILABILITY_PERCENT),
|
|
'{{MAINTENANCE_NOTICE_HOURS}}': str(sla.MAINTENANCE_NOTICE_HOURS),
|
|
'{{SUPPORT_EMAIL}}': str(sla.SUPPORT_EMAIL),
|
|
'{{SUPPORT_PHONE}}': str(sla.SUPPORT_PHONE),
|
|
'{{SUPPORT_HOURS}}': str(sla.SUPPORT_HOURS),
|
|
'{{RESPONSE_CRITICAL_H}}': str(sla.RESPONSE_CRITICAL_H),
|
|
'{{RESOLUTION_CRITICAL_H}}': str(sla.RESOLUTION_CRITICAL_H),
|
|
'{{RESPONSE_HIGH_H}}': str(sla.RESPONSE_HIGH_H),
|
|
'{{RESOLUTION_HIGH_H}}': str(sla.RESOLUTION_HIGH_H),
|
|
'{{RESPONSE_MEDIUM_H}}': str(sla.RESPONSE_MEDIUM_H),
|
|
'{{RESOLUTION_MEDIUM_H}}': str(sla.RESOLUTION_MEDIUM_H),
|
|
'{{RESPONSE_LOW_H}}': str(sla.RESPONSE_LOW_H),
|
|
|
|
// --- PAYMENTS ---
|
|
'{{MONTHLY_FEE}}': str(pay.MONTHLY_FEE_EUR),
|
|
'{{PAYMENT_DUE_DAY}}': str(pay.PAYMENT_DUE_DAY),
|
|
'{{PAYMENT_METHOD}}': str(pay.PAYMENT_METHOD),
|
|
'{{PAYMENT_DAYS}}': str(pay.PAYMENT_DAYS),
|
|
|
|
// --- SECURITY ---
|
|
'{{INCIDENT_NOTICE_HOURS}}': str(sec.INCIDENT_NOTICE_HOURS),
|
|
'{{LOG_RETENTION_DAYS}}': str(sec.LOG_RETENTION_DAYS),
|
|
'{{SECURITY_LOG_RETENTION_DAYS}}': str(sec.SECURITY_LOG_RETENTION_DAYS),
|
|
|
|
// --- NDA ---
|
|
'{{PURPOSE}}': str(nda.PURPOSE),
|
|
'{{DURATION_YEARS}}': str(nda.DURATION_YEARS),
|
|
'{{PENALTY_AMOUNT}}': nda.PENALTY_AMOUNT_EUR !== null ? str(nda.PENALTY_AMOUNT_EUR) : '',
|
|
|
|
// --- CONSENT ---
|
|
'{{WEBSITE_NAME}}': con.WEBSITE_NAME || str(p.WEBSITE_URL),
|
|
'{{ANALYTICS_TOOLS}}': str(con.ANALYTICS_TOOLS),
|
|
'{{MARKETING_PARTNERS}}': str(con.MARKETING_PARTNERS),
|
|
|
|
// --- ALIASES (canonical names from new templates) ---
|
|
'{{COMPANY_LEGAL_NAME}}': str(p.LEGAL_NAME),
|
|
'{{COMPANY_LEGAL_FORM}}': str(p.LEGAL_FORM),
|
|
'{{COMPANY_ADDRESS_LINE}}': str(p.ADDRESS_LINE),
|
|
'{{COMPANY_POSTAL_CODE}}': str(p.POSTAL_CODE),
|
|
'{{COMPANY_CITY}}': str(p.CITY),
|
|
'{{COMPANY_COUNTRY}}': str(p.COUNTRY),
|
|
'{{COMPANY_ADDRESS_FULL}}': [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' '), p.COUNTRY].filter(Boolean).join(', '),
|
|
'{{WEBSITE_URL}}': str(p.WEBSITE_URL),
|
|
'{{CONTACT_PHONE}}': str(p.PHONE),
|
|
'{{REPRESENTED_BY_NAME}}': str(p.CEO_NAME),
|
|
'{{REGISTER_COURT}}': str(p.REGISTER_COURT),
|
|
'{{REGISTER_NUMBER}}': str(p.REGISTER_NUMBER),
|
|
'{{VAT_ID}}': str(p.VAT_ID),
|
|
'{{SERVICE_DESCRIPTION_SHORT}}': str(s.DESCRIPTION),
|
|
'{{NOTICE_PERIOD_DAYS}}': str(s.TERMINATION_NOTICE_DAYS),
|
|
'{{ANALYTICS_TOOLS_LIST}}': str(con.ANALYTICS_TOOLS),
|
|
'{{MARKETING_PARTNERS_LIST}}': str(con.MARKETING_PARTNERS),
|
|
'{{DATA_PROCESSING_LOCATIONS}}': str(s.DATA_LOCATION),
|
|
'{{SUPERVISORY_AUTHORITY_NAME}}': str(prv.SUPERVISORY_AUTHORITY_NAME),
|
|
'{{SUPERVISORY_AUTHORITY_ADDRESS}}': str(prv.SUPERVISORY_AUTHORITY_ADDRESS),
|
|
|
|
// --- HOSTING ---
|
|
'{{HOSTING_PROVIDER_NAME}}': str(h.PROVIDER_NAME),
|
|
'{{HOSTING_PROVIDER_COUNTRY}}': str(h.COUNTRY),
|
|
'{{HOSTING_PROVIDER_CONTRACT_TYPE}}': str(h.CONTRACT_TYPE),
|
|
|
|
// --- FEATURES (text fields) ---
|
|
'{{CONSENT_WITHDRAWAL_PATH}}': str(f.CONSENT_WITHDRAWAL_PATH),
|
|
'{{SECURITY_MEASURES_SUMMARY}}': str(f.SECURITY_MEASURES_SUMMARY),
|
|
'{{DATA_SUBJECT_REQUEST_CHANNEL}}': str(f.DATA_SUBJECT_REQUEST_CHANNEL),
|
|
'{{TRANSFER_GUARDS}}': str(f.TRANSFER_GUARDS),
|
|
'{{REGULATED_PROFESSION_TEXT}}': str(f.REGULATED_PROFESSION_TEXT),
|
|
'{{EDITORIAL_RESPONSIBLE_NAME}}': str(f.EDITORIAL_RESPONSIBLE_NAME),
|
|
'{{EDITORIAL_RESPONSIBLE_ADDRESS}}': str(f.EDITORIAL_RESPONSIBLE_ADDRESS),
|
|
'{{DISPUTE_RESOLUTION_TEXT}}': str(f.DISPUTE_RESOLUTION_TEXT),
|
|
'{{NEWSLETTER_PROVIDER_DETAIL}}': str(f.NEWSLETTER_PROVIDER_DETAIL),
|
|
'{{PAYMENT_PROVIDER_DETAIL}}': str(f.PAYMENT_PROVIDER_DETAIL),
|
|
'{{SOCIAL_MEDIA_DETAIL}}': str(f.SOCIAL_MEDIA_DETAIL),
|
|
'{{ANALYTICS_TOOLS_DETAIL}}': str(f.ANALYTICS_TOOLS_DETAIL),
|
|
'{{MARKETING_TOOLS_DETAIL}}': str(f.MARKETING_TOOLS_DETAIL),
|
|
'{{CMP_NAME}}': str(f.CMP_NAME),
|
|
'{{PRICES_TEXT}}': str(f.PRICES_TEXT),
|
|
'{{PAYMENT_TERMS_TEXT}}': str(f.PAYMENT_TERMS_TEXT),
|
|
'{{CONTRACT_TERM_TEXT}}': str(f.CONTRACT_TERM_TEXT),
|
|
'{{SLA_URL}}': str(f.SLA_URL),
|
|
'{{EXPORT_POLICY_TEXT}}': str(f.EXPORT_POLICY_TEXT),
|
|
'{{LIMITATION_CAP_TEXT}}': str(f.LIMITATION_CAP_TEXT),
|
|
'{{CONSUMER_WITHDRAWAL_TEXT}}': str(f.CONSUMER_WITHDRAWAL_TEXT),
|
|
'{{SUPPORT_CHANNELS_TEXT}}': str(f.SUPPORT_CHANNELS_TEXT),
|
|
|
|
// --- TOM ---
|
|
'{{ISB_NAME}}': str(tom.ISB_NAME),
|
|
'{{GF_NAME}}': str(tom.GF_NAME),
|
|
'{{DOCUMENT_VERSION}}': str(tom.DOCUMENT_VERSION),
|
|
'{{NEXT_REVIEW_DATE}}': str(tom.NEXT_REVIEW_DATE),
|
|
|
|
// --- DPA / AVV ---
|
|
'{{AG_NAME}}': str(dpa.AG_NAME) || str(c.LEGAL_NAME),
|
|
'{{AG_STRASSE}}': str(dpa.AG_STRASSE) || str(c.ADDRESS_LINE),
|
|
'{{AG_PLZ_ORT}}': str(dpa.AG_PLZ_ORT) || [c.POSTAL_CODE, c.CITY].filter(Boolean).join(' '),
|
|
'{{AN_NAME}}': str(dpa.AN_NAME) || str(p.LEGAL_NAME),
|
|
'{{AN_STRASSE}}': str(dpa.AN_STRASSE) || str(p.ADDRESS_LINE),
|
|
'{{AN_PLZ_ORT}}': str(dpa.AN_PLZ_ORT) || [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' '),
|
|
'{{VERARBEITUNGSGEGENSTAND}}': str(dpa.VERARBEITUNGSGEGENSTAND),
|
|
'{{VERARBEITUNGSZWECK}}': str(dpa.VERARBEITUNGSZWECK),
|
|
'{{VERARBEITUNGSARTEN}}': str(dpa.VERARBEITUNGSARTEN),
|
|
'{{DATENKATEGORIEN}}': str(dpa.DATENKATEGORIEN),
|
|
'{{PERSONENKATEGORIEN}}': str(dpa.PERSONENKATEGORIEN),
|
|
'{{BREACH_NOTIFICATION_HOURS}}': str(dpa.BREACH_NOTIFICATION_HOURS) || str(sec.INCIDENT_NOTICE_HOURS),
|
|
'{{INSTRUCTION_RETENTION_YEARS}}': str(dpa.INSTRUCTION_RETENTION_YEARS),
|
|
'{{SUB_PROCESSOR_NOTICE_WEEKS}}': str(dpa.SUB_PROCESSOR_NOTICE_WEEKS),
|
|
'{{SUB_PROCESSOR_OBJECTION_WEEKS}}': str(dpa.SUB_PROCESSOR_OBJECTION_WEEKS),
|
|
'{{DATA_EXPORT_FORMAT}}': str(dpa.DATA_EXPORT_FORMAT),
|
|
'{{RETURN_CHOICE_WEEKS}}': str(dpa.RETURN_CHOICE_WEEKS),
|
|
'{{DELETION_DAYS}}': str(dpa.DELETION_DAYS),
|
|
'{{REACTIVATION_MONTHS}}': str(dpa.REACTIVATION_MONTHS),
|
|
'{{TERMINATION_WEEKS}}': str(dpa.TERMINATION_WEEKS),
|
|
'{{CHANGE_NOTICE_WEEKS}}': str(dpa.CHANGE_NOTICE_WEEKS),
|
|
'{{THIRD_COUNTRY_OBJECTION_WEEKS}}': str(dpa.THIRD_COUNTRY_OBJECTION_WEEKS),
|
|
'{{AN_DSB_NAME}}': str(dpa.AN_DSB_NAME) || str(prv.DPO_NAME),
|
|
'{{AN_DSB_EMAIL}}': str(dpa.AN_DSB_EMAIL) || str(prv.DPO_EMAIL),
|
|
'{{AG_ORT}}': str(dpa.AG_ORT),
|
|
'{{AN_ORT}}': str(dpa.AN_ORT),
|
|
'{{VERTRAGSDATUM}}': str(dpa.VERTRAGSDATUM) || str(l.VERSION_DATE),
|
|
'{{AG_UNTERZEICHNER_NAME}}': str(dpa.AG_UNTERZEICHNER_NAME),
|
|
'{{AG_UNTERZEICHNER_FUNKTION}}': str(dpa.AG_UNTERZEICHNER_FUNKTION),
|
|
'{{AN_UNTERZEICHNER_NAME}}': str(dpa.AN_UNTERZEICHNER_NAME) || str(p.CEO_NAME),
|
|
'{{AN_UNTERZEICHNER_FUNKTION}}': str(dpa.AN_UNTERZEICHNER_FUNKTION),
|
|
'{{GERICHTSSTAND}}': str(dpa.GERICHTSSTAND) || str(l.JURISDICTION_CITY),
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Computed flags
|
|
// =============================================================================
|
|
|
|
export function computeFlags(ctx: TemplateContext): ComputedFlags {
|
|
return {
|
|
IS_B2C: ctx.CUSTOMER.IS_CONSUMER,
|
|
IS_B2B: ctx.CUSTOMER.IS_BUSINESS,
|
|
SERVICE_IS_SAAS: ctx.SERVICE.MODEL === 'SaaS',
|
|
SERVICE_IS_HYBRID: ctx.SERVICE.MODEL === 'Hybrid',
|
|
HAS_PENALTY: ctx.NDA.PENALTY_AMOUNT_EUR !== null && ctx.NDA.PENALTY_AMOUNT_EUR !== '',
|
|
HAS_ANALYTICS: !!ctx.CONSENT.ANALYTICS_TOOLS,
|
|
HAS_MARKETING: !!ctx.CONSENT.MARKETING_PARTNERS,
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Section relevance — which sections are useful for a given template
|
|
// =============================================================================
|
|
|
|
/** All placeholder patterns covered by each context section */
|
|
const SECTION_COVERS: Record<keyof TemplateContext, string[]> = {
|
|
PROVIDER: ['{{COMPANY_NAME}}', '{{PROVIDER_NAME}}', '{{DISCLOSING_PARTY}}', '{{SERVICE_PROVIDER}}', '{{COMPANY_ADDRESS}}', '{{PROVIDER_ADDRESS}}', '{{CONTACT_EMAIL}}', '{{PROVIDER_EMAIL}}', '{{ABUSE_EMAIL}}', '{{REPORT_EMAIL}}', '{{COMPANY_LEGAL_NAME}}', '{{COMPANY_LEGAL_FORM}}', '{{COMPANY_ADDRESS_LINE}}', '{{COMPANY_POSTAL_CODE}}', '{{COMPANY_CITY}}', '{{COMPANY_COUNTRY}}', '{{COMPANY_ADDRESS_FULL}}', '{{WEBSITE_URL}}', '{{CONTACT_PHONE}}', '{{REPRESENTED_BY_NAME}}', '{{REGISTER_COURT}}', '{{REGISTER_NUMBER}}', '{{VAT_ID}}'],
|
|
CUSTOMER: ['{{CUSTOMER_NAME}}', '{{RECEIVING_PARTY}}', '{{CUSTOMER}}'],
|
|
SERVICE: ['{{SERVICE_NAME}}', '{{PLATFORM_NAME}}', '{{SERVICE_DESCRIPTION}}', '{{SERVICE_MODEL}}', '{{SERVICE_TIER}}', '{{DATA_LOCATION}}', '{{EXPORT_FORMATS}}', '{{EXPORT_WINDOW_DAYS}}', '{{MIN_TERM_MONTHS}}', '{{TERMINATION_NOTICE_DAYS}}', '{{SERVICE_DESCRIPTION_SHORT}}', '{{NOTICE_PERIOD_DAYS}}', '{{DATA_PROCESSING_LOCATIONS}}'],
|
|
LEGAL: ['{{GOVERNING_LAW}}', '{{JURISDICTION_CITY}}', '{{VERSION_DATE}}', '{{EFFECTIVE_DATE}}', '{{START_DATE}}'],
|
|
PRIVACY: ['{{PRIVACY_CONTACT_EMAIL}}', '{{DPO_NAME}}', '{{DPO_EMAIL}}', '{{COPYRIGHT_CONTACT_NAME}}', '{{COPYRIGHT_EMAIL}}', '{{PRIVACY_POLICY_URL}}', '{{COOKIE_POLICY_URL}}', '{{ANALYTICS_RETENTION_MONTHS}}', '{{DATA_TRANSFER_THIRD_COUNTRIES}}', '{{SUPERVISORY_AUTHORITY_NAME}}', '{{SUPERVISORY_AUTHORITY_ADDRESS}}'],
|
|
SLA: ['{{AVAILABILITY_PERCENT}}', '{{MAINTENANCE_NOTICE_HOURS}}', '{{SUPPORT_EMAIL}}', '{{SUPPORT_PHONE}}', '{{SUPPORT_HOURS}}', '{{RESPONSE_CRITICAL_H}}', '{{RESOLUTION_CRITICAL_H}}', '{{RESPONSE_HIGH_H}}', '{{RESOLUTION_HIGH_H}}', '{{RESPONSE_MEDIUM_H}}', '{{RESOLUTION_MEDIUM_H}}', '{{RESPONSE_LOW_H}}'],
|
|
PAYMENTS: ['{{MONTHLY_FEE}}', '{{PAYMENT_DUE_DAY}}', '{{PAYMENT_METHOD}}', '{{PAYMENT_DAYS}}'],
|
|
SECURITY: ['{{INCIDENT_NOTICE_HOURS}}', '{{LOG_RETENTION_DAYS}}', '{{SECURITY_LOG_RETENTION_DAYS}}'],
|
|
NDA: ['{{PURPOSE}}', '{{DURATION_YEARS}}', '{{PENALTY_AMOUNT}}'],
|
|
CONSENT: ['{{WEBSITE_NAME}}', '{{ANALYTICS_TOOLS}}', '{{MARKETING_PARTNERS}}', '{{ANALYTICS_TOOLS_LIST}}', '{{MARKETING_PARTNERS_LIST}}'],
|
|
HOSTING: ['{{HOSTING_PROVIDER_NAME}}', '{{HOSTING_PROVIDER_COUNTRY}}', '{{HOSTING_PROVIDER_CONTRACT_TYPE}}'],
|
|
FEATURES: ['{{CONSENT_WITHDRAWAL_PATH}}', '{{SECURITY_MEASURES_SUMMARY}}', '{{DATA_SUBJECT_REQUEST_CHANNEL}}', '{{TRANSFER_GUARDS}}', '{{REGULATED_PROFESSION_TEXT}}', '{{EDITORIAL_RESPONSIBLE_NAME}}', '{{EDITORIAL_RESPONSIBLE_ADDRESS}}', '{{DISPUTE_RESOLUTION_TEXT}}', '{{NEWSLETTER_PROVIDER_DETAIL}}', '{{PAYMENT_PROVIDER_DETAIL}}', '{{SOCIAL_MEDIA_DETAIL}}', '{{ANALYTICS_TOOLS_DETAIL}}', '{{MARKETING_TOOLS_DETAIL}}', '{{CMP_NAME}}', '{{PRICES_TEXT}}', '{{PAYMENT_TERMS_TEXT}}', '{{CONTRACT_TERM_TEXT}}', '{{SLA_URL}}', '{{EXPORT_POLICY_TEXT}}', '{{LIMITATION_CAP_TEXT}}', '{{CONSUMER_WITHDRAWAL_TEXT}}', '{{SUPPORT_CHANNELS_TEXT}}'],
|
|
TOM: ['{{ISB_NAME}}', '{{GF_NAME}}', '{{DOCUMENT_VERSION}}', '{{NEXT_REVIEW_DATE}}'],
|
|
DPA: ['{{AG_NAME}}', '{{AG_STRASSE}}', '{{AG_PLZ_ORT}}', '{{AN_NAME}}', '{{AN_STRASSE}}', '{{AN_PLZ_ORT}}', '{{VERARBEITUNGSGEGENSTAND}}', '{{VERARBEITUNGSZWECK}}', '{{VERARBEITUNGSARTEN}}', '{{DATENKATEGORIEN}}', '{{PERSONENKATEGORIEN}}', '{{BREACH_NOTIFICATION_HOURS}}', '{{INSTRUCTION_RETENTION_YEARS}}', '{{SUB_PROCESSOR_NOTICE_WEEKS}}', '{{SUB_PROCESSOR_OBJECTION_WEEKS}}', '{{DATA_EXPORT_FORMAT}}', '{{RETURN_CHOICE_WEEKS}}', '{{DELETION_DAYS}}', '{{REACTIVATION_MONTHS}}', '{{TERMINATION_WEEKS}}', '{{AN_DSB_NAME}}', '{{AN_DSB_EMAIL}}', '{{AG_ORT}}', '{{AN_ORT}}', '{{VERTRAGSDATUM}}', '{{AG_UNTERZEICHNER_NAME}}', '{{AG_UNTERZEICHNER_FUNKTION}}', '{{AN_UNTERZEICHNER_NAME}}', '{{AN_UNTERZEICHNER_FUNKTION}}', '{{GERICHTSSTAND}}'],
|
|
}
|
|
|
|
/**
|
|
* Returns which context sections are relevant for a given list of template placeholders.
|
|
*/
|
|
export function getRelevantSections(placeholders: string[]): (keyof TemplateContext)[] {
|
|
const pSet = new Set(placeholders)
|
|
return (Object.keys(SECTION_COVERS) as (keyof TemplateContext)[]).filter(
|
|
(section) => SECTION_COVERS[section].some((ph) => pSet.has(ph))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Returns a list of placeholder keys that are NOT covered by the context bridge.
|
|
* These need to be filled in manually.
|
|
*/
|
|
export function getUncoveredPlaceholders(placeholders: string[], ctx: TemplateContext): string[] {
|
|
const bridged = contextToPlaceholders(ctx)
|
|
return placeholders.filter((ph) => !(ph in bridged))
|
|
}
|
|
|
|
/**
|
|
* Strict validation: returns placeholder keys that are required but empty.
|
|
* "Required" means: placeholder appears in template AND bridge covers it AND bridge produces empty string.
|
|
*/
|
|
export function getMissingRequired(placeholders: string[], ctx: TemplateContext): string[] {
|
|
const bridged = contextToPlaceholders(ctx)
|
|
return placeholders.filter((ph) => ph in bridged && !bridged[ph])
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helpers to set nested context values by dot-path
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Returns a new TemplateContext with the value at dotPath set.
|
|
* e.g. setContextPath(ctx, 'PROVIDER.LEGAL_NAME', 'ACME GmbH')
|
|
*/
|
|
export function setContextPath(ctx: TemplateContext, dotPath: string, value: unknown): TemplateContext {
|
|
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
|
|
const key = rest.join('.')
|
|
return {
|
|
...ctx,
|
|
[section]: {
|
|
...(ctx[section] as unknown as Record<string, unknown>),
|
|
[key]: value,
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a nested context value by dot-path.
|
|
*/
|
|
export function getContextPath(ctx: TemplateContext, dotPath: string): unknown {
|
|
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
|
|
const sectionObj = ctx[section] as unknown as Record<string, unknown>
|
|
return sectionObj?.[rest.join('.')]
|
|
}
|