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
- 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>
532 lines
23 KiB
TypeScript
532 lines
23 KiB
TypeScript
/**
|
||
* 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: '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<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('.')]
|
||
}
|