services/api/src/modules/notifications/notification.service.js
Resumen
Este archivo implementa la entrega de entradas del outbox de notificaciones por correo. Su responsabilidad principal es tomar un outboxEntry, construir el mensaje con buildNotificationEmail, resolver el destinatario real desde User, enviarlo mediante sendNotificationEmail y actualizar el estado persistente de la entrada. El flujo está diseñado para registrar fallos sin romper la operación de dominio que originó la notificación.
El módulo forma parte del subsistema de notificaciones y trabaja junto con notification.templates.js, notification.client.js y notification.repository.js. En el repositorio, la persistencia del outbox se mantiene separada de la entrega; ese comportamiento ya aparece reflejado en el repositorio y en el emisor de eventos de notificación .
[!NOTE] El envío solo acepta el canal
primary_emaily requiererecipientUserId. Si esa condición no se cumple, la entrega falla conUNSUPPORTED_RECIPIENT_CHANNEL.
Cómo funciona
flowchart TD
A[deliverOutboxEntry] --> B[buildNotificationEmail]
B -->|sin mensaje| C[Return skipped]
B -->|mensaje| D[markOutboxEntrySending]
D --> E[resolveRecipient]
E --> F[User findById]
F --> G[sendNotificationEmail]
G -->|error| H[markOutboxEntryFailed]
H --> I[safeLog]
G -->|ok| J[markOutboxEntrySent]
J --> K[Return delivered]
H --> L[Return errorCode]
El flujo empieza con la construcción del correo usando eventType y payload. Si la plantilla no devuelve un mensaje, la función sale con skipped: true. Si sí existe el mensaje, marca la entrada como sending, busca el email del usuario, envía el correo con una clave de idempotencia basada en el _id del outbox y, al final, marca la entrada como sent.
Si ocurre cualquier error, el módulo normaliza el código, intenta marcar la entrada como failed y registra el problema con un log estructurado. Si la actualización de estado también falla, el log cambia a nivel warn con el código OUTBOX_STATUS_UPDATE_FAILED.
Componentes clave
{
"title": "deliverOutboxEntry",
"content": "[badge:public] Orquesta la entrega completa de una entrada del outbox. Construye el correo, resuelve el destinatario, llama al proveedor y persiste el resultado final.",
"type": "function",
"filePath": "services/api/src/modules/notifications/notification.service.js",
"badges": [
"public"
]
}
{
"title": "resolveRecipient",
"content": "Valida el canal soportado y busca el email del usuario destinatario con `User.findById(...).select('email').lean()`. Devuelve solo la direcci\u00f3n de correo.",
"type": "function",
"filePath": "services/api/src/modules/notifications/notification.service.js",
"badges": []
}
{
"title": "safeErrorCode",
"content": "Normaliza los errores de entrega. Solo acepta c\u00f3digos con el patr\u00f3n `A-Z0-9_` y longitud v\u00e1lida. Si no, devuelve `EMAIL_DELIVERY_FAILED`.",
"type": "function",
"filePath": "services/api/src/modules/notifications/notification.service.js",
"badges": []
}
{
"title": "safeLog",
"content": "Escribe logs estructurados del subsistema de notificaciones. Usa `console.error` para errores y `console.warn` para advertencias.",
"type": "function",
"filePath": "services/api/src/modules/notifications/notification.service.js",
"badges": []
}
Datos y campos
[
{
"name": "outboxEntry",
"type": "object",
"required": true,
"description": "Entrada persistida del outbox que se intenta entregar.",
"children": [
{
"name": "_id",
"type": "ObjectId|string",
"required": true,
"description": "Identificador de la entrada. Se usa como `outboxId` y como `idempotencyKey` del proveedor."
},
{
"name": "eventType",
"type": "string",
"required": true,
"description": "Tipo de evento de notificación. Se pasa a `buildNotificationEmail`."
},
{
"name": "recipientChannel",
"type": "string",
"required": true,
"description": "Canal del destinatario. Este servicio solo acepta `primary_email`."
},
{
"name": "recipientUserId",
"type": "string|null",
"description": "Identificador del usuario destinatario. Se usa para resolver el email."
},
{
"name": "payload",
"type": "object",
"description": "Datos del evento que alimentan la plantilla del correo."
}
]
}
]
[
{
"name": "deliverOutboxEntry result",
"type": "object",
"required": true,
"description": "Resultado devuelto por `deliverOutboxEntry`.",
"children": [
{
"name": "delivered",
"type": "boolean",
"required": true,
"description": "Indica si el envío terminó correctamente."
},
{
"name": "skipped",
"type": "boolean",
"description": "Aparece cuando `buildNotificationEmail` no devuelve un mensaje."
},
{
"name": "errorCode",
"type": "string",
"description": "Código de error normalizado cuando la entrega falla."
},
{
"name": "providerMessageId",
"type": "string|null",
"description": "Identificador devuelto por el proveedor de email cuando el envío fue aceptado."
}
]
}
]
Comportamientos
safeErrorCode(error)— Devuelve un código seguro para persistencia y logging, oEMAIL_DELIVERY_FAILED.safeLog(level, code, outboxEntry)— Registra un evento de notificación concode,eventTypeyoutboxId.resolveRecipient(outboxEntry)— Valida el canal, busca el usuario y devuelveuser.email.deliverOutboxEntry(outboxEntry)— Ejecuta todo el ciclo de entrega y devuelve el resultado final.
[!IMPORTANT]
deliverOutboxEntryusa el_iddel outbox comoidempotencyKeyal enviar el correo. Ese valor sale del propio registro persistido.
Riesgos y casos límite
[!WARNING] Si
recipientChannelno esprimary_email, la función lanza un error con códigoUNSUPPORTED_RECIPIENT_CHANNEL.
[!WARNING] Si
recipientUserIdexiste pero el usuario no tiene email, la función lanzaNOTIFICATION_RECIPIENT_NOT_FOUND.
[!CAUTION] Si
buildNotificationEmaildevuelve un valor falsy, la entrega no continúa. La función retorna{ delivered: false, skipped: true }.
[!NOTE] Si fallan las actualizaciones de estado del outbox, el error no interrumpe el retorno principal. El módulo solo deja un log con
OUTBOX_STATUS_UPDATE_FAILED.
[!IMPORTANT] Los códigos de error persistidos pasan por
safeErrorCode. Eso evita que códigos arbitrarios o malformados lleguen al outbox.
Relacionado
services/api/src/modules/notifications/notification.client.jsenvía el correo real medianteResendy valida la configuración del proveedor. También expone overrides de prueba .services/api/src/modules/notifications/notification.templates.jsconstruye el correo a partir deeventTypeypayload, y resuelve la URL del logo de email .services/api/src/modules/notifications/notification.repository.jspersiste los cambios de estado del outbox:sending,sentyfailed.services/api/src/modules/notifications/notification.events.jsregistra eventos de dominio en el outbox y luego intenta la entrega sincrónica .services/api/src/models/User.jsaporta la búsqueda del destinatario porrecipientUserId, ya que este servicio lee el email desde ese modelo.