# RenderNotificationTemplate

## Overview

RenderNotificationTemplate is the dispatch-time render entry point used by the dispatcher (and by template-preview tooling). Given `{ eventType, channelId, locale, payloadVars }`, it locates a NotificationTemplate by `(eventType, channelId, locale)` falling back to `(eventType, channelId, defaultLocale)` when no exact match exists, validates `payloadVars` against the template's declared variable schema, and performs `{{variable}}` substitution against `subject`, `body`, and (when present) `htmlBody`. Substitution into `htmlBody` HTML-escapes user-supplied values; substitution into the plain-text `body` inserts values verbatim. The result is `{ subject, body, htmlBody? }` returned to the caller.

This is a read-only computation: rendered output is never persisted by this query — the parent Notification row stores `payloadVars` (the input), not the rendered text. Re-rendering on demand always reflects the current template content. CQRS-wise, RenderNotificationTemplate is a pure-read query because it neither mutates the template, the variable schema, nor any Notification row.

## Business Rules

- Accepts `{ eventType, channelId, locale, payloadVars, defaultLocale? }`; `defaultLocale` is optional and defaults to `en-US`
- Looks up the template by exact `(eventType, channelId, locale)` first
- Falls back to `(eventType, channelId, defaultLocale)` when the exact-locale row is missing and `locale != defaultLocale`
- Fails with `TEMPLATE_NOT_FOUND` when neither the exact-locale nor the default-locale row exists
- Validates `payloadVars` against the template's `variableSchema` — missing required variables or wrong types raise `TEMPLATE_VAR_VALIDATION_FAILED`
- Substitutes `{{variable}}` placeholders in `subject`, `body`, and `htmlBody` (when present)
- HTML-escapes substituted values inside `htmlBody`; inserts values verbatim in `body`
- Optional declared variables that are not supplied render as empty string (no error)
- Variables present in `payloadVars` but not declared by the schema are silently ignored (extras are not an error)
- Templating supports `{{variable}}` substitution only — no conditionals, loops, or filters; constructs that look like control flow are inserted as literal text
- Returns `{ subject, body, htmlBody? }` — `htmlBody` is undefined / absent when the template does not declare one
- Does not persist any state; the rendered output is render-and-forget

## Process Flow

```mermaid
flowchart TD
    A[Receive eventType, channelId, locale, payloadVars] --> B[Look up template by eventType+channelId+locale]
    B --> C{Row found?}
    C -->|Yes| F[Validate payloadVars against variableSchema]
    C -->|No, locale != defaultLocale| D[Look up by eventType+channelId+defaultLocale]
    D --> E{Row found?}
    E -->|Yes| F
    E -->|No| G[Error: TEMPLATE_NOT_FOUND]
    C -->|No, locale == defaultLocale| G
    F -->|Required missing or wrong type| H[Error: TEMPLATE_VAR_VALIDATION_FAILED]
    F -->|Valid| I[Interpolate placeholders in subject and body, verbatim]
    I --> J{Template has htmlBody?}
    J -->|Yes| K[Interpolate htmlBody with HTML-escaped values]
    J -->|No| L[Return subject, body, htmlBody undefined]
    K --> M[Return subject, body, htmlBody]
```

## External Dependencies

- None

## Error Scenarios

- **TEMPLATE_NOT_FOUND**: No NotificationTemplate row matches the supplied identifier
- **TEMPLATE_VAR_VALIDATION_FAILED**: `payloadVars` is missing a required declared variable or contains a wrong-type value

## Test Cases

- renders subject and body with placeholders substituted when an exact-locale template exists and payloadVars satisfies the schema
- falls back to the default-locale template when the exact-locale row is missing
- prefers the exact-locale template when both the exact and default-locale rows exist
- fails with TEMPLATE_NOT_FOUND when neither the exact-locale nor the default-locale template exists
- fails with TEMPLATE_VAR_VALIDATION_FAILED when a required declared variable is missing
- fails with TEMPLATE_VAR_VALIDATION_FAILED when a declared variable receives a wrong-type value
- ignores variables present in payloadVars that are not declared by the schema
- renders an optional declared variable that is not supplied as empty string
- returns htmlBody when the template declares one and applies substitution to it
- returns htmlBody as undefined / absent when the template does not declare one
- HTML-escapes user-supplied variable values in htmlBody so embedded markup cannot break the document
- inserts variable values verbatim in the plain-text body without HTML-escaping
- substitutes the same variable at every position in subject, body, and htmlBody
- treats constructs that look like loops or conditionals (e.g. `{{#if}}`) as literal text
- does not persist any state; subsequent template edits are reflected on the next render call
