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 fileciteturn0file14turn0file19turn0file16
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 fileciteturn0file1turn0file7
[!IMPORTANT] Mientras la eliminación está pendiente, el
Requirementse 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 fileciteturn0file1turn0file16
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 fileciteturn0file19turn0file16
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 comodeletedAthistórico.deletion.attempts[badge:required]number— Empieza en1y 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 enConversationdurante el archivado.deletedRequirement[badge:optional]object | null— Huella histórica escrita en laConversationdel dueño.messages[badge:optional]array— Puede recibir un mensaje de sistema conkind: "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 dedeletion.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.
markRequirementForDeletionoutcome—"not_found","marked"o"resumed".requirement— Documento sinconversationReservations.invalidated— Número de reservas invalidadas.reservationsSeen— Número de reservas leídas desde el documento.
archiveConversationsOfDeletedRequirementoutcome—"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.
deleteRequirementoutcome—"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 enmongoose.Types.ObjectId.step(hooks, name)— Ejecuta un hook de prueba si existe.withoutReservations(requirement)— Devuelve una copia sinconversationReservations.buildRequirementDeletedEvent(requirementId)— Construye el mensaje de sistemarequirement_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 quedanConversationactivas que lo referencien.pendingError(error)— AñadedeletionPending = trueal 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 fileciteturn0file19turn0file1
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 fileciteturn0file1turn0file11
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 fileciteturn0file1turn0file11
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 fileciteturn0file16turn0file7
[!NOTE] La función usa
resumeOnlycomo mecanismo interno. En ese modo no crea una nueva marca y puede devolverpendingconmedia_in_progresssi 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,
Conversationpuede conservaractiveRequirementIdhasta la fase E5.2. El propio comentario aclara que eso no rompe el estado mientras el Requirement exista
not_foundno distingue entre inexistente, ajeno u Offer. Esa ambigüedad evita revelar cuál caso ocurrióresumeOnlynunca crea una marca nueva. Si el Requirement no está ya pendiente, devuelvenot_foundsin escribir nada- Si el archivado no deja
remaining = 0, la eliminación no avanza. El resultado sigue siendopending - 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 deleteListingFilesSafelysolo borra archivos que ninguna otra publicación usa ni puede adquirir. El módulo de medios describe esa coordinación conMediaFileClaimy su estado por archivo
Related
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 fileciteturn0file1turn0file7turn0file15
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 fileciteturn0file1turn0file15