> ## 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.

# Catálogo de canales y resolución en cascada

> Cuando esto se entregue, cualquier módulo podrá preguntar "¿puedo notificar a esta persona sobre esto, y cómo?" y recibir una respuesta que ya considera nuestro estado operativo.

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

> Cuando esto se entregue, cualquier módulo podrá preguntar "¿puedo notificar a esta persona sobre
> esto, y cómo?" y recibir una respuesta que ya considera nuestro estado operativo, la configuración
> del tenant y las decisiones de la propia persona.

## Contexto

Todo módulo necesita notificar a alguien, y sin una respuesta única cada uno inventa la suya — uno
verifica consentimiento, otro olvida quiet hours, un tercero envía igual porque su mensaje le pareció
importante. Así termina una plataforma con cuatro caminos de entrega y un reclamo.

ADR-019 lo convierte en una función pura sobre cuatro niveles de configuración y un conjunto de
compuertas del destinatario. Que sea pura importa: `resolveChannels` no hace I/O, así que se puede
testear exhaustivamente, y un cambio de reglas es un cambio en una función probada y no en cuatro
lugares.

La fila del canal y el puerto existen desde el día uno aunque el adaptador llegue después (DEC-E1).

## Alcance *(normativo)*

* Los cuatro niveles: plataforma, tenant, módulo, enrutamiento de evento.
* `resolveChannels(evento, tenant, módulo, destinatario)` como función pura en `packages/core`.
* Compuertas del destinatario: consentimiento, supresión, centro de preferencias, y para marketing
  quiet hours y frequency caps.
* Categoría por entrada de enrutamiento: `transactional | marketing | product`.
* Una razón tipada para cada exclusión, registrada cuando un mensaje no se envía.
* Endpoints de gestión para cada nivel.

## Fuera de alcance *(normativo)*

* Renderizar — FS-MSG-0002. La resolución decide si y dónde, no qué.
* Encolar y entregar — FS-MSG-0005 y FS-MSG-0006.
* El almacenamiento del consentimiento, que es de `core` y acá se lee, nunca se escribe.
* Fallback entre canales (push falla → email), que es F2 y queda anotado (DEC-E4).

## Comportamiento *(normativo)*

1. La resolución corre los niveles **en orden**, y cualquiera que diga no la termina: plataforma
   operativa → tenant habilitado y configurado → módulo habilitado → existe enrutamiento del evento.
2. Luego las compuertas del destinatario: **consentimiento para ese canal** ∧ **no suprimido** ∧ el
   centro de preferencias lo permite ∧ (solo marketing) quiet hours y frequency caps.
3. **Transaccional no es opt-out** (DEC-E3). Pasa el centro de preferencias, quiet hours y caps, pero
   **nunca** pasa la supresión: un rebote duro significa que la dirección no funciona, y la categoría
   no cambia eso.
4. Cada "no" devuelve una **razón tipada**, y la razón se registra. "No enviado" sin razón hace
   imposible el soporte e imposible responder una pregunta de cumplimiento.
5. La función es **pura**: sin base de datos, sin caché, sin leer el reloj. Sus entradas son el
   evento, la configuración ya cargada y el snapshot del destinatario.
6. Las quiet hours se evalúan en la **zona horaria del destinatario** cuando se conoce, cayendo a la
   del tenant. Una regla de quiet hours en la zona equivocada es peor que ninguna.
7. El resultado es una **lista de canales**, no uno. Un evento puede ir legítimamente a in-app y email.
8. PROHIBIDO: cualquier camino de envío que no llame a esta función · una configuración de tenant que
   desactive la verificación de supresión o de consentimiento · una categoría que salte la cascada.

## Datos *(normativo)*

| Tabla                               | Invariantes clave                                                                                                          |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `messaging.platform_channel_status` | una fila por canal; `is_operative`; refleja un flag operacional; es nuestra, no del tenant                                 |
| `messaging.tenant_channel_settings` | (`tenant_id`, `channel_code`); `is_enabled`; `credentials_ref`; habilitar exige el entitlement y credenciales configuradas |
| `messaging.module_channel_settings` | (`tenant_id`, `module`, `channel_code`); `is_enabled`                                                                      |
| `messaging.event_channel_routing`   | (`tenant_id`, `event_name`, `channel_code`); `template_id`; `category`; `is_enabled`                                       |

## API *(normativo)*

| Endpoint                                 | Clase      | Permiso                             | Presupuesto |
| ---------------------------------------- | ---------- | ----------------------------------- | ----------- |
| `GET/PUT /v1/messaging/channel-settings` | Management | `messaging.channels.{read\|update}` | p95 \<1 s   |
| `GET/PUT /v1/messaging/event-routing`    | Management | `messaging.routing.{read\|update}`  | p95 \<1 s   |
| `POST /v1/messaging/routing/simulate`    | Management | `messaging.routing.simulate`        | p95 \<1 s   |

`simulate` responde "si este evento ocurriera para este contacto ahora, qué enviaríamos y por qué no
el resto" — la herramienta de soporte más útil del módulo.

## Eventos *(normativo)*

Ninguno emitido. Esta feature consume los eventos de todos los módulos y decide qué sigue.

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

1. 100% de cobertura de ramas en `resolveChannels`, con un fixture por nivel y por compuerta.
2. Un destinatario suprimido no recibe nada, incluido transaccional.
3. Quien optó por salir de marketing sigue recibiendo transaccional.
4. Las quiet hours se evalúan en la zona del destinatario, verificado cruzando la línea de cambio de
   fecha.
5. Cada exclusión devuelve una razón tipada distinta, y la razón queda registrada.
6. Desactivar un canal a nivel de módulo detiene los envíos de ese módulo sin afectar a otros.
7. `simulate` no escribe nada y devuelve tanto los canales resueltos como las razones del resto.
8. **Negativo:** ninguna configuración desactiva la compuerta de consentimiento ni la de supresión.

## Ejecución

Un solo slice, comando síncrono. La función pura vive en `packages/core` junto al evaluador de
acumulación; las tablas y endpoints en `backend/api`. Parte de TS-002.

## Preguntas abiertas

| # | Pregunta                                                                                          | Decide | Para             |
| - | ------------------------------------------------------------------------------------------------- | ------ | ---------------- |
| 1 | Quiet hours por defecto para un tenant nuevo — ¿21:00–09:00 local, o ninguna hasta configurarlas? | 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.*
