/** * Template-Spec v1 — Context Bridge * * Maps a structured TemplateContext (canonical entities: PROVIDER, CUSTOMER, etc.) * to the flat {{PLACEHOLDER}} map used by legacy templates. * * Flow: TemplateContext → contextToPlaceholders() → Record * → string.replace({{KEY}}, value) in renderer */ // ============================================================================= // Types // ============================================================================= export interface ProviderCtx { LEGAL_NAME: string LEGAL_FORM?: string ADDRESS_LINE: string POSTAL_CODE: string CITY: string COUNTRY: string EMAIL: string PHONE?: string WEBSITE_URL?: string CEO_NAME?: string REGISTER_COURT?: string REGISTER_NUMBER?: string VAT_ID?: string } export interface CustomerCtx { LEGAL_NAME: string ADDRESS_LINE?: string POSTAL_CODE?: string CITY?: string COUNTRY?: string CONTACT_NAME?: string EMAIL: string IS_CONSUMER: boolean IS_BUSINESS: boolean } export interface ServiceCtx { NAME: string DESCRIPTION: string MODEL: 'SaaS' | 'PaaS' | 'IaaS' | 'OnPrem' | 'Hybrid' | '' TIER: string DATA_LOCATION: string EXPORT_FORMATS?: string[] EXPORT_WINDOW_DAYS?: number MIN_TERM_MONTHS?: number TERMINATION_NOTICE_DAYS?: number } export interface LegalCtx { GOVERNING_LAW: string JURISDICTION_CITY: string VERSION_DATE: string // YYYY-MM-DD EFFECTIVE_DATE: string // YYYY-MM-DD LANG: 'de' | 'en' } export interface PrivacyCtx { CONTACT_EMAIL: string DPO_NAME: string DPO_EMAIL: string SUPERVISORY_AUTHORITY_NAME?: string SUPERVISORY_AUTHORITY_ADDRESS?: string PRIVACY_POLICY_URL: string COOKIE_POLICY_URL?: string ANALYTICS_RETENTION_MONTHS?: number DATA_TRANSFER_THIRD_COUNTRIES?: string } export interface SLACtx { AVAILABILITY_PERCENT: number | '' MAINTENANCE_NOTICE_HOURS: number | '' SUPPORT_EMAIL: string SUPPORT_PHONE?: string SUPPORT_HOURS: string RESPONSE_CRITICAL_H: number | '' RESOLUTION_CRITICAL_H: number | '' RESPONSE_HIGH_H: number | '' RESOLUTION_HIGH_H: number | '' RESPONSE_MEDIUM_H: number | '' RESOLUTION_MEDIUM_H: number | '' RESPONSE_LOW_H: number | '' } export interface PaymentsCtx { MONTHLY_FEE_EUR: number | '' PAYMENT_DUE_DAY: number | '' PAYMENT_METHOD: string PAYMENT_DAYS: number | '' } export interface SecurityCtx { INCIDENT_NOTICE_HOURS: number | '' LOG_RETENTION_DAYS: number | '' SECURITY_LOG_RETENTION_DAYS: number | '' } export interface NDACtx { PURPOSE: string DURATION_YEARS: number | '' PENALTY_AMOUNT_EUR: number | null | '' } export interface ConsentCtx { WEBSITE_NAME: string ANALYTICS_TOOLS: string | null MARKETING_PARTNERS: string | null } export interface HostingCtx { PROVIDER_NAME: string COUNTRY: string CONTRACT_TYPE: string // e.g. "AVV nach Art. 28 DSGVO" } export interface FeaturesCtx { // ── Impressum ────────────────────────────────────────────────────────────── HAS_REGULATED_PROFESSION: boolean REGULATED_PROFESSION_TEXT: string HAS_EDITORIAL_RESPONSIBLE: boolean EDITORIAL_RESPONSIBLE_NAME: string EDITORIAL_RESPONSIBLE_ADDRESS: string HAS_DISPUTE_RESOLUTION: boolean DISPUTE_RESOLUTION_TEXT: string // ── DSI / Datenschutzerklärung ───────────────────────────────────────────── HAS_THIRD_COUNTRY: boolean TRANSFER_GUARDS: string // e.g. "EU-Standardvertragsklauseln (SCC)" SECURITY_MEASURES_SUMMARY: string // e.g. "TLS, RBAC, Backups, Logging" DATA_SUBJECT_REQUEST_CHANNEL: string // e.g. "per E-Mail an {{CONTACT_EMAIL}}" CONSENT_WITHDRAWAL_PATH: string // e.g. "Footer-Link ›Cookie-Einstellungen‹" // ── Service-Features (DSI/AGB) ───────────────────────────────────────────── HAS_NEWSLETTER: boolean NEWSLETTER_PROVIDER_DETAIL: string HAS_ACCOUNT: boolean HAS_PAYMENTS: boolean PAYMENT_PROVIDER_DETAIL: string HAS_SUPPORT: boolean HAS_SOCIAL_MEDIA: boolean SOCIAL_MEDIA_DETAIL: string // ── Tool-Details (DSI + Cookie Banner) ──────────────────────────────────── ANALYTICS_TOOLS_DETAIL: string MARKETING_TOOLS_DETAIL: string CMP_NAME: string CMP_LOGS_CONSENTS: boolean HAS_FUNCTIONAL_COOKIES: boolean // ── AGB (SaaS) ──────────────────────────────────────────────────────────── HAS_PAID_PLANS: boolean PRICES_TEXT: string PAYMENT_TERMS_TEXT: string CONTRACT_TERM_TEXT: string HAS_SLA: boolean SLA_URL: string HAS_EXPORT_POLICY: boolean EXPORT_POLICY_TEXT: string LIMITATION_CAP_TEXT: string HAS_WITHDRAWAL: boolean CONSUMER_WITHDRAWAL_TEXT: string SUPPORT_CHANNELS_TEXT: string } export interface TemplateContext { PROVIDER: ProviderCtx CUSTOMER: CustomerCtx SERVICE: ServiceCtx LEGAL: LegalCtx PRIVACY: PrivacyCtx SLA: SLACtx PAYMENTS: PaymentsCtx SECURITY: SecurityCtx NDA: NDACtx CONSENT: ConsentCtx HOSTING: HostingCtx FEATURES: FeaturesCtx } export interface ComputedFlags { IS_B2C: boolean IS_B2B: boolean SERVICE_IS_SAAS: boolean SERVICE_IS_HYBRID: boolean HAS_PENALTY: boolean HAS_ANALYTICS: boolean HAS_MARKETING: boolean } // ============================================================================= // Empty default context // ============================================================================= export const EMPTY_CONTEXT: TemplateContext = { PROVIDER: { LEGAL_NAME: '', LEGAL_FORM: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '', COUNTRY: 'DE', EMAIL: '', PHONE: '', WEBSITE_URL: '', CEO_NAME: '', REGISTER_COURT: '', REGISTER_NUMBER: '', VAT_ID: '', }, CUSTOMER: { LEGAL_NAME: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '', COUNTRY: 'DE', CONTACT_NAME: '', EMAIL: '', IS_CONSUMER: false, IS_BUSINESS: true, }, SERVICE: { NAME: '', DESCRIPTION: '', MODEL: 'SaaS', TIER: '', DATA_LOCATION: 'Deutschland', EXPORT_FORMATS: [], EXPORT_WINDOW_DAYS: 30, MIN_TERM_MONTHS: 12, TERMINATION_NOTICE_DAYS: 30, }, LEGAL: { GOVERNING_LAW: 'Deutschland', JURISDICTION_CITY: '', VERSION_DATE: '', EFFECTIVE_DATE: '', LANG: 'de', }, PRIVACY: { CONTACT_EMAIL: '', DPO_NAME: '', DPO_EMAIL: '', SUPERVISORY_AUTHORITY_NAME: '', SUPERVISORY_AUTHORITY_ADDRESS: '', PRIVACY_POLICY_URL: '', COOKIE_POLICY_URL: '', ANALYTICS_RETENTION_MONTHS: 13, DATA_TRANSFER_THIRD_COUNTRIES: 'nicht statt', }, SLA: { AVAILABILITY_PERCENT: 99.5, MAINTENANCE_NOTICE_HOURS: 72, SUPPORT_EMAIL: '', SUPPORT_PHONE: '', SUPPORT_HOURS: 'Mo–Fr 09:00–18:00 CET', RESPONSE_CRITICAL_H: 2, RESOLUTION_CRITICAL_H: 8, RESPONSE_HIGH_H: 4, RESOLUTION_HIGH_H: 24, RESPONSE_MEDIUM_H: 24, RESOLUTION_MEDIUM_H: 120, RESPONSE_LOW_H: 72, }, PAYMENTS: { MONTHLY_FEE_EUR: '', PAYMENT_DUE_DAY: 1, PAYMENT_METHOD: 'Rechnung', PAYMENT_DAYS: 14, }, SECURITY: { INCIDENT_NOTICE_HOURS: 24, LOG_RETENTION_DAYS: 7, SECURITY_LOG_RETENTION_DAYS: 30, }, NDA: { PURPOSE: '', DURATION_YEARS: 5, PENALTY_AMOUNT_EUR: null }, CONSENT: { WEBSITE_NAME: '', ANALYTICS_TOOLS: null, MARKETING_PARTNERS: null }, HOSTING: { PROVIDER_NAME: '', COUNTRY: 'Deutschland', CONTRACT_TYPE: 'AVV nach Art. 28 DSGVO', }, FEATURES: { // Impressum HAS_REGULATED_PROFESSION: false, REGULATED_PROFESSION_TEXT: '', HAS_EDITORIAL_RESPONSIBLE: false, EDITORIAL_RESPONSIBLE_NAME: '', EDITORIAL_RESPONSIBLE_ADDRESS: '', HAS_DISPUTE_RESOLUTION: false, DISPUTE_RESOLUTION_TEXT: '', // DSI HAS_THIRD_COUNTRY: false, TRANSFER_GUARDS: 'EU-Standardvertragsklauseln (SCC)', SECURITY_MEASURES_SUMMARY: 'TLS 1.3, RBAC, Verschlüsselung at rest, regelmäßige Backups, Logging', DATA_SUBJECT_REQUEST_CHANNEL: 'per E-Mail', CONSENT_WITHDRAWAL_PATH: 'Footer-Link „Cookie-Einstellungen"', // Service features HAS_NEWSLETTER: false, NEWSLETTER_PROVIDER_DETAIL: '', HAS_ACCOUNT: false, HAS_PAYMENTS: false, PAYMENT_PROVIDER_DETAIL: '', HAS_SUPPORT: false, HAS_SOCIAL_MEDIA: false, SOCIAL_MEDIA_DETAIL: '', // Tools ANALYTICS_TOOLS_DETAIL: '', MARKETING_TOOLS_DETAIL: '', CMP_NAME: '', CMP_LOGS_CONSENTS: false, HAS_FUNCTIONAL_COOKIES: false, // AGB HAS_PAID_PLANS: false, PRICES_TEXT: '', PAYMENT_TERMS_TEXT: '', CONTRACT_TERM_TEXT: '', HAS_SLA: false, SLA_URL: '', HAS_EXPORT_POLICY: false, EXPORT_POLICY_TEXT: '', LIMITATION_CAP_TEXT: '', HAS_WITHDRAWAL: false, CONSUMER_WITHDRAWAL_TEXT: '', SUPPORT_CHANNELS_TEXT: '', }, } // ============================================================================= // 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 of placeholder values. * Keys are the {{PLACEHOLDER}} strings used in templates. */ export function contextToPlaceholders(ctx: TemplateContext): Record { 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 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), } } // ============================================================================= // 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 = { 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}}'], } /** * 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 Record), [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 Record return sectionObj?.[rest.join('.')] }