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 uncorrelation_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ásbounced,complained,failed.- Ingesta de estados por webhooks del proveedor.
correlation_idyorigin_event_iden 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)
- Una fila de envío se crea solo después de que
resolveChannelspasó todas las compuertas. Un mensaje bloqueado no produce fila — produce una razón registrada (FS-MSG-0001). - Todo envío lleva
correlation_idyorigin_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. - El historial es append-only. Los estados llegan desordenados —
delivereddespués deopenedpasa con clientes de correo agresivos— y el historial guarda lo que llegó, en orden de llegada, con los timestamps del proveedor. - El estado actual se deriva del historial, nunca se guarda como columna mutable.
- Los webhooks del proveedor se verifican por firma y son idempotentes: el mismo evento entregado dos veces escribe una fila.
- Un
bouncedduro o uncomplainedescriben una supresión automáticamente (FS-MSG-0004), en la misma transacción. Un rebote que no suprime volverá a rebotar. send_status_historyse particiona mensualmente y sigue la retención del plan, exportando antes de soltar la partición. Los conteos agregados se conservan siempre.- El contenido del mensaje se guarda en el envío y se borra con el titular al suprimirse; la fila sobrevive como contador anonimizado.
- 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)
- Una fila de envío existe solo cuando la cascada pasó; un mensaje bloqueado produce una razón y ninguna fila.
- Todo envío tiene
correlation_idyorigin_event_idno nulos. - Los estados que llegan desordenados se conservan todos, y el estado actual derivado es correcto.
- El mismo webhook entregado dos veces escribe una fila de historial.
- Un webhook con firma inválida se rechaza sin escritura.
- Un rebote duro escribe una supresión en la misma transacción que el estado.
- Una consulta rastrea desde un
origin_event_idhasta todo envío resultante y su estado final. - La supresión borra el contenido y deja la fila como contador anonimizado.
- 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 enbackend/api y encolan su procesamiento.