services/api/src/modules/notifications/notification.client.js

Overview

Este módulo encapsula el envío de correos transaccionales de notificación mediante Resend. También centraliza la configuración obligatoria del proveedor de email y ofrece una vía controlada para sustituirlo en pruebas. El resto del sistema lo usa como cliente de salida, no como una API pública.

La configuración que exige este archivo vive en env y depende de RESEND_API_KEY, RESEND_FROM y RESEND_REPLY_TO. Si falta cualquiera de esos valores, el módulo falla con un error explícito antes de intentar enviar nada.

[!NOTE] Este módulo está pensado para infraestructura de correo transaccional. Su uso principal aparece dentro de notification.service.js, que construye y entrega notificaciones del outbox.

How it works

El flujo del archivo es pequeño, pero muy concreto: primero resuelve el proveedor activo, luego envía el mensaje, y finalmente normaliza el resultado o el error. En pruebas, el proveedor real puede ser reemplazado temporalmente por un stub.

flowchart TD
  Caller[Caller]
  SendNotificationEmail[sendNotificationEmail]
  GetProvider[getProvider]
  EnvCheck[env resendApiKey resendFrom resendReplyTo]
  ProviderOverride[providerOverride]
  ResendClient[Resend client]
  ProviderSend[emails send]
  ConfigError[EMAIL_PROVIDER_NOT_CONFIGURED]
  RejectedError[EMAIL_PROVIDER_REJECTED]

  Caller --> SendNotificationEmail
  SendNotificationEmail --> GetProvider
  GetProvider --> EnvCheck
  EnvCheck -->|missing config| ConfigError
  GetProvider -->|override set| ProviderOverride
  GetProvider -->|default| ResendClient
  SendNotificationEmail --> ProviderSend
  ProviderSend -->|provider error| RejectedError
  ProviderSend -->|success| Caller

getProvider() valida la configuración obligatoria y devuelve el proveedor activo. Si existe providerOverride, lo usa; si no, crea y reutiliza una instancia de Resend. sendNotificationEmail() añade from y replyTo al mensaje, llama a provider.emails.send(), y convierte el fallo del proveedor en un error estable.

Key components

Estos son los elementos exportados o directamente responsables del comportamiento del módulo. El archivo no define clases ni enums; solo funciones y estado de módulo.

{
    "title": "sendNotificationEmail",
    "content": "Env\u00eda un mensaje de correo usando el proveedor activo. A\u00f1ade from y replyTo desde env y devuelve data o null.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.client.js",
    "badges": [
        "public"
    ]
}
{
    "title": "setNotificationEmailProviderForTests",
    "content": "Permite inyectar un proveedor alternativo solo cuando NODE_ENV es test.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.client.js",
    "badges": [
        "public"
    ]
}
{
    "title": "resetNotificationEmailProviderForTests",
    "content": "Elimina el proveedor alternativo durante tests para restaurar el comportamiento normal del m\u00f3dulo.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.client.js",
    "badges": [
        "public"
    ]
}

Data & fields

El archivo mantiene dos variables de estado a nivel de módulo. Además, depende de tres valores de configuración externa para operar correctamente.

  • resendClient [badge:optional] Resend | undefined — Caché del cliente real de Resend. Se crea una sola vez y se reutiliza.
  • providerOverride [badge:optional] unknown | undefined — Proveedor alternativo usado solo en pruebas.
  • env.resendApiKey [badge:required] string — Clave de API necesaria para construir el cliente real.
  • env.resendFrom [badge:required] string — Dirección o identidad remitente que se inserta en cada envío.
  • env.resendReplyTo [badge:required] string — Dirección usada como replyTo en cada correo.
[
    {
        "name": "message",
        "type": "object",
        "required": true,
        "description": "Objeto de mensaje que se pasa a provider.emails.send. El módulo le agrega from y replyTo."
    },
    {
        "name": "options.idempotencyKey",
        "type": "string",
        "description": "Clave opcional enviada al proveedor para idempotencia."
    }
]

Behaviors

Las funciones del archivo tienen responsabilidades separadas y muy predecibles. La validación de configuración sucede antes del envío, y los helpers de test protegen el uso fuera de entorno de pruebas.

  • getProvider() — Resuelve el proveedor activo. Lanza EMAIL_PROVIDER_NOT_CONFIGURED si falta configuración crítica.
  • sendNotificationEmail(message, { idempotencyKey } = {}) — Envía el correo con from y replyTo obligatorios, y lanza EMAIL_PROVIDER_REJECTED si el proveedor responde con error.
  • setNotificationEmailProviderForTests(provider) — Reemplaza el proveedor solo en NODE_ENV=test.
  • resetNotificationEmailProviderForTests() — Limpia el override de pruebas cuando el entorno es test.

[!WARNING] setNotificationEmailProviderForTests() rechaza cualquier uso fuera de NODE_ENV=test. El archivo fuerza esa restricción con un error explícito.

[!IMPORTANT] Si resendApiKey, resendFrom o resendReplyTo faltan, sendNotificationEmail() no intenta enviar nada. El error resultante usa el código EMAIL_PROVIDER_NOT_CONFIGURED.

[!NOTE] Si provider.emails.send() devuelve un objeto con error, el módulo no devuelve ese error crudo. Lo normaliza a EMAIL_PROVIDER_REJECTED.

Workflows

El flujo principal es un envío de correo con validación previa, envío al proveedor y normalización del resultado. Este camino es el que consume notification.service.js cuando entrega entradas del outbox.

sequenceDiagram
  participant Caller
  participant Client as notification.client
  participant Provider as active provider

  Caller->>Client: sendNotificationEmail message
  Client->>Client: getProvider
  alt missing env config
    Client-->>Caller: throw EMAIL_PROVIDER_NOT_CONFIGURED
  else test override or Resend client
    Client->>Provider: emails send
    alt provider returns error
      Client-->>Caller: throw EMAIL_PROVIDER_REJECTED
    else success
      Client-->>Caller: data or null
    end
  end
  1. sendNotificationEmail() recibe el mensaje y, opcionalmente, idempotencyKey.
  2. getProvider() valida la configuración y resuelve el proveedor activo.
  3. El módulo llama a provider.emails.send() con from y replyTo añadidos.
  4. Si el proveedor falla, el archivo lanza EMAIL_PROVIDER_REJECTED.
  5. Si el envío tiene éxito, devuelve result.data o null.

Gotchas & edge cases

Este archivo tiene dos comportamientos de fallo muy importantes. Ambos son explícitos y están diseñados para que el resto del sistema pueda distinguir configuración ausente de rechazo del proveedor.

[!WARNING] La falta de cualquiera de estas variables bloquea el envío: resendApiKey, resendFrom o resendReplyTo. El error usa EMAIL_PROVIDER_NOT_CONFIGURED.

[!CAUTION] El override de proveedor solo existe para tests. Si se intenta usar en producción o en cualquier entorno distinto de test, el módulo lanza un error inmediato.

[!NOTE] El cliente Resend se guarda en memoria del proceso. Eso significa que el archivo reutiliza la misma instancia mientras el proceso siga vivo.

Este archivo se conecta directamente con el subsistema de notificaciones y con la configuración del backend. notification.service.js lo usa para realizar el envío real del correo desde una entrada del outbox.

Los tests del backend usan setNotificationEmailProviderForTests() y resetNotificationEmailProviderForTests() para evitar llamadas reales a Resend y simular respuestas controladas. Eso aparece en pruebas como services/api/test/password-reset.test.js y services/api/test/password-create.test.js.

La configuración de entorno que alimenta este módulo vive en services/api/src/config/env.js, donde se declaran resendApiKey, resendFrom y resendReplyTo.