Files
breakpilot-compliance/admin-compliance/app/sdk/document-generator/contextBridge.ts
Benjamin Admin e0f7f2134e
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Template-Spec v1 Phase C — IF-Renderer + HOSTING/FEATURES + 4 neue DE-Templates
- contextBridge.ts: HostingCtx + FeaturesCtx (35 Felder), ~50 neue Platzhalter-Aliases
- ruleEngine.ts: buildBoolContext() + applyConditionalBlocks() (IF/IF_NOT/IF_ANY)
- ruleEngine.test.ts: 67 Tests (+18 für Phase C), alle grün
- page.tsx: IF-Renderer in Pipeline, HOSTING+FEATURES Formular-Sections, erweiterter SDK-Prefill
- scripts/apply_templates_023.py: 4 neue DE-Templates (Cookie v2, DSE, AGB, Impressum)
- migrations/023_new_templates_de.sql: Dokumentation + Verifikations-Query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 14:35:56 +01:00

532 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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, string>
* → 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: 'MoFr 09:0018: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<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 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<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}}'],
}
/**
* 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<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 Record<string, unknown>
return sectionObj?.[rest.join('.')]
}