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 deperiodStartniperiodEnd. 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— calculaperiodStart,periodEndresuelto yperiodKey.User.find(userQuery)— recupera usuarios activos, verificados y suscritos al digest.NotificationOutbox.find(...)— verifica si ya existe un digest para eserecipientUserIdyperiodKey.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 eventoLISTING_PERFORMANCE_DIGESTen modoapply.
[!NOTE]
buildListingPerformanceDigestspuede devolvernullpara usuarios sin listings elegibles. En ese caso, el script solo incrementausersWithoutListings.
[!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-runtiene prioridad si aparece junto con--apply. En ese caso, la corrida sigue siendo simulada.
[!WARNING]
--period-enddebe 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
skipes segura aquí porque el script no modifica los campos del filtro base durante la corrida.
[!CAUTION] La idempotencia usa
NotificationOutboxcomo fuente de verdad. Un digest previo se omite aunque su estado seasent,failedopending.
Related
Este script se integra con varias piezas del backend de services/api/src:
services/api/src/models/User.jspara seleccionar usuarios elegibles.services/api/src/models/NotificationOutbox.jspara idempotencia porperiodKey.services/api/src/config/env.jspara construir URLs de la aplicación.services/api/src/modules/digest/listing-performance-digest.service.jspara calcular periodos y digests.services/api/src/modules/digest/digest-recommendations.jspara recomendaciones sin actividad.services/api/src/modules/auth/auth.service.jsparabuildDigestUnsubscribeUrl.services/api/src/modules/notifications/notification.events.jsparaEVENT_TYPESyemitNotificationEvent.
[!NOTE] El script no expone HTTP ni rutas. Su interfaz es de consola y está pensada para tareas programadas o ejecución manual controlada.