requirement-deletion.service.js

Overview

Este módulo implementa la eliminación segura de un Requirement en tres fases: marcar, archivar conversaciones asociadas y borrar físicamente el documento. La propia cabecera del archivo describe el flujo como B3.0-E5.1, E5.2 y E5.3, con comportamiento idempotente y errores propagados al llamador fileciteturn0file14turn0file19turn0file16

La lógica trabaja sobre Listing y Conversation, y coordina la limpieza de medios con deleteListingFilesSafely. También se apoya en el coordinador A3 de reservas para invalidar asociaciones antes del borrado final fileciteturn0file1turn0file7

[!IMPORTANT] Mientras la eliminación está pendiente, el Requirement se archiva y deja de aceptar nuevas asociaciones. El flujo no informa éxito completo hasta que el borrado físico termina y no quedan referencias activas fileciteturn0file1turn0file16

How it works

flowchart TD
  Request[deleteRequirement]
  Mark[markRequirementForDeletion]
  Resume[resumeOnly]
  Invalid[not_found]
  Bar1[invalidateReservations]
  Archive[archiveConversationsOfDeletedRequirement]
  Bar2[invalidateReservationsOfMarkedRequirement]
  Media[deleteListingFilesSafely]
  Remove[Listing deleteOne]
  Done[deleted]
  Pending[pending]

  Request --> Mark
  Mark -->|invalid ids or no match| Invalid
  Mark -->|marked or resumed| Bar1
  Mark --> Archive
  Archive -->|not marked or incomplete| Pending
  Archive --> Bar2
  Archive --> Media
  Media -->|partial failure| Pending
  Media --> Remove
  Remove -->|success| Done
  Remove -->|no match and completed elsewhere| Done
  Request --> Resume

El flujo empieza con la marca condicional del Requirement. Si el documento ya estaba marcado, la función reanuda la eliminación y aumenta deletion.attempts. Después invalida reservas, archiva y desvincula Conversation, limpia medios y solo entonces ejecuta el borrado físico condicional fileciteturn0file19turn0file16

Key components

{
    "title": "markRequirementForDeletion",
    "content": "Marca o reanuda la eliminaci\u00f3n de un Requirement propio. Escribe deletion pendiente, archiva la publicaci\u00f3n y devuelve el documento sin reservas internas.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/requirement-deletion.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "archiveConversationsOfDeletedRequirement",
    "content": "Invalida reservas A3, archiva las Conversation que siguen apuntando al Requirement y deja cero referencias activas antes del borrado f\u00edsico.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/requirement-deletion.service.js",
    "badges": [
        "public"
    ]
}
{
    "title": "deleteRequirement",
    "content": "Orquesta la eliminaci\u00f3n completa. Coordina marca, archivado, medios y borrado f\u00edsico, y se\u00f1ala con deletionPending cualquier fallo posterior a la marca.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/requirement-deletion.service.js",
    "badges": [
        "public"
    ]
}

Function parameters

[
    {
        "name": "requirementId",
        "type": "string | mongoose.Types.ObjectId",
        "required": true,
        "description": "Identificador del Requirement que se marcará, archivará o borrará."
    },
    {
        "name": "ownerId",
        "type": "string | mongoose.Types.ObjectId",
        "required": true,
        "description": "Identificador del dueño. Debe coincidir con createdBy."
    },
    {
        "name": "hooks",
        "type": "object | null",
        "description": "Puntos de pausa para pruebas. El archivo usa mark:afterMark, mark:afterInvalidate, archive:afterBarrier, archive:beforeConversation, archive:afterConversation, archive:beforeVerify, delete:afterMark, delete:afterArchive y delete:afterMedia."
    },
    {
        "name": "resumeOnly",
        "type": "boolean",
        "description": "Modo interno. No crea una marca nueva. Solo reanuda una eliminación ya pendiente."
    }
]

Data & fields

El módulo trabaja con campos persistidos y con resultados intermedios. Los campos de Listing y Conversation se usan como señales de estado, no como simples datos decorativos.

  • deletion.status [badge:required] string — Se fija en "pending" cuando empieza la eliminación.
  • deletion.requestedAt [badge:required] Date — Marca temporal de la solicitud. Se reutiliza como deletedAt histórico.
  • deletion.attempts [badge:required] number — Empieza en 1 y aumenta al reanudar.
  • publication.status [badge:required] string — Se fuerza a "archived" en la misma escritura que crea la marca.
  • conversationReservations [badge:read-only] array — Reservas internas usadas solo para invalidación. No se devuelven al llamador.
  • activeRequirementId [badge:optional] ObjectId | null — Se limpia en Conversation durante el archivado.
  • deletedRequirement [badge:optional] object | null — Huella histórica escrita en la Conversation del dueño.
  • messages [badge:optional] array — Puede recibir un mensaje de sistema con kind: "requirement_deleted".
  • requirementId [badge:required] ObjectId — Identificador del Requirement eliminado.
  • title [badge:optional] string | null — Título histórico copiado desde el Requirement.
  • deletedAt [badge:required] Date — Toma el valor de deletion.requestedAt.
  • _id [badge:required] ObjectId — Identificador del mensaje.
  • role [badge:required] string — Siempre "system".
  • kind [badge:required] string — Siempre "requirement_deleted".
  • text [badge:required] string — Vacío en este módulo.
  • requirementId [badge:required] ObjectId — Refiere al Requirement archivado.
  • createdAt [badge:required] Date — Timestamp de creación del evento.
  • markRequirementForDeletion
    • outcome — "not_found", "marked" o "resumed".
    • requirement — Documento sin conversationReservations.
    • invalidated — Número de reservas invalidadas.
    • reservationsSeen — Número de reservas leídas desde el documento.
  • archiveConversationsOfDeletedRequirement
    • outcome — "not_found", "not_marked", "archived" o "incomplete".
    • archived — Número de conversaciones archivadas con evento.
    • archivedWithoutEvent — Número de conversaciones archivadas sin duplicar evento.
    • unlinkedForeign — Número de conversaciones ajenas desvinculadas.
    • reservationsInvalidated — Reservas invalidadas por la barrera.
    • remaining — Conversaciones que siguen apuntando al Requirement.
  • deleteRequirement
    • outcome — "not_found", "deleted" o "pending".
    • concurrent — Indica que otra ejecución ya completó el borrado.
    • mediaDeleted — Archivos borrados en ImageKit.
    • mediaPreserved — Archivos preservados porque otra publicación los retiene.
    • reason — "archive_incomplete", "media_failed" o "media_in_progress".
    • failures — Fallos devueltos por la capa de medios.

Behaviors

  • toObjectId(value) — Convierte un valor en mongoose.Types.ObjectId.
  • step(hooks, name) — Ejecuta un hook de prueba si existe.
  • withoutReservations(requirement) — Devuelve una copia sin conversationReservations.
  • buildRequirementDeletedEvent(requirementId) — Construye el mensaje de sistema requirement_deleted.
  • archiveOwnConversation(...) — Archiva una conversación del dueño y agrega el evento si todavía no existe.
  • completedElsewhere(requirementId) — Comprueba si el Requirement ya no existe y ya no quedan Conversation activas que lo referencien.
  • pendingError(error) — Añade deletionPending = true al error antes de relanzarlo.

markRequirementForDeletion

La función valida requirementId y ownerId antes de tocar MongoDB. Si ambos son válidos, intenta una escritura condicional sobre un único documento que coincida con _id, createdBy, intent: "requirement" y deletion: null

Si la escritura coincide, fija deletion completo y archiva publication.status en la misma operación. Si ya estaba marcado, reanuda con un $inc de deletion.attempts y conserva requestedAt. Después invalida las reservas que el propio documento devolvió y devuelve el Requirement sin campos internos fileciteturn0file19turn0file1

archiveConversationsOfDeletedRequirement

La función solo actúa si el Requirement existe, pertenece al dueño y tiene deletion.status = "pending". Primero aplica una barrera con invalidateReservationsOfMarkedRequirement. Esa barrera impide que nuevas asociaciones con el Requirement puedan confirmarse fileciteturn0file1turn0file11

Luego recorre las Conversation que todavía tienen activeRequirementId igual al Requirement. Si la conversación pertenece al dueño, la archiva, la desvincula y agrega deletedRequirement y el evento en una sola escritura. Si pertenece a otro dueño, solo elimina activeRequirementId y no copia el título ni el evento fileciteturn0file1turn0file11

deleteRequirement

La función ejecuta la marca o reanudación, luego el archivado de conversaciones, luego la coordinación de medios y, por último, el borrado físico condicionado. Si el archivo de medios falla con una pérdida parcial, devuelve pending con reason: "media_failed". Si alguna fase queda incompleta, también devuelve pending y no afirma éxito prematuro fileciteturn0file16turn0file7

[!NOTE] La función usa resumeOnly como mecanismo interno. En ese modo no crea una nueva marca y puede devolver pending con media_in_progress si otra operación ya tomó posesión de archivos exclusivos

Gotchas & edge cases

[!WARNING] El archivo documenta una limitación conocida sin transacciones. Si una asociación confirma antes de que se invalide su reserva, Conversation puede conservar activeRequirementId hasta la fase E5.2. El propio comentario aclara que eso no rompe el estado mientras el Requirement exista

  • not_found no distingue entre inexistente, ajeno u Offer. Esa ambigüedad evita revelar cuál caso ocurrió
  • resumeOnly nunca crea una marca nueva. Si el Requirement no está ya pendiente, devuelve not_found sin escribir nada
  • Si el archivado no deja remaining = 0, la eliminación no avanza. El resultado sigue siendo pending
  • Si el borrado físico falla después de la marca, el error se propaga con deletionPending = true. La eliminación queda pendiente y puede reintentarse
  • deleteListingFilesSafely solo borra archivos que ninguna otra publicación usa ni puede adquirir. El módulo de medios describe esa coordinación con MediaFileClaim y su estado por archivo

Este servicio se integra con services/api/src/modules/conversations/requirement-association.service.js para invalidar reservas y bloquear nuevas asociaciones. También usa services/api/src/modules/media/media-claims.service.js para la coordinación de archivos y services/api/src/models/Listing.js y services/api/src/models/Conversation.js para las escrituras finales fileciteturn0file1turn0file7turn0file15

La ruta HTTP que consume esta lógica vive en listings.routes.js, donde el DELETE /:id del Requirement se apoya en deleteRequirement. El módulo de rutas también mantiene la regla de que un Listing con eliminación pendiente ya no se modifica fileciteturn0file1turn0file15