services/api/src/scripts/anonymizeExpiredAccounts.js

Overview

Este script implementa la fase de día 30 del cierre diferido de cuenta. Toma usuarios con accountStatus: 'pending_deletion' y anonymizationScheduledAt vencida, anonimiza su identidad pública, prepara la purga definitiva del día 60 y emite una notificación de cuenta anonimizada. También limpia documentos fuente del anunciante en listings anonimizados, porque esos archivos pueden contener datos personales y su borrado se trata como irreversible en esta fase.

El archivo funciona tanto como módulo importable como script ejecutable desde línea de comandos. Cuando se ejecuta directamente, carga variables de entorno, conecta a MongoDB, corre la rutina y imprime un resumen JSON del proceso.

[!IMPORTANT] Este script no sustituye email, phone ni username. Según el propio código, esos campos se reemplazan en purgeExpiredAccounts.js durante el día 60.

How it works

flowchart TD
  Start[Script entrypoint] --> Env[Load dotenv]
  Env --> Mongo[Connect to MongoDB]
  Mongo --> Runner[runAnonymizeExpiredAccounts]
  Runner --> Query[Find due pending_deletion users]
  Query -->|dry run| Count[Count matches only]
  Query -->|apply| Batch[Process user batch]
  Batch --> UserFlow[anonymizeSingleUser]
  UserFlow --> Listings[Update protection listings]
  Listings --> Docs[Delete source document assets]
  Docs --> UserUpdate[Update user status and retention contact]
  UserUpdate --> Notify[Emit ACCOUNT_ANONYMIZED]
  Notify --> Summary[Return summary JSON]
  Count --> Summary
  Summary --> End[Print result and disconnect]

El flujo central empieza con una consulta paginada sobre User. En dry-run, el script solo cuenta coincidencias y avanza con skip. En modo apply, cada usuario procesado deja de coincidir con el filtro, y solo los que fallan se excluyen en esa misma corrida para evitar bucles interminables.

La transición real ocurre dentro de anonymizeSingleUser. Primero cifra el email actual, luego mueve los listings del usuario de protection a anonymized, limpia documentos fuente ya anonimizados, actualiza el usuario a anonymized_pending_purge y finalmente emite el evento ACCOUNT_ANONYMIZED.

[!NOTE] La limpieza de documentos fuente tiene un comportamiento tolerante a fallos. Si el borrado en ImageKit falla, el script conserva la entrada para reintentarla después y no bloquea la anonimización.

Key components

{
    "title": "anonymizeSingleUser",
    "content": "Aplica la transici\u00f3n de d\u00eda 30 para un usuario. Actualiza listings, cifra el contacto retenido, limpia tokens de restauraci\u00f3n y emite ACCOUNT_ANONYMIZED.",
    "type": "function",
    "filePath": "services/api/src/scripts/anonymizeExpiredAccounts.js",
    "badges": [
        "public"
    ]
}
{
    "title": "runAnonymizeExpiredAccounts",
    "content": "Recorre usuarios vencidos en modo dry-run o apply, procesa lotes y devuelve un resumen con conteos y errores.",
    "type": "function",
    "filePath": "services/api/src/scripts/anonymizeExpiredAccounts.js",
    "badges": [
        "public"
    ]
}

El helper interno deleteSourceDocumentsForAnonymizedListings(userId) recorre listings del usuario ya marcados como anonymized, intenta borrar sus assets de documentos fuente y vacía sourceDocuments solo después de un borrado exitoso. Si falla, registra solo código e identificador del listing.

Data & fields

[
    {
        "name": "user",
        "type": "object",
        "required": true,
        "description": "Documento de usuario que ya fue cargado desde MongoDB. El script usa su _id, email y purgeScheduledAt."
    },
    {
        "name": "now",
        "type": "Date",
        "required": true,
        "description": "Marca temporal de referencia para fijar la transición y construir el enlace de exportación."
    }
]
[
    {
        "name": "dryRun",
        "type": "boolean",
        "description": "Si es true, el script solo contabiliza coincidencias y no persiste cambios."
    },
    {
        "name": "batchSize",
        "type": "number",
        "description": "Cantidad máxima de usuarios por lote. El valor por defecto es 50."
    },
    {
        "name": "now",
        "type": "Date",
        "description": "Instante de evaluación de vencimiento. Por defecto usa new Date()."
    }
]
[
    {
        "name": "mode",
        "type": "'dry-run' | 'apply'",
        "required": true,
        "description": "Modo efectivo de ejecución."
    },
    {
        "name": "batchSize",
        "type": "number",
        "required": true,
        "description": "Tamaño de lote usado durante la corrida."
    },
    {
        "name": "matched",
        "type": "number",
        "required": true,
        "description": "Cantidad total de usuarios que coincidieron con el filtro."
    },
    {
        "name": "anonymized",
        "type": "number",
        "required": true,
        "description": "Cantidad de usuarios anonimizados con éxito."
    },
    {
        "name": "errors",
        "type": "number",
        "required": true,
        "description": "Cantidad de usuarios que produjeron un error."
    },
    {
        "name": "errorUserIds",
        "type": "string[]",
        "required": true,
        "description": "Lista de identificadores de usuarios con error."
    }
]

Campos persistidos por este script

  • Listing.lifecycle.status — pasa de protection a anonymized para listings del usuario.
  • Listing.lifecycle.anonymizedAt — registra la marca temporal de la anonimización.
  • Listing.lifecycle.publicOwnerLabel — se fija en Anunciante de Plano.
  • Listing.sourceDocuments — se vacía cuando el borrado de assets se completa con éxito.
  • User.accountStatus — cambia de pending_deletion a anonymized_pending_purge.
  • User.anonymizedAt — se establece con now.
  • User.retentionContact — guarda encryptField(user.email) para uso futuro.
  • User.restorationToken y User.restorationTokenExpiresAt — se eliminan con $unset.

Behaviors

  • deleteSourceDocumentsForAnonymizedListings(userId) — busca listings anonimizados del usuario, borra assets de documentos fuente y limpia sourceDocuments solo después del éxito.
  • anonymizeSingleUser(user, now) — ejecuta la transición de dominio completa para un usuario vencido y dispara la notificación asociada.
  • runAnonymizeExpiredAccounts(options) — pagina usuarios vencidos, ejecuta anonimización en modo apply o solo cuenta en dry-run, y devuelve un resumen estructurado.
  • isMainModule bootstrap — detecta ejecución directa del archivo, carga .env, lee MONGODB_URI, interpreta --apply, --dry-run y --batch-size=, conecta a MongoDB y desconecta al final.

Workflows

sequenceDiagram
  participant CLI as Script entrypoint
  participant Env as dotenv
  participant Mongo as mongoose
  participant Runner as runAnonymizeExpiredAccounts
  participant UserModel as User
  participant ListingModel as Listing
  participant MediaService as deleteListingSourceDocumentAssets
  participant Notify as emitNotificationEvent

  CLI->>Env: load env files
  CLI->>Mongo: connect
  CLI->>Runner: execute with mode and batch size
  Runner->>UserModel: find due pending_deletion users
  alt dry run
    Runner->>Runner: count matches only
  else apply
    Runner->>ListingModel: update protection listings
    Runner->>MediaService: delete source document assets
    Runner->>UserModel: update user status
    Runner->>Notify: emit ACCOUNT_ANONYMIZED
  end
  Runner-->>CLI: summary
  CLI->>Mongo: disconnect

La secuencia refleja el patrón del script: primero se prepara el entorno, después se procesa la colección User, y por último se deja un resumen legible por consola. El envío de eventos ocurre después de persistir el cambio de estado de dominio.

Proceso operativo

  1. Preparar entorno | El script carga ../../.env y luego dotenv.config() estándar.
  2. Elegir modo | --apply activa la ejecución real; en caso contrario, corre en dry-run.
  3. Determinar lote | --batch-size=... fija el tamaño, con 50 como valor por defecto.
  4. Conectar MongoDB | Usa MONGODB_URI o mongodb://127.0.0.1:27017/plano_backoffice.
  5. Procesar usuarios | Ejecuta runAnonymizeExpiredAccounts y acumula métricas.
  6. Cerrar sesión | Imprime el resumen JSON y desconecta de MongoDB.

Gotchas & edge cases

[!WARNING] En modo apply, una falla por usuario no detiene toda la corrida. El script registra code y userId, agrega el id a failedIds y continúa con el siguiente lote.

[!WARNING] Los fallos al borrar documentos fuente no bloquean la anonimización. El código conserva el registro de sourceDocuments para reintento posterior y solo vacía el campo cuando el borrado termina bien.

[!NOTE] El script solo procesa usuarios con accountStatus: 'pending_deletion' y anonymizationScheduledAt <= now. Los demás usuarios no entran en el lote.

[!CAUTION] El email retenido se cifra con encryptField(user.email) y se guarda en retentionContact, pero el propio archivo indica que todavía no existe un consumidor para ese dato.

Este script se conecta con varias piezas ya existentes del backend. Usa ../models/User.js y ../models/Listing.js para leer y actualizar estado de cuenta y lifecycle del anuncio. También depende de ../utils/encryption.js, ../modules/notifications/notification.events.js, ../modules/auth/account-export.service.js y ../modules/media/media.service.js para cifrado, eventos, enlace de exportación y borrado de assets.

Su comportamiento forma parte del ciclo diferido que el código comenta junto con purgeExpiredAccounts.js, donde el día 60 se completa la purga y se resuelven campos como email, phone y username.

[!NOTE] En esta unidad no hay rutas HTTP ni controladores. El archivo es un script de mantenimiento y orquestación interna.