pending-requirement-recovery.service.js

Overview

Este módulo implementa la recuperación interna de eliminaciones de Requirements que ya estaban solicitadas. Reanuda el protocolo de borrado sobre documentos con deletion.status = "pending", incluso cuando la cuenta dueña ya no está activa. También reutiliza el flujo de deleteRequirement en modo resumeOnly, con la misma barrera A3, el archivado condicionado de Conversation y el borrado físico condicional de medios. fileciteturn0file8turn0file0

El archivo no expone endpoints HTTP ni usa autenticación HTTP. En su lugar, exige un contexto interno marcado con un Symbol privado del módulo. Ese contexto limita el uso accidental desde otros módulos, pero no actúa como una frontera criptográfica. fileciteturn0file8turn0file0

[!NOTE] La recuperación solo actúa si el Requirement ya está pendiente. Si el documento no cumple las precondiciones, el servicio devuelve un estado de salida sin escribir nada. fileciteturn0file7turn0file8

How it works

El flujo principal empieza con una validación del contexto interno. Después relee el estado actual del Listing, calcula si la recuperación es elegible y, solo entonces, llama a deleteRequirement con resumeOnly: true. Si el borrado termina con éxito, el servicio clasifica si la operación se completó aquí o en otro lugar. Si la operación falla de forma operativa, devuelve pending_again en lugar de lanzar la excepción. fileciteturn0file7turn0file8

flowchart TD
    Start[resumePendingRequirementDeletion]
    Ctx[assertContext]
    Read[inspectPendingRequirement]
    Check[eligibility]
    StopA[Return non eligible]
    Step1[recovery afterPreconditions]
    Delete[deleteRequirement resumeOnly]
    Map[Map delete result]
    Recheck[Recheck existence and refs]
    StopB[Return completed or not pending]
    Retry[Return pending again]

    Start --> Ctx --> Read --> Check
    Check -->|not eligible| StopA
    Check -->|eligible| Step1 --> Delete --> Map
    Map -->|deleted| StopA
    Map -->|not found| Recheck --> StopB
    Map -->|other| Retry

La lectura de estado es siempre anterior a la escritura. El servicio reevalúa las precondiciones justo antes de actuar, no solo a partir de una clasificación previa. Eso evita usar un inventario antiguo como si fuera definitivo. fileciteturn0file7turn0file8

Key components

Estos son los exports públicos del módulo y su responsabilidad inmediata. El archivo también define helpers internos para validación, identificación y manejo de errores. fileciteturn0file0turn0file7

{
    "title": "createRecoveryContext",
    "content": "Crea el contexto interno de recuperaci\u00f3n. Valida operatorRef, reason y runId, y devuelve un objeto congelado con una marca privada.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/pending-requirement-recovery.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "inspectPendingRequirement",
    "content": "Lee el estado observado de un Requirement pendiente. Devuelve una vista de solo lectura con estado pendiente, due\u00f1o, medios y referencias a conversaciones.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/pending-requirement-recovery.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "eligibility",
    "content": "Convierte la observaci\u00f3n en una decisi\u00f3n de elegibilidad. Distingue entre not_found, not_pending, owner_unknown, erasing_present y eligible.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/pending-requirement-recovery.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "resumePendingRequirementDeletion",
    "content": "Reanuda la eliminaci\u00f3n de un Requirement pendiente. Relee las precondiciones, invoca deleteRequirement en modo resumeOnly y traduce el resultado a una salida de recuperaci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/pending-requirement-recovery.service.js",
    "badges": [
        "public"
    ]
}

Estas funciones forman la superficie pública del archivo. El resto del módulo son utilidades internas, como assertContext, step, isObjectId y errorCode. fileciteturn0file0turn0file7

Data & fields

El módulo trabaja con un conjunto pequeño de constantes y estructuras de retorno. Las validaciones son estrictas, y los valores inválidos terminan en errores o en salidas no elegibles. fileciteturn0file0turn0file7

createRecoveryContext input

[
    {
        "name": "operatorRef",
        "type": "string",
        "required": true,
        "description": "Referencia interna del operador. Debe cumplir SAFE_REF."
    },
    {
        "name": "reason",
        "type": "string",
        "required": true,
        "description": "Motivo interno de recuperación. Solo se acepta D1_RECOVERY."
    },
    {
        "name": "runId",
        "type": "string",
        "required": true,
        "description": "Identificador de ejecución. Debe cumplir SAFE_REF."
    }
]

resumePendingRequirementDeletion input

[
    {
        "name": "requirementId",
        "type": "string",
        "required": true,
        "description": "Id de MongoDB del Requirement a recuperar."
    },
    {
        "name": "context",
        "type": "object",
        "required": true,
        "description": "Contexto interno marcado por createRecoveryContext.",
        "children": [
            {
                "name": "operatorRef",
                "type": "string",
                "required": true,
                "description": "Referencia interna del operador."
            },
            {
                "name": "reason",
                "type": "string",
                "required": true,
                "description": "Motivo interno. Solo D1_RECOVERY es válido."
            },
            {
                "name": "runId",
                "type": "string",
                "required": true,
                "description": "Id de ejecución interna."
            }
        ]
    },
    {
        "name": "hooks",
        "type": "object",
        "description": "Hooks opcionales para pruebas. El servicio invoca recovery:afterPreconditions si existe."
    }
]

Constantes internas

  • RECOVERY_REASONS [badge:read-only] string[] — Solo contiene "D1_RECOVERY".
  • CONTEXT_BRAND [badge:read-only] Symbol — Marca privada del contexto interno.
  • SAFE_REF [badge:read-only] RegExp — Acepta solo letras, números, punto, guion bajo y guion medio. La longitud va de 1 a 64.
  • exists [badge:required] boolean — Indica si el Listing existe y coincide con intent: "requirement".
  • reason [badge:optional] string — Solo aparece cuando el requirementId es inválido. Devuelve invalid_id.
  • pending [badge:optional] boolean — true si deletion.status === "pending".
  • attempts [badge:optional] number | null — Número de intentos de borrado observados.
  • requestedAt [badge:optional] string | null — Fecha ISO de la solicitud de borrado.
  • ownerId [badge:optional] string | null — Valor persistido en createdBy, solo si es un ObjectId válido.
  • ownerAccountStatus [badge:optional] string | null — Estado de cuenta leído desde User.accountStatus.
  • mediaFiles [badge:required] number — Cantidad de ids de archivos derivados por listingImageKitFileIds.
  • claims [badge:required] object — Conteo por estado de MediaFileClaim. Las claves son estados y los valores son números.
  • conversationRefs [badge:required] number — Conteo de Conversation con activeRequirementId igual al Listing.
  • outcome [badge:required] string — Puede ser completed, completed_elsewhere, not_found, not_pending, owner_unknown, erasing_present o pending_again.
  • reason [badge:optional] string — Explica salidas como error, pending o causas devueltas por deleteRequirement.
  • code [badge:optional] string — Código normalizado con errorCode, truncado a 64 caracteres.
  • counts [badge:optional] object — Solo aparece cuando el borrado se completa. Incluye contadores de conversaciones y medios.
    • conversationRefsBefore [badge:required] number — Referencias de conversación observadas antes de actuar.
    • mediaDeleted [badge:required] number — Medios borrados por deleteRequirement.
    • mediaPreserved [badge:required] number — Medios preservados por deleteRequirement.

Behaviors

El archivo combina validación, lectura de estado y coordinación de recuperación. Cada función tiene una responsabilidad acotada y predecible. fileciteturn0file0turn0file7

  • createRecoveryContext — Valida los campos de entrada, marca el objeto con CONTEXT_BRAND y lo congela.
  • assertContext — Rechaza cualquier contexto que no tenga la marca privada interna.
  • step — Ejecuta un hook por nombre solo si existe y es una función. El comentario del archivo lo marca como utilitario para pruebas.
  • isObjectId — Acepta solo valores que son ObjectId válidos de 24 hex caracteres.
  • errorCode — Normaliza codeName, code o name y lo recorta a 64 caracteres.
  • inspectPendingRequirement — Lee Listing, MediaFileClaim, User y Conversation para construir una observación sin datos personales.
  • eligibility — Convierte la observación en un estado de decisión. Devuelve una etiqueta de elegibilidad única.
  • resumePendingRequirementDeletion — Relee el estado, valida elegibilidad, llama a deleteRequirement con resumeOnly: true y clasifica el resultado. fileciteturn0file7turn0file8

[!IMPORTANT] El dueño no llega desde el llamador. El servicio toma createdBy del documento persistido y lo usa como ownerId. Si ese valor no es válido, la recuperación devuelve owner_unknown y no escribe nada. fileciteturn0file7turn0file8

Workflows

Este flujo resume la secuencia operacional del módulo. El servicio relee el estado antes de actuar y no confía en una clasificación previa. fileciteturn0file7turn0file8

1. Crear contexto | `createRecoveryContext` valida `operatorRef`, `reason` y `runId`, y devuelve un contexto congelado.
2. Verificar contexto | `resumePendingRequirementDeletion` llama a `assertContext` antes de continuar.
3. Releer estado | `inspectPendingRequirement` consulta `Listing`, `MediaFileClaim`, `User` y `Conversation`.
4. Calcular elegibilidad | `eligibility` descarta casos no encontrados, no pendientes, sin dueño o con medios en `erasing`.
5. Reanudar borrado | `deleteRequirement` se ejecuta con `resumeOnly: true`.
6. Clasificar salida | El resultado se traduce a `completed`, `completed_elsewhere`, `not_pending`, `owner_unknown`, `erasing_present` o `pending_again`.

El archivo también contempla una doble lectura del final. Si deleteRequirement responde not_found, el servicio vuelve a revisar si el Listing y sus referencias de conversación siguen existiendo. Esa segunda comprobación distingue entre completed_elsewhere y not_pending.

Gotchas & edge cases

El comportamiento del módulo es estricto en varios puntos. Las salidas operativas están diseñadas para no lanzar excepciones, salvo la validación del contexto y de los parámetros internos. fileciteturn0file0turn0file7

[!WARNING] resumePendingRequirementDeletion exige un contexto creado por createRecoveryContext. Un objeto plano o sin la marca privada dispara un error inmediato.

  • Si deletion.status no es pending, la función devuelve not_pending sin escribir nada.
  • Si createdBy no es un ObjectId válido, la salida es owner_unknown.
  • Si existe algún MediaFileClaim en estado erasing, la salida es erasing_present.
  • Si deleteRequirement lanza un error, el módulo devuelve pending_again con un código normalizado.
  • Si el documento desaparece y ya no hay referencias de Conversation, la salida puede ser completed_elsewhere.
  • El módulo no define endpoints HTTP ni usa autenticación HTTP.

[!NOTE] La marca privada del contexto solo evita usos accidentales dentro del proceso. El propio archivo aclara que no es una frontera criptográfica.

Este módulo se apoya en varias piezas del backend de services/api. Cada una aporta una parte del estado o del flujo de eliminación. fileciteturn0file7turn0file8

  • services/api/src/models/Listing.js — Fuente principal del Requirement y de su estado de borrado.
  • services/api/src/models/Conversation.js — Conteo de referencias activas con activeRequirementId.
  • services/api/src/models/User.js — Lectura del accountStatus del dueño persistido.
  • services/api/src/models/MediaFileClaim.js — Estado de los archivos ligados al Listing.
  • services/api/src/modules/listings/requirement-deletion.service.js — Protocolo que este módulo reanuda con resumeOnly: true.
  • services/api/src/modules/media/media-claims.service.js — Provee listingImageKitFileIds, que deriva los ids de medios observados.