services/api/src/scripts/auditOrphanListings.js

Overview

Este script audita Listing huérfanos heredados de seed.js y separa tres casos: valid, seed_orphan y ambiguous_orphan. La lógica usa evidencias concretas de origen histórico, como admin_users, títulos exactos de fixtures y huellas de media de picsum.photos/seed/ .

También incluye rutas de limpieza controlada. Por defecto corre en modo lectura, y sus borrados se limitan a listas explícitas de _id ya clasificadas en la misma ejecución. El archivo deja claro que no forma parte del Account Closure Lifecycle y no toca User.accountStatus ni Listing.lifecycle.status .

[!IMPORTANT] Este script es una herramienta manual de integridad y migración. No es un job recurrente, ni usa cron, ni se ejecuta al arrancar la API .

How it works

flowchart TD
  Start[Direct run]
  Env[Load env and connect Mongo]
  Audit[runAuditOrphanListings]
  Classify[classifyOrphanListing]
  Summary[printSummary]
  SeedPlan[buildApprovedSeedOrphanCleanupPlan]
  OwnerlessPlan[buildOwnerlessAdministrativeCleanupPlan]
  SeedApply[applySeedOrphanCleanup]
  RelationApply[applyApprovedSeedOrphanCleanup]
  OwnerlessApply[applyOwnerlessAdministrativeCleanup]
  Media[deleteListingMediaAssets]

  Start --> Env
  Env --> Audit
  Audit --> Classify
  Audit --> Summary
  Audit --> SeedPlan
  Audit --> OwnerlessPlan
  SeedPlan --> SeedApply
  SeedPlan --> RelationApply
  SeedApply --> Media
  RelationApply --> Media
  OwnerlessPlan --> OwnerlessApply
  OwnerlessApply --> Media

El flujo directo carga variables de entorno, conecta mongoose, ejecuta la auditoría y calcula planes de limpieza. Después imprime el resumen y aplica cambios solo si el operador activa --apply o --apply-ownerless .

La auditoría lee Listing, User, ListingLike, ListingComment, ListingEvent, Lead y Campaign, y además consulta la colección legacy admin_users. La clasificación conserva una postura conservadora: si un createdBy resuelve a un User real, el Listing se considera válido y se excluye del detalle .

Key components

{
    "title": "classifyOrphanListing",
    "content": "Clasifica un Listing ya hu\u00e9rfano como seed_orphan o ambiguous_orphan. Usa admin_users, t\u00edtulos conocidos y media con la huella picsum.photos/seed/.",
    "type": "function",
    "filePath": "services/api/src/scripts/auditOrphanListings.js",
    "badges": [
        "public"
    ]
}
{
    "title": "runAuditOrphanListings",
    "content": "Ejecuta la auditor\u00eda completa. Carga Listings y Users, clasifica hu\u00e9rfanos, y opcionalmente calcula relaciones.",
    "type": "function",
    "filePath": "services/api/src/scripts/auditOrphanListings.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildApprovedSeedOrphanCleanupPlan",
    "content": "Construye el plan relation-aware para seed_orphan. Separa candidatos aprobados y excluidos, y aplica guardas sobre usuarios vigentes y ImageKit compartido.",
    "type": "function",
    "filePath": "services/api/src/scripts/auditOrphanListings.js",
    "badges": [
        "public"
    ]
}
{
    "title": "buildOwnerlessAdministrativeCleanupPlan",
    "content": "Construye una limpieza administrativa separada para ambiguous_orphan sin ownership recuperable. Devuelve candidatos seguros y casos para revisi\u00f3n manual.",
    "type": "function",
    "filePath": "services/api/src/scripts/auditOrphanListings.js",
    "badges": [
        "public"
    ]
}

Data & fields

Constants

  • KNOWN_SEED_LISTING_TITLES [badge:read-only] Set<string> — Títulos exactos reconocidos como fixtures históricos de seed.js.
  • SEED_MEDIA_PATTERN [badge:read-only] RegExp — Patrón picsum.photos/seed/ usado como huella de media de seed.
  • PREVIOUSLY_OBSERVED_SAFE_COUNT [badge:read-only] number — Conteo histórico informativo para comparar corridas, sin efecto de control.

Resultado de auditoría

[
    {
        "name": "totals",
        "type": "object",
        "required": true,
        "description": "Conteos globales de la corrida.",
        "children": [
            {
                "name": "total",
                "type": "number",
                "required": true,
                "description": "Total de Listings cargados desde Mongo."
            },
            {
                "name": "valid",
                "type": "number",
                "required": true,
                "description": "Listings cuyo createdBy resuelve a un User real."
            },
            {
                "name": "orphan",
                "type": "number",
                "required": true,
                "description": "Listings sin User válido."
            },
            {
                "name": "seed_orphan",
                "type": "number",
                "required": true,
                "description": "Huérfanos con evidencia directa de seed.js."
            },
            {
                "name": "ambiguous_orphan",
                "type": "number",
                "required": true,
                "description": "Huérfanos sin evidencia suficiente de seed.js."
            }
        ]
    },
    {
        "name": "rows",
        "type": "array",
        "required": true,
        "description": "Detalle de cada Listing huérfano clasificado.",
        "children": [
            {
                "name": "_id",
                "type": "string",
                "required": true,
                "description": "Identificador del Listing."
            },
            {
                "name": "title",
                "type": "string",
                "required": true,
                "description": "Título del Listing."
            },
            {
                "name": "intent",
                "type": "string",
                "required": true,
                "description": "requirement si el intent original lo indica; offer en otros casos."
            },
            {
                "name": "publicationStatus",
                "type": "string",
                "required": true,
                "description": "publication.status o draft si falta."
            },
            {
                "name": "lifecycleStatus",
                "type": "string",
                "required": true,
                "description": "lifecycle.status o missing si no existe."
            },
            {
                "name": "createdBy",
                "type": "string|null",
                "required": true,
                "description": "Valor crudo de ownership almacenado en el Listing."
            },
            {
                "name": "ownerResolved",
                "type": "boolean",
                "required": true,
                "description": "Siempre false en las filas huérfanas reportadas."
            },
            {
                "name": "ownerSnapshot",
                "type": "string",
                "required": true,
                "description": "contact.name capturado en la auditoría."
            },
            {
                "name": "createdAt",
                "type": "date|null",
                "required": true,
                "description": "Fecha de creación del documento."
            },
            {
                "name": "classification",
                "type": "string",
                "required": true,
                "description": "seed_orphan o ambiguous_orphan."
            },
            {
                "name": "reasons",
                "type": "array",
                "required": true,
                "description": "Evidencias que justifican seed_orphan."
            },
            {
                "name": "notes",
                "type": "array",
                "required": true,
                "description": "Notas informativas para ambiguous_orphan."
            },
            {
                "name": "relations",
                "type": "object",
                "description": "Conteos de dependencias cuando includeRelations está activo.",
                "children": [
                    {
                        "name": "likes",
                        "type": "number",
                        "required": true,
                        "description": "Cantidad de ListingLike."
                    },
                    {
                        "name": "comments",
                        "type": "number",
                        "required": true,
                        "description": "Cantidad de ListingComment."
                    },
                    {
                        "name": "events",
                        "type": "number",
                        "required": true,
                        "description": "Cantidad de ListingEvent."
                    },
                    {
                        "name": "leads",
                        "type": "number",
                        "required": true,
                        "description": "Cantidad de Lead."
                    },
                    {
                        "name": "referrals",
                        "type": "number",
                        "required": true,
                        "description": "Listings que apuntan al relatedListingId."
                    },
                    {
                        "name": "campaigns",
                        "type": "number",
                        "required": true,
                        "description": "Campaigns que apuntan al Listing."
                    }
                ]
            }
        ]
    }
]

Plan de limpieza seed_orphan

[
    {
        "name": "_id",
        "type": "string",
        "required": true,
        "description": "Identificador del Listing aprobado para limpieza."
    },
    {
        "name": "title",
        "type": "string",
        "required": true,
        "description": "Título del Listing."
    },
    {
        "name": "likesToDelete",
        "type": "array",
        "required": true,
        "description": "IDs exactos de ListingLike a borrar."
    },
    {
        "name": "commentsToDelete",
        "type": "array",
        "required": true,
        "description": "IDs exactos de ListingComment a borrar."
    },
    {
        "name": "eventsToDelete",
        "type": "array",
        "required": true,
        "description": "IDs exactos de ListingEvent a borrar."
    },
    {
        "name": "leadsToDelete",
        "type": "array",
        "required": true,
        "description": "IDs exactos de Lead a borrar."
    },
    {
        "name": "mediaCleanupRequired",
        "type": "boolean",
        "required": true,
        "description": "Indica si la media contiene assets reales de ImageKit."
    },
    {
        "name": "explicitlyApproved",
        "type": "boolean",
        "required": true,
        "description": "Marca holdouts aprobados manualmente en la misma corrida."
    },
    {
        "name": "reason",
        "type": "string",
        "required": true,
        "description": "Justificación operativa del lugar en el plan."
    }
]

Plan administrativo ambiguous_orphan

[
    {
        "name": "_id",
        "type": "string",
        "required": true,
        "description": "Identificador del Listing."
    },
    {
        "name": "title",
        "type": "string",
        "required": true,
        "description": "Título del Listing."
    },
    {
        "name": "mediaCleanupRequired",
        "type": "boolean",
        "required": true,
        "description": "Indica si requiere limpieza de media al borrar."
    },
    {
        "name": "decision",
        "type": "string",
        "required": true,
        "description": "safe_to_delete_ownerless cuando pasa todas las guardas."
    },
    {
        "name": "reason",
        "type": "string",
        "required": true,
        "description": "Motivo de la decisión administrativa."
    }
]

Behaviors

  • normalizeTitle(title) — Convierte el título a cadena y recorta espacios.
  • hasSeedMediaFingerprint(media) — Detecta media con picsum.photos/seed/.
  • classifyOrphanListing(listing, { adminUserEmailById }) — Devuelve seed_orphan o ambiguous_orphan, más razones o notas.
  • loadAdminUserEmailById() — Lee la colección admin_users y construye un Map por _id.
  • countRelations(listingId) — Cuenta likes, comments, events, leads, referrals y campaigns.
  • runAuditOrphanListings({ includeRelations = true }) — Carga Listings y Users, clasifica orphans y, opcionalmente, adjunta relaciones.
  • hasNoRelations(row) — Verifica que todas las relaciones cuenten cero.
  • computeSafeSeedOrphanIds(rows) — Extrae solo _id de seed_orphan con cero relaciones.
  • printSummary(result) — Imprime totales, candidatos seguros y filas clasificadas.
  • applySeedOrphanCleanup(safeIds) — Limpia media, borra Listings por _id exacto y reporta fallos de media.
  • loadRelationRecords(listingId) — Carga documentos relacionados por Listing.
  • collectRelationIdentityIds(records) — Reúne identidades referenciadas por likes, comments, events y leads.
  • hasImageKitMedia(media) — Detecta media real de ImageKit por provider.
  • findSharedImageKitFileIds(listingId, media) — Busca fileIds de ImageKit compartidos con otros Listings.
  • buildApprovedSeedOrphanCleanupPlan(rows, options) — Construye el plan relation-aware y excluye casos con usuarios vigentes, emails vigentes o ImageKit compartido.
  • applyApprovedSeedOrphanCleanup(plan) — Revalida estado, borra relaciones aprobadas, limpia media y elimina el Listing.
  • buildOwnerlessAdministrativeCleanupPlan(rows) — Aísla ambiguous_orphan con ownership nulo y sin relaciones ni media compartida.
  • applyOwnerlessAdministrativeCleanup(plan) — Revalida estado y borra Listings ownerless seguros.
  • printOwnerlessCleanupPlan(result) — Imprime el plan administrativo y sus exclusiones.
  • printCleanupPlan(result) — Imprime el plan relation-aware y sus exclusiones.

Workflows

1. Cargar entorno | El bloque directo ejecuta `dotenv.config` y abre `mongoose` contra `MONGODB_URI` o la base local por defecto.
2. Auditar datos | `runAuditOrphanListings` carga Listings, Users y la colección legacy `admin_users`.
3. Clasificar huérfanos | `classifyOrphanListing` decide entre `seed_orphan` y `ambiguous_orphan`.
4. Construir planes | `buildApprovedSeedOrphanCleanupPlan` y `buildOwnerlessAdministrativeCleanupPlan` separan candidatos y exclusiones.
5. Ejecutar en modo lectura | Sin flags destructivos, el script imprime el resumen y termina sin modificar datos.
6. Aplicar limpieza seed | `--apply` borra solo `seed_orphan` con cero relaciones en esa misma corrida.
7. Aplicar limpieza relation-aware | `--apply` también ejecuta el plan aprobado con guardas adicionales.
8. Aplicar limpieza ownerless | `--apply-ownerless` ejecuta la ruta separada para `ambiguous_orphan`.

Gotchas & edge cases

[!WARNING] classifyOrphanListing no clasifica por palabras como prueba o test solas. Solo añade una nota informativa. El código evita tratar ese título como prueba de origen seed.js

[!IMPORTANT] --apply solo borra seed_orphan con cero relaciones en la misma corrida. ambiguous_orphan queda fuera y requiere --apply-ownerless

[!CAUTION] Antes de cada borrado, los flujos de apply revalidan el estado actual. Si el Listing ya no es huérfano, el script lo omite

[!WARNING] Si una media de ImageKit está compartida con otro Listing, el candidato se excluye. Ni la aprobación explícita lo habilita para borrado

[!NOTE] PREVIOUSLY_OBSERVED_SAFE_COUNT solo informa un cambio entre corridas. No bloquea la ejecución y no decide borrados

[!TIP] includeRelations puede desactivarse en runAuditOrphanListings para una pasada rápida. En ese caso, rows no incluye relations

Este script se relaciona directamente con los modelos Listing, User, ListingLike, ListingComment, ListingEvent, Lead y Campaign. También depende de deleteListingMediaAssets en services/api/src/modules/media/media.service.js para limpiar media asociada antes del borrado físico .

Su comportamiento se apoya en la colección legacy admin_users, usada solo como fuente de evidencia histórica. La intención general es cerrar deuda documental y de integridad, no redefinir el ciclo de vida operativo de cuentas o Listings .