> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Plantillas de mensaje

> Cuando esto se entregue, un mensaje se verá como la marca del tenant, dirá bien el nombre del member, y no podrá salir con una variable que no existe.

> Traducción. Autoritativo: [`fs-msg-0002-templates.md`](/modules/messaging/features/fs-msg-0002-templates).

## Contexto

La falla que esta feature previene es la que todo cliente ha visto: `Hola {{first_name}},` llegando a
una bandeja. Ocurre cuando las plantillas aceptan variables arbitrarias y nadie las verifica contra
datos reales.

Acá las plantillas son **componentes React Email en el repositorio**, versionados y revisados como
código. Es un canje deliberado contra un editor visual: significa que cambiar una plantilla es un
deploy, y también que un marketero no la puede romper un viernes a las 6. Las variables se validan
contra `core.attribute_definitions`, de modo que una plantilla que referencia un campo que el tenant
no tiene falla al publicar y no al enviar.

## Alcance *(normativo)*

* `messaging.message_templates`: versionadas, por tenant, por canal.
* Componentes React Email con contrato de variables declarado.
* Validación de variables contra `core.attribute_definitions` al publicar.
* Branding por tenant: logo, colores, nombre del remitente, pie.
* Variantes por locale de la misma plantilla.
* Vista previa con datos de ejemplo y envío de prueba a una dirección elegida.

## Fuera de alcance *(normativo)*

* Un editor de arrastrar y soltar. Las plantillas son código.
* Traducción de textos. El tenant aporta el texto por locale.
* Testing A/B — F2, y pertenece a campañas y no a plantillas.

## Comportamiento *(normativo)*

1. Una plantilla es **versionada e inmutable una vez publicada**. Un envío registra la versión que
   usó, así un mensaje se puede reproducir exacto un año después durante una disputa.
2. Toda plantilla declara su **contrato de variables**. Publicar valida cada variable contra las
   definiciones de atributos del tenant y la forma del payload del evento; una variable desconocida
   **bloquea la publicación**.
3. Renderizar con un valor ausente usa el **fallback declarado**, obligatorio para toda variable. No
   hay camino que renderice un placeholder vacío ni un token crudo.
4. Una plantilla pertenece a exactamente un canal. El mismo mensaje en email y push son dos
   plantillas, porque las restricciones no son comparables.
5. Las variantes por locale comparten id; la resolución usa el locale del destinatario y cae al
   default del tenant. Una variante ausente cae, no falla.
6. Las plantillas de marketing **deben incluir enlace de baja**, inyectado por el renderer y no
   removible por el autor.
7. Vista previa y envíos de prueba tienen rate limit y quedan auditados. Un envío de prueba va solo a
   una dirección que el probador controla.
8. PROHIBIDO: renderizar con una variable no validada · una plantilla de marketing sin baja · editar
   una versión publicada en sitio.

## Datos *(normativo)*

| Tabla                         | Invariantes clave                                                                                                                                                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messaging.message_templates` | `tenant_id`; `code` único por (tenant, canal, locale); `channel_code` FK; `version` entero, inmutable una vez publicada; `component_ref`; `variables` JSONB con fallbacks obligatorios; `published_at`; `is_active` |

## API *(normativo)*

| Endpoint                                      | Clase      | Permiso                                      | Presupuesto |
| --------------------------------------------- | ---------- | -------------------------------------------- | ----------- |
| `GET/POST/PATCH /v1/messaging/templates`      | Management | `messaging.templates.{read\|create\|update}` | p95 \<1 s   |
| `POST /v1/messaging/templates/{id}/publish`   | Management | `messaging.templates.publish`                | p95 \<1 s   |
| `POST /v1/messaging/templates/{id}/preview`   | Management | `messaging.templates.preview`                | p95 \<1 s   |
| `POST /v1/messaging/templates/{id}/test-send` | Management | `messaging.templates.test_send`              | p95 \<1 s   |

## Eventos *(normativo)*

Ninguno. Los cambios de plantilla son administrativos y quedan cubiertos por la auditoría.

## Criterios de aceptación *(normativo)*

1. Publicar una plantilla con una variable inexistente para el tenant se rechaza, nombrándola.
2. Una versión publicada no se puede editar; editar crea la versión + 1.
3. Un envío registra su versión de plantilla, y esa versión renderiza igual un año después.
4. Un valor ausente renderiza su fallback declarado — nunca un placeholder vacío ni un token crudo.
5. Una plantilla de marketing renderiza con enlace de baja aunque el autor lo haya omitido.
6. Un destinatario con locale no soportado recibe la variante por defecto del tenant.
7. Un envío de prueba llega solo a la dirección indicada y queda auditado.
8. **Negativo:** ninguna variable sin fallback declarado se puede publicar.

## Ejecución

Un solo slice, comando síncrono. Los componentes viven en `packages/email-templates`; el registro y
los endpoints en `backend/api`. El renderizado ocurre en el worker, nunca en el request de la API.

## Preguntas abiertas

| # | Pregunta                                                                                        | Decide | Para             |
| - | ----------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | ¿Un tenant puede sobreescribir una plantilla de sistema (OTP, invitación), o esas son nuestras? | Daniel | antes de aprobar |

## Changelog

| Versión | Fecha      | Cambio           | Por qué | Autor                  |
| ------- | ---------- | ---------------- | ------- | ---------------------- |
| 0.1.0   | 2026-08-17 | Borrador inicial | —       | daniel + claude-opus-5 |

## Registro de entrega

*Aún no implementado.*
