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

Overview

Este módulo centraliza la construcción de correos transaccionales de notificaciones. Recibe un eventType y un payload, y devuelve un objeto con subject, html y text. notification.service.js lo consume antes de resolver al destinatario y enviar el mensaje al proveedor de correo.

La plantilla compartida usa el branding de Somos Plano, un encabezado compacto y un footer de seguridad. También separa el asset de marca de las URLs de acción, para no mezclar env.emailAssetsUrl con env.appUrl.

[!NOTE] Este archivo no envía correos. Solo construye contenido y valida datos mínimos por evento.

How it works

flowchart TD
    Entry[buildNotificationEmail] -->|ACCOUNT_DELETION_REQUESTED| Deletion[deletionRequestedTemplate]
    Entry -->|ACCOUNT_RESTORATION_LINK_RESENT| Resend[restorationLinkResentTemplate]
    Entry -->|ACCOUNT_RESTORED| Restored[accountRestoredTemplate]
    Entry -->|ACCOUNT_ANONYMIZED| Anonymized[accountAnonymizedTemplate]
    Entry -->|ACCOUNT_PURGE_WARNING| PurgeWarning[accountPurgeWarningTemplate]
    Entry -->|ACCOUNT_PURGED| Purged[accountPurgedTemplate]
    Entry -->|PASSWORD_RESET_REQUESTED| PasswordReset[passwordResetRequestedTemplate]
    Entry -->|PASSWORD_CREATED| PasswordCreated[passwordCreatedTemplate]
    Entry -->|EMAIL_VERIFICATION_REQUESTED| EmailVerification[emailVerificationRequestedTemplate]
    Entry -->|WELCOME| Welcome[welcomeTemplate]
    Entry -->|LISTING_PERFORMANCE_DIGEST| Digest[listingPerformanceDigestTemplate]

    Deletion --> Layout[emailLayout]
    Resend --> Layout
    Restored --> Layout
    Anonymized --> Layout
    PurgeWarning --> Layout
    Purged --> Layout
    PasswordReset --> Layout
    PasswordCreated --> Layout
    EmailVerification --> Layout
    Welcome --> Layout
    Digest --> Layout

    Layout --> Header[buildBrandHeaderHtml]
    Layout --> Logo[resolveEmailLogoUrl]
    Layout --> Escape[escapeHtml]
    Layout --> Date[formatDate]

    Digest --> Metrics[metricsList]
    Digest --> Sentiment[renderSentimentBlock]
    Digest --> ListingBlock[listingBlock]
    Digest --> Greeting[greetingLine]

buildNotificationEmail actúa como fábrica principal. Si el evento tiene plantilla, arma el correo y aplica validaciones previas. Si el evento no existe aquí, devuelve null. El layout común luego normaliza la marca, el footer y las acciones.

La URL del logo se resuelve de forma explícita. Si EMAIL_ASSETS_URL está vacío o malformado, el encabezado omite la imagen. Si la URL contiene un subpath, ese subpath se preserva exactamente.

Key components

{
    "title": "buildNotificationEmail",
    "content": "F\u00e1brica principal del m\u00f3dulo. Selecciona la plantilla adecuada, valida URLs obligatorias y devuelve el contenido listo para env\u00edo o null cuando el eventType no tiene plantilla.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.templates.js",
    "badges": [
        "public"
    ]
}
{
    "title": "emailLayout",
    "content": "Construye el HTML y el texto plano compartidos por todas las plantillas. Inserta el encabezado, el cuerpo, las acciones y el footer de seguridad.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.templates.js",
    "badges": [
        "public"
    ]
}
{
    "title": "resolveEmailLogoUrl",
    "content": "Resuelve la URL absoluta del logo de email a partir de env.emailAssetsUrl. Devuelve cadena vac\u00eda cuando la configuraci\u00f3n no es usable.",
    "type": "function",
    "filePath": "services/api/src/modules/notifications/notification.templates.js",
    "badges": [
        "public"
    ]
}

Data & fields

Salida común del layout

El layout compartido devuelve siempre la misma forma. El HTML y el texto plano salen juntos, para que el correo tenga versión visual y alternativa.

[
    {
        "name": "subject",
        "type": "string",
        "required": true,
        "description": "Asunto final del correo."
    },
    {
        "name": "html",
        "type": "string",
        "required": true,
        "description": "Contenido HTML completo del mensaje."
    },
    {
        "name": "text",
        "type": "string",
        "required": true,
        "description": "Versión en texto plano del mismo mensaje."
    }
]

Contrato compartido de emailLayout

Este contrato define la forma del contenido que cada plantilla inyecta en el layout. Las acciones se filtran por url, así que un botón sin enlace no se renderiza.

[
    {
        "name": "heading",
        "type": "string",
        "required": true,
        "description": "Título principal del correo."
    },
    {
        "name": "bodyHtml",
        "type": "string",
        "required": true,
        "description": "Fragmento HTML que se inserta bajo el título."
    },
    {
        "name": "bodyText",
        "type": "string",
        "required": true,
        "description": "Texto plano equivalente al cuerpo HTML."
    },
    {
        "name": "actions",
        "type": "array<object>",
        "description": "Botones de acción opcionales mostrados al centro.",
        "children": [
            {
                "name": "label",
                "type": "string",
                "required": true,
                "description": "Texto visible del botón y del fallback textual."
            },
            {
                "name": "url",
                "type": "string",
                "required": true,
                "description": "Destino del botón."
            }
        ]
    }
]

Campos usados por buildNotificationEmail

El payload cambia según el evento. El factory valida algunos campos y deja otros como opcionales, porque el propio layout ignora acciones sin URL.

[
    {
        "name": "eventType",
        "type": "string",
        "required": true,
        "description": "Clave del evento de notificación."
    },
    {
        "name": "payload",
        "type": "object",
        "description": "Datos de la plantilla. Los campos obligatorios dependen de eventType.",
        "children": [
            {
                "name": "restorationUrl",
                "type": "string",
                "description": "Obligatoria para ACCOUNT_DELETION_REQUESTED y ACCOUNT_RESTORATION_LINK_RESENT."
            },
            {
                "name": "resetUrl",
                "type": "string",
                "description": "Obligatoria para PASSWORD_RESET_REQUESTED."
            },
            {
                "name": "verificationUrl",
                "type": "string",
                "description": "Obligatoria para EMAIL_VERIFICATION_REQUESTED."
            },
            {
                "name": "accountRole",
                "type": "string",
                "description": "Usada por WELCOME. Las variantes actuales son creator y seeker."
            },
            {
                "name": "displayName",
                "type": "string",
                "description": "Nombre visible usado en LISTING_PERFORMANCE_DIGEST."
            },
            {
                "name": "listingsUrl",
                "type": "string",
                "description": "Obligatoria para LISTING_PERFORMANCE_DIGEST."
            },
            {
                "name": "activityUrl",
                "type": "string",
                "description": "Obligatoria para LISTING_PERFORMANCE_DIGEST."
            },
            {
                "name": "leadsUrl",
                "type": "string",
                "description": "Obligatoria para LISTING_PERFORMANCE_DIGEST."
            },
            {
                "name": "unsubscribeUrl",
                "type": "string",
                "description": "Enlace opcional para dejar de recibir Tu Plano Digest."
            },
            {
                "name": "listings",
                "type": "array<object>",
                "description": "Bloques por anuncio usados por LISTING_PERFORMANCE_DIGEST.",
                "children": [
                    {
                        "name": "title",
                        "type": "string",
                        "description": "Título del anuncio."
                    },
                    {
                        "name": "periodTotals",
                        "type": "object",
                        "description": "Métricas del anuncio en el periodo."
                    }
                ]
            },
            {
                "name": "moreListingsCount",
                "type": "number",
                "description": "Cantidad de anuncios adicionales resumidos en el digest."
            }
        ]
    }
]

Campos internos de apoyo

Estos valores no forman parte del contrato público, pero sí explican el comportamiento del módulo.

  • EMAIL_LOGO_PATH [badge:read-only] string — Ruta relativa fija del isotipo de email.
  • BRAND_LOGO_SIZE [badge:read-only] number — Tamaño cuadrado del logo en el header.
  • EXPORT_ACTION_LABEL [badge:read-only] string — Etiqueta reutilizada para la acción de exportación.
  • WELCOME_ROLE_COPY [badge:read-only] object — Copia específica para creator y seeker.
  • SENTIMENT_LABELS [badge:read-only] object — Traducción visible de positive, neutral, negative y none.

Behaviors

Helpers compartidos

  • resolveEmailLogoUrl — Verifica env.emailAssetsUrl y construye la URL final del logo. Devuelve '' si el valor no sirve.
  • buildBrandHeaderHtml — Renderiza el encabezado de marca con <table role="presentation">. Usa logo e SOMOS PLANO como texto real.
  • escapeHtml — Escapa &, <, >, ", y ' en texto y URLs insertadas en HTML.
  • formatDate — Formatea fechas válidas como es-MX con America/Mexico_City. Devuelve '' para fechas inválidas.
  • emailLayout — Construye el documento HTML completo y su equivalente en texto plano. Añade footer y acciones filtradas.
  • metricsList — Convierte pares etiqueta-valor en una lista HTML y otra textual.
  • greetingLine — Usa displayName recortado. Si está vacío, devuelve solo Hola.
  • formatSentimentPercentages — Calcula porcentajes enteros a partir de positivos, neutrales y negativos.
  • renderSentimentBlock — Muestra un mensaje vacío cuando no hay comentarios. No inventa porcentajes.
  • listingBlock — Agrupa métricas y sentimiento de un anuncio individual.

Plantillas de cuenta y seguridad

  • deletionRequestedTemplate — Notifica la solicitud de eliminación. Incluye restauración y exportación.
  • restorationLinkResentTemplate — Explica que el enlace anterior ya no sirve. Solo conserva el nuevo enlace.
  • accountRestoredTemplate — Confirma que la cuenta volvió a estar activa.
  • accountAnonymizedTemplate — Explica la anonimización pública tras 30 días. Mantiene la exportación.
  • accountPurgeWarningTemplate — Advierte sobre la eliminación definitiva en 7 días.
  • accountPurgedTemplate — Marca el cierre definitivo del proceso de retención.
  • passwordResetRequestedTemplate — Entrega el enlace para restablecer la contraseña.
  • passwordCreatedTemplate — Informa que ya existe una contraseña para la cuenta.
  • emailVerificationRequestedTemplate — Pide verificar el correo dentro de 24 horas.
  • welcomeTemplate — Confirma la verificación y añade copy según accountRole. Usa env.appUrl como destino final.

[!IMPORTANT] buildNotificationEmail exige URLs específicas en varios eventos. Si faltan, lanza INVALID_NOTIFICATION_PAYLOAD.

Digest de actividad

  • listingPerformanceDigestTemplate — Construye el correo Tu Plano Digest · Últimos 15 días.
  • Usa hasPeriodActivity para decidir entre resumen de actividad o recomendaciones.
  • Usa listings, totals y periodTotals para mostrar métricas agregadas.
  • Añade unsubscribeUrl solo cuando existe.
  • Incluye botones hacia listingsUrl, activityUrl y leadsUrl.

Gotchas & edge cases

[!WARNING] Si EMAIL_ASSETS_URL está vacío, el correo sigue construyéndose. Solo desaparece el <img> del logo.

[!WARNING] Si EMAIL_ASSETS_URL está malformado, el módulo registra INVALID_EMAIL_ASSETS_URL y omite la imagen. No falla en silencio.

[!WARNING] La URL del logo conserva subpaths completos. Un join incorrecto perdería la parte final del origen.

[!CAUTION] buildNotificationEmail devuelve null para eventType no soportados. No intenta fabricar un correo genérico.

[!CAUTION] El header no usa flex ni grid. Usa tabla y celdas alineadas al centro. Ese patrón está fijo en la plantilla.

[!NOTE] El correo siempre incluye footer de seguridad. Ese texto vive en emailLayout, no en cada plantilla.

  • services/api/src/modules/notifications/notification.service.js — Consume buildNotificationEmail, resuelve destinatario y llama a sendNotificationEmail.
  • services/api/src/modules/notifications/notification.client.js — Entrega el mensaje al proveedor configurado.
  • services/api/src/modules/notifications/notification.repository.js — Persiste el estado del outbox antes y después del envío.
  • services/api/src/modules/notifications/notification.events.js — Registra eventos de dominio y delega la entrega.
  • services/api/src/config/env.js — Provee env.emailAssetsUrl y env.appUrl, que este módulo usa para branding y enlaces.