Skip to main content
Traducción. Autoritativo: fs-msg-0003-sends-and-status.md.

Contexto

DEC-E7 pide trazabilidad end to end: desde el evento de dominio, por la regla que disparó, el job de notificación, el envío, su estado en el proveedor, y hacia afuera al webhook del tenant — todo unido por un correlation_id. Esa es la diferencia entre una respuesta de soporte y un encogimiento de hombros. La consecuencia de diseño es que un envío es append-only con historial de estados separado. Sobreescribir una columna de estado perdería la línea de tiempo, y la línea de tiempo es lo que responde la pregunta. También hace de la tabla de estados la de mayor volumen del módulo, y por eso se particiona desde el inicio.

Alcance (normativo)

  • messaging.sends: una fila por mensaje por destinatario por canal.
  • messaging.send_status_history: append-only, particionada mensualmente.
  • messaging.send_statuses: paramétrica, queued → sent → delivered → opened → clicked, más bounced, complained, failed.
  • Ingesta de estados por webhooks del proveedor.
  • correlation_id y origin_event_id en cada envío.
  • Eventos de dominio por transición, disponibles como webhooks salientes.

Fuera de alcance (normativo)

  • Decidir enviar — FS-MSG-0001. Una fila de envío existe solo tras pasar la cascada.
  • Despacho y rate limiting — FS-MSG-0005.
  • Analítica y agregación. Esto es el log de eventos; la reportería lo lee.

Comportamiento (normativo)

  1. Una fila de envío se crea solo después de que resolveChannels pasó todas las compuertas. Un mensaje bloqueado no produce fila — produce una razón registrada (FS-MSG-0001).
  2. Todo envío lleva correlation_id y origin_event_id, heredados del evento que lo causó. Un envío sin ninguno de los dos es un defecto: no se puede explicar después.
  3. El historial es append-only. Los estados llegan desordenados —delivered después de opened pasa con clientes de correo agresivos— y el historial guarda lo que llegó, en orden de llegada, con los timestamps del proveedor.
  4. El estado actual se deriva del historial, nunca se guarda como columna mutable.
  5. Los webhooks del proveedor se verifican por firma y son idempotentes: el mismo evento entregado dos veces escribe una fila.
  6. Un bounced duro o un complained escriben una supresión automáticamente (FS-MSG-0004), en la misma transacción. Un rebote que no suprime volverá a rebotar.
  7. send_status_history se particiona mensualmente y sigue la retención del plan, exportando antes de soltar la partición. Los conteos agregados se conservan siempre.
  8. El contenido del mensaje se guarda en el envío y se borra con el titular al suprimirse; la fila sobrevive como contador anonimizado.
  9. Toda consulta contra el historial lleva el predicado de partición.

Datos (normativo)

API (normativo)

El rango de tiempo del listado es obligatorio: es la clave de partición.

Eventos (normativo)

messaging.message.sent|delivered|opened|clicked|bounced|complained, todos disponibles como webhooks salientes. Llevan contact_id, send_id y el correlation_id heredado, nunca contenido.

Criterios de aceptación (normativo)

  1. Una fila de envío existe solo cuando la cascada pasó; un mensaje bloqueado produce una razón y ninguna fila.
  2. Todo envío tiene correlation_id y origin_event_id no nulos.
  3. Los estados que llegan desordenados se conservan todos, y el estado actual derivado es correcto.
  4. El mismo webhook entregado dos veces escribe una fila de historial.
  5. Un webhook con firma inválida se rechaza sin escritura.
  6. Un rebote duro escribe una supresión en la misma transacción que el estado.
  7. Una consulta rastrea desde un origin_event_id hasta todo envío resultante y su estado final.
  8. La supresión borra el contenido y deja la fila como contador anonimizado.
  9. Negativo: no existe columna mutable de estado actual en sends.

Ejecución

Pipeline asíncrono. Las filas de envío las escribe el worker de despacho; los webhooks del proveedor aterrizan en backend/api y encolan su procesamiento.

Preguntas abiertas

Changelog

Registro de entrega

Aún no implementado.