sendListingPerformanceDigests.js

Overview

services/api/src/scripts/sendListingPerformanceDigests.js es un script de ejecución directa para generar y emitir digests de rendimiento de listings. Forma parte de “Tu Plano Digest” y procesa usuarios activos y verificados que no han desactivado esta notificación.

El script puede ejecutarse en modo dry-run o en modo apply. En ambos casos calcula el periodo, consulta usuarios elegibles, aplica idempotencia con NotificationOutbox y prepara el payload del evento LISTING_PERFORMANCE_DIGEST.

[!IMPORTANT] La idempotencia depende de payload.periodKey, no de periodStart ni periodEnd. Dos corridas del mismo ciclo usan la misma clave canónica.

How it works

sequenceDiagram
    participant CLI as CLI
    participant Script as sendListingPerformanceDigests
    participant Dotenv as dotenv
    participant Mongo as mongoose
    participant UserModel as User
    participant Outbox as NotificationOutbox
    participant DigestSvc as listing performance digest service
    participant RecSvc as digest recommendations
    participant AuthSvc as auth service
    participant EventSvc as notification events

    CLI->>Script: start with flags
    Script->>Dotenv: load env files
    Script->>Mongo: connect MONGODB_URI
    Script->>UserModel: find eligible users in batches
    Script->>Outbox: check existing digests by periodKey
    Script->>DigestSvc: computeDigestPeriod and buildListingPerformanceDigests
    Script->>RecSvc: buildDigestRecommendations when needed
    Script->>AuthSvc: buildDigestUnsubscribeUrl
    Script->>EventSvc: emitNotificationEvent for each digest
    Script->>Mongo: disconnect

El flujo comienza con la resolución de variables de entorno y la conexión a MongoDB. Después, el script pagina usuarios elegibles con User.find, valida si ya existe un digest del mismo periodKey en NotificationOutbox y evita reprocesar esos casos.

Si un usuario todavía no tiene digest, el script llama a buildListingPerformanceDigests. Cuando el digest existe, arma el payload final con enlaces de la app, nombre visible y URL de baja. Luego, en apply, emite el evento de notificación; en dry-run, solo contabiliza el resultado.

Key components

La pieza principal es la función exportada runSendListingPerformanceDigests. El bloque de entrada isMainModule convierte el archivo en un comando ejecutable desde Node.js.

{
    "title": "runSendListingPerformanceDigests",
    "content": "Funci\u00f3n p\u00fablica que procesa usuarios elegibles, construye digests de rendimiento de listings y emite eventos de notificaci\u00f3n o simula la corrida en dry-run.",
    "type": "function",
    "filePath": "services/api/src/scripts/sendListingPerformanceDigests.js",
    "badges": [
        "public"
    ]
}

Data & fields

La función trabaja con opciones de ejecución, filtros de usuario, periodos de digest y un resumen final. También construye un payload persistible para el evento de notificación.

[
    {
        "name": "dryRun",
        "type": "boolean",
        "description": "Si es true, el script calcula los digests sin emitir eventos ni persistirlos. El valor por defecto es true."
    },
    {
        "name": "batchSize",
        "type": "number",
        "description": "Cantidad de usuarios por lote. Si el valor no es válido o no es positivo, se usa 50."
    },
    {
        "name": "periodEnd",
        "type": "Date",
        "description": "Instante de cierre del periodo. Si no se envía, se usa la fecha actual."
    }
]
[
    {
        "name": "userQuery",
        "type": "object",
        "required": true,
        "description": "Filtro usado para seleccionar usuarios elegibles.",
        "children": [
            {
                "name": "accountStatus",
                "type": "string",
                "required": true,
                "description": "Debe ser 'active'."
            },
            {
                "name": "emailVerifiedAt",
                "type": "null exclusion",
                "required": true,
                "description": "Debe existir un valor distinto de null."
            },
            {
                "name": "notificationPreferences.listingPerformanceDigest",
                "type": "boolean exclusion",
                "description": "Debe ser distinto de false. Si el campo no existe, el usuario sigue siendo elegible."
            }
        ]
    }
]
[
    {
        "name": "payload",
        "type": "object",
        "required": true,
        "description": "Payload final enviado al evento LISTING_PERFORMANCE_DIGEST y guardado para dedupe.",
        "children": [
            {
                "name": "periodKey",
                "type": "string",
                "required": true,
                "description": "Identidad canónica del periodo de 15 días."
            },
            {
                "name": "displayName",
                "type": "string",
                "required": true,
                "description": "Nombre público del usuario para el saludo del correo."
            },
            {
                "name": "hasPeriodActivity",
                "type": "boolean",
                "required": true,
                "description": "Indica si el periodo tuvo actividad detectable."
            },
            {
                "name": "recommendations",
                "type": "array",
                "required": true,
                "description": "Lista de recomendaciones calculadas cuando no hubo actividad en el periodo."
            },
            {
                "name": "listingsUrl",
                "type": "string",
                "required": true,
                "description": "Enlace canónico para gestionar anuncios en el perfil."
            },
            {
                "name": "activityUrl",
                "type": "string",
                "required": true,
                "description": "Enlace al apartado de cuenta del perfil."
            },
            {
                "name": "leadsUrl",
                "type": "string",
                "required": true,
                "description": "Enlace al apartado de leads del perfil."
            },
            {
                "name": "unsubscribeUrl",
                "type": "string",
                "required": true,
                "description": "URL de baja generada para el usuario."
            }
        ]
    }
]
[
    {
        "name": "summary",
        "type": "object",
        "required": true,
        "description": "Resumen final devuelto por la función.",
        "children": [
            {
                "name": "mode",
                "type": "string",
                "required": true,
                "description": "Indica 'dry-run' o 'apply'."
            },
            {
                "name": "batchSize",
                "type": "number",
                "required": true,
                "description": "Tamaño de lote usado durante la corrida."
            },
            {
                "name": "periodKey",
                "type": "string",
                "required": true,
                "description": "Clave canónica del periodo procesado."
            },
            {
                "name": "periodStart",
                "type": "string",
                "required": true,
                "description": "Fecha ISO de inicio del periodo."
            },
            {
                "name": "periodEnd",
                "type": "string",
                "required": true,
                "description": "Fecha ISO de cierre del periodo."
            },
            {
                "name": "eligibleUsers",
                "type": "number",
                "required": true,
                "description": "Usuarios que pasaron el filtro base."
            },
            {
                "name": "digestsCreated",
                "type": "number",
                "required": true,
                "description": "Digests preparados o contabilizados durante la ejecución."
            },
            {
                "name": "skippedIdempotent",
                "type": "number",
                "required": true,
                "description": "Usuarios omitidos porque ya tenían un digest para el mismo periodKey."
            },
            {
                "name": "usersWithoutListings",
                "type": "number",
                "required": true,
                "description": "Usuarios elegibles sin digest disponible."
            },
            {
                "name": "errors",
                "type": "number",
                "required": true,
                "description": "Total de fallos contados por la corrida."
            },
            {
                "name": "errorUserIds",
                "type": "array",
                "required": true,
                "description": "IDs de usuarios asociados a errores."
            }
        ]
    }
]

Behaviors

  • runSendListingPerformanceDigests — procesa usuarios por lotes y devuelve un resumen con métricas de ejecución.
  • computeDigestPeriod — calcula periodStart, periodEnd resuelto y periodKey.
  • User.find(userQuery) — recupera usuarios activos, verificados y suscritos al digest.
  • NotificationOutbox.find(...) — verifica si ya existe un digest para ese recipientUserId y periodKey.
  • buildListingPerformanceDigests(...) — calcula los digests de usuarios pendientes.
  • buildDigestRecommendations(...) — construye recomendaciones cuando el usuario no tuvo actividad en el periodo.
  • buildDigestUnsubscribeUrl(userId) — genera la URL de baja para el usuario.
  • emitNotificationEvent(...) — emite el evento LISTING_PERFORMANCE_DIGEST en modo apply.

[!NOTE] buildListingPerformanceDigests puede devolver null para usuarios sin listings elegibles. En ese caso, el script solo incrementa usersWithoutListings.

[!WARNING] Si falla buildListingPerformanceDigests, el script marca todos los usuarios pendientes del lote como error y continúa con el siguiente lote.

Workflows

El archivo tiene dos formas de ejecución: como módulo importado y como script autónomo. El flujo es el mismo en ambos casos; cambia solo el punto de entrada.

1. Resolver modo | Detecta `--apply`, `--dry-run`, `--batch-size=<n>` y `--period-end=<ISO>`.
2. Cargar entorno | Ejecuta `dotenv.config` con `../../.env` y luego la configuración por defecto.
3. Conectar a MongoDB | Usa `MONGODB_URI` o el valor local por defecto.
4. Calcular periodo | Llama a `computeDigestPeriod` y obtiene `periodStart`, `periodEnd` resuelto y `periodKey`.
5. Obtener usuarios elegibles | Consulta `User` por lotes con `skip` y `limit`.
6. Aplicar idempotencia | Busca entradas existentes en `NotificationOutbox` con el mismo `periodKey`.
7. Construir digest | Llama a `buildListingPerformanceDigests` para los usuarios pendientes.
8. Emitir o simular | En `apply`, llama a `emitNotificationEvent`; en `dry-run`, solo cuenta resultados.
9. Desconectar | Cierra la conexión con `mongoose.disconnect`.
10. Imprimir resumen | Escribe el resumen JSON en consola.

Entrada como script

Cuando el archivo se ejecuta directamente, usa este bloque de arranque:

node services/api/src/scripts/sendListingPerformanceDigests.js --dry-run

También acepta estas variantes:

node services/api/src/scripts/sendListingPerformanceDigests.js --apply --batch-size=100
node services/api/src/scripts/sendListingPerformanceDigests.js --period-end=2024-01-15T00:00:00.000Z

Gotchas & edge cases

[!WARNING] --dry-run tiene prioridad si aparece junto con --apply. En ese caso, la corrida sigue siendo simulada.

[!WARNING] --period-end debe ser una fecha ISO-8601 válida. Si no lo es, el script lanza un error antes de conectar a MongoDB.

[!IMPORTANT] La paginación con skip es segura aquí porque el script no modifica los campos del filtro base durante la corrida.

[!CAUTION] La idempotencia usa NotificationOutbox como fuente de verdad. Un digest previo se omite aunque su estado sea sent, failed o pending.

Este script se integra con varias piezas del backend de services/api/src:

  • services/api/src/models/User.js para seleccionar usuarios elegibles.
  • services/api/src/models/NotificationOutbox.js para idempotencia por periodKey.
  • services/api/src/config/env.js para construir URLs de la aplicación.
  • services/api/src/modules/digest/listing-performance-digest.service.js para calcular periodos y digests.
  • services/api/src/modules/digest/digest-recommendations.js para recomendaciones sin actividad.
  • services/api/src/modules/auth/auth.service.js para buildDigestUnsubscribeUrl.
  • services/api/src/modules/notifications/notification.events.js para EVENT_TYPES y emitNotificationEvent.

[!NOTE] El script no expone HTTP ni rutas. Su interfaz es de consola y está pensada para tareas programadas o ejecución manual controlada.