Rutas de listings
Overview
Este archivo define la interfaz HTTP de Listing para lectura y escritura. Expone la colección pública, el detalle por identificador y las mutaciones protegidas para crear, editar, cambiar estado y eliminar listings. También calcula likedByCurrentUser en la vista de colección cuando llega un Bearer token válido, y reutiliza ensamblado público compartido para la lista y el detalle .
Las rutas de escritura usan requireAuth y requireVerifiedEmail en POST, PATCH, PATCH /:id/status y DELETE. Las rutas de lectura no exigen verificación de correo, y GET /api/listings sigue siendo público, con mine=true como puerta para la vista autenticada del usuario .
[!NOTE]
likedByCurrentUsersolo aparece en la respuesta de colección. El detalle por:idusa el ensamblado público, pero no agrega esa marca derivada .
How it works
flowchart TD
Router[listings routes] --> ListPublic[GET api listings]
Router --> ListOne[GET api listings id]
Router --> Create[POST api listings]
Router --> Update[PATCH api listings id]
Router --> Status[PATCH api listings id status]
Router --> Delete[DELETE api listings id]
Router --> SourceDocs[source documents router]
ListPublic --> PublicAsm[assemblePublicListingsForViewer]
ListPublic --> LikesMap[buildLikedByCurrentUserMap]
ListPublic --> Jwt[verifyAccessToken]
ListPublic --> ListingModel[Listing model]
ListOne --> ListingModel
ListOne --> PublicAsm
Create --> Auth[requireAuth]
Create --> Email[requireVerifiedEmail]
Create --> Sanitize[sanitizeClientListingPayload]
Create --> Validate[validateListingPayload]
Create --> Media[resolveTrustedListingMedia]
Create --> Claim[claimListingMediaFiles]
Create --> ListingModel
Create --> Referral[validateRelatedOffer]
Update --> Auth
Update --> Email
Update --> Sanitize
Update --> Validate
Update --> Media
Update --> Claim
Update --> ListingModel
Update --> Referral
Update --> Cleanup[cleanupExpiredReferralRelations]
Status --> Auth
Status --> Email
Status --> ListingModel
Status --> Limit[checkListingLimit]
Status --> ReferralSync[syncReferralGraceForOffer]
Delete --> Auth
Delete --> Email
Delete --> ListingModel
Delete --> FileDelete[deleteListingFilesSafely]
La ruta agrupa lectura, edición, publicación y borrado dentro de un solo express.Router(). El flujo público filtra por estado, paginación y autor; el flujo de escritura valida autenticación, pertenencia y reglas de negocio. Además, las operaciones de medios usan reclamos y liberación controlada para evitar carreras entre requests concurrentes.
Lectura pública
La colección pública aplica filtros por lifecycle.status, publication.status, createdBy, status, userId e intent. Si se envía un Bearer válido, la ruta intenta resolver el usuario actual y marca cada listing con likedByCurrentUser usando ListingLike y buildLikedByCurrentUserMap .
Escrituras protegidas
Las mutaciones usan requireAuth y requireVerifiedEmail, y luego vuelven a comprobar propiedad, estado de ciclo de vida, límites del plan y coherencia del payload. La creación y la edición también pasan por validación del payload y por controles específicos de medios, referrals y ubicación exacta.
Key components
Estas son las piezas internas más importantes del archivo. Todas participan en la decisión de lectura o escritura, o en la preparación de la respuesta.
{
"title": "resolveIntent",
"content": "Normaliza el valor de intent. Solo \"requirement\" se conserva como requirement; cualquier otro valor resuelve a offer.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
{
"title": "checkListingLimit",
"content": "Lee el plan del usuario y valida cu\u00e1ntos listings activos puede tener para la intenci\u00f3n dada. Devuelve null si no hay problema o un body 403 si se alcanz\u00f3 el l\u00edmite.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
{
"title": "clearExactLocationIfNotDeclared",
"content": "Borra los datos de ubicaci\u00f3n exacta cuando location.declaration resuelve a not_declared. Conserva la informaci\u00f3n de \u00e1rea, pero vac\u00eda address, postalCode, latitude, longitude, googleMapsUrl, mapSnapshotUrl y short.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
{
"title": "respondGuardedWriteMiss",
"content": "Responde cuando una escritura protegida no encuentra coincidencia. Diferencia entre eliminaci\u00f3n pendiente y ausencia o falta de permisos.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
{
"title": "respondMediaWriteMiss",
"content": "Gestiona el fallo de una escritura con media revision. Libera reclamos que ya no siguen referenciados y responde con 409, 404 o el body de conflicto seg\u00fan el estado actual.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
{
"title": "respondRequirementDeletion",
"content": "Orquesta la eliminaci\u00f3n segura de requirements. Traduce los resultados de deleteRequirement a respuestas 200, 404, 500 o 502 con reintento.",
"type": "function",
"filePath": "services/api/src/modules/listings/listings.routes.js",
"badges": []
}
Data & fields
Este router trabaja con filtros de colección, payloads de escritura y varios estados internos. Los datos visibles al cliente pasan por allowlists y por reglas de limpieza antes de persistirse.
Constantes de control
Estas constantes guían la respuesta del router y el filtrado de escritura.
NOT_PENDING_DELETION— filtro de escritura que exigedeletion: null.LISTING_DELETION_PENDING_BODY— body común para conflictos por eliminación pendiente.LISTING_EDIT_CONFLICT_BODY— body común para choques de edición de medios.REQUIREMENT_DELETION_RETRY_BODY— body de reintento para eliminaciones de requirement.PLAN_LIMITS— cuotas porUser.plan.free—offerLimit: 5,requirementLimit: null.professional—offerLimit: 15,requirementLimit: null.creator— ilimitado en ambas intenciones.enterprise— ilimitado en ambas intenciones.
PLAN_LABELS— nombres legibles usados en mensajes de cuota.NON_EDITABLE_LIFECYCLE— estados que ya no admiten edición:protection,anonymized,purged.EXACT_LOCATION_FIELDS— campos borrados cuando la ubicación se declara como no declarada.
Query params de la colección
El endpoint de colección acepta paginación y filtros de búsqueda. Solo page y limit afectan la paginación; el resto cambia el conjunto devuelto.
[
{
"name": "page",
"type": "number",
"description": "Número de página. Si no es válido, el router usa 1."
},
{
"name": "limit",
"type": "number",
"description": "Cantidad de resultados por página. Si no es válido o supera 50, el router usa 10."
},
{
"name": "mine",
"type": "string",
"description": "Cuando vale \"true\", exige Bearer token y limita la consulta al usuario autenticado."
},
{
"name": "status",
"type": "string",
"description": "Filtra por publication.status. Si no es published, exige mine=true."
},
{
"name": "userId",
"type": "string",
"description": "Filtra por createdBy. Debe ser un ObjectId válido."
},
{
"name": "intent",
"type": "string",
"description": "Filtra por intention. requirement exige igualdad exacta; offer incluye los legacy sin campo."
}
]
Path params
Los endpoints de detalle, edición, cambio de estado y eliminación comparten el identificador de ruta.
[
{
"name": "id",
"type": "string",
"required": true,
"description": "Identificador del Listing que se consulta o modifica."
}
]
Payload de creación y edición
La ruta no acepta el cuerpo crudo completo. Primero lo pasa por sanitizeClientListingPayload, que solo conserva campos escribibles por cliente.
[
{
"name": "title",
"type": "string",
"description": "Título del listing."
},
{
"name": "intent",
"type": "string",
"description": "Intención del listing. Se normaliza con resolveIntent."
},
{
"name": "price",
"type": "number | object",
"description": "Dato de precio permitido por la allowlist del payload."
},
{
"name": "location",
"type": "object",
"description": "Subdocumento de ubicación. Puede conservar campos hermanos al hacer PATCH parcial."
},
{
"name": "description",
"type": "string",
"description": "Descripción del anuncio."
},
{
"name": "contact",
"type": "object",
"description": "Bloque de contacto permitido por el sanitizer."
},
{
"name": "media",
"type": "array",
"description": "Lista de medios. El router resuelve y verifica los archivos de ImageKit antes de guardar."
},
{
"name": "pdf",
"type": "object",
"description": "Metadatos del PDF generado bajo demanda."
},
{
"name": "meta",
"type": "object",
"description": "Subdocumento de metadatos del listing."
},
{
"name": "requirement",
"type": "object",
"description": "Bloque de requirement y referral cuando el listing pertenece a esa intención."
},
{
"name": "publication",
"type": "object",
"description": "Solo conserva los campos publicados por el cliente, como status y role."
},
{
"name": "submissionId",
"type": "string",
"description": "Identificador idempotente del intento de publicación."
}
]
Campos de ubicación exacta
Cuando location.declaration se resuelve como not_declared, el backend borra los datos precisos de ubicación aunque el cliente los haya enviado o aunque vengan heredados de un documento existente.
[
{
"name": "address",
"type": "string",
"description": "Se vacía si la ubicación se marca como no declarada."
},
{
"name": "postalCode",
"type": "string",
"description": "Se vacía si la ubicación se marca como no declarada."
},
{
"name": "latitude",
"type": "number | null",
"description": "Se pone en null si la ubicación se marca como no declarada."
},
{
"name": "longitude",
"type": "number | null",
"description": "Se pone en null si la ubicación se marca como no declarada."
},
{
"name": "googleMapsUrl",
"type": "string",
"description": "Se vacía si la ubicación se marca como no declarada."
},
{
"name": "mapSnapshotUrl",
"type": "string",
"description": "Se vacía si la ubicación se marca como no declarada."
},
{
"name": "short",
"type": "string",
"description": "Se vacía si la ubicación se marca como no declarada."
}
]
Reglas de referral
El router también toca campos de referral dentro de requirement.referral. Esa lógica se usa para validar el anuncio relacionado y preservar la gracia cuando el status de un offer cambia.
requirement.referral.relatedListingId— debe ser unObjectIdválido cuando existe.requirement.referral.relatedListingUnpublishedAt— se reinicia anullal crear o al cambiar el vínculo.validateRelatedOffer(...)— valida que el anuncio relacionado siga siendo válido.syncReferralGraceForOffer(...)— sincroniza la gracia cuando cambiapublication.status.cleanupExpiredReferralRelations()— limpia relaciones expiradas antes de ciertas escrituras.
Behaviors
Estas son las funciones y decisiones de negocio visibles en el archivo. Cada una devuelve una respuesta o prepara una escritura concreta.
isDeletionPending(listing)— devuelvetruecuandolisting.deletion.statusespending.isLifecycleEditable(listing)— devuelvetruesi el listing no está enprotection,anonymizedopurged.resolveIntent(rawIntent)— normaliza la intención del payload arequirementuoffer.checkListingLimit(userId, intent)— compara el conteo de listings activos del usuario con la cuota del plan y devuelve un body403si supera el límite.clearExactLocationIfNotDeclared(location)— elimina los campos exactos cuando la declaración de ubicación no existe.respondGuardedWriteMiss(res, context)— responde409si la eliminación quedó pendiente, o404si la escritura no puede continuar.respondMediaWriteMiss(res, context)— libera reclamos no referenciados y responde con el estado correcto para conflictos de medios.respondRequirementDeletion(res, context)— traduce el resultado dedeleteRequirementa la respuesta HTTP final.router.get("/")— lista listings con filtros, paginación, populate y marcalikedByCurrentUser.router.get("/:id")— devuelve un listing público o el propio listing del dueño si no está publicado.router.post("/")— crea un listing autenticado y verificado por correo.router.patch("/:id")— edita un listing propio con validación de payload y control de medios.router.patch("/:id/status")— cambiapublication.statusentrepublished,draftyarchived.router.delete("/:id")— elimina un listing propio, o delega a la eliminación segura si esrequirement.
API
GET /api/listings
Este endpoint devuelve la colección de listings. Es público por defecto, pero puede cambiar el alcance con mine=true o con filtros de estado y autor. Si llega un Bearer válido, añade likedByCurrentUser a cada elemento de la respuesta .
{
"title": "List listings",
"description": "Returns paginated listings with public filters, optional owner scope, and likedByCurrentUser when a valid token is present.",
"method": "GET",
"baseUrl": "/api",
"endpoint": "/listings",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": false
}
],
"queryParams": [
{
"key": "page",
"value": "1",
"required": false
},
{
"key": "limit",
"value": "10",
"required": false
},
{
"key": "mine",
"value": "true",
"required": false
},
{
"key": "status",
"value": "published",
"required": false
},
{
"key": "userId",
"value": "66c5f2f9e6c1c1d4b3a8f901",
"required": false
},
{
"key": "intent",
"value": "offer",
"required": false
}
],
"pathParams": [],
"bodyType": "none",
"requestBody": "",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Success",
"body": "{\n \"success\": true,\n \"count\": 2,\n \"total\": 24,\n \"page\": 1,\n \"pages\": 12,\n \"data\": [\n {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\",\n \"likedByCurrentUser\": false\n }\n ]\n}"
},
"401": {
"description": "Authentication required for non published status or mine scope",
"body": "{\n \"success\": false,\n \"message\": \"Se requiere autenticaci\u00f3n para consultar anuncios no publicados.\"\n}"
},
"400": {
"description": "Invalid userId",
"body": "{\n \"success\": false,\n \"message\": \"userId inv\u00e1lido.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudieron obtener los listings.\"\n}"
}
}
}
GET /api/listings/:id
Este endpoint devuelve un listing concreto por su identificador. El detalle es público si el listing está publicado. Si el listing no está publicado, solo el dueño autenticado puede verlo; en caso contrario, responde con 404 genérico .
{
"title": "Get listing by id",
"description": "Returns one listing by id. Draft or archived listings are only visible to their owner.",
"method": "GET",
"baseUrl": "/api",
"endpoint": "/listings/:id",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": false
}
],
"queryParams": [],
"pathParams": [
{
"key": "id",
"value": "Listing id",
"required": true
}
],
"bodyType": "none",
"requestBody": "",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Success",
"body": "{\n \"success\": true,\n \"data\": {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\",\n \"publication\": {\n \"status\": \"published\"\n }\n }\n}"
},
"400": {
"description": "Invalid id",
"body": "{\n \"success\": false,\n \"message\": \"ID inv\u00e1lido.\"\n}"
},
"404": {
"description": "Not found or not allowed",
"body": "{\n \"success\": false,\n \"message\": \"Listing no encontrado.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudo obtener el listing.\"\n}"
}
}
}
POST /api/listings
Este endpoint crea un listing nuevo para el usuario autenticado. La ruta primero normaliza intent, aplica idempotencia con submissionId, valida la cuota del plan y después procesa medios, referral y payload final antes de persistir el documento.
{
"title": "Create listing",
"description": "Creates a new listing for the authenticated and verified user, with plan checks, idempotency, media claims and payload validation.",
"method": "POST",
"baseUrl": "/api",
"endpoint": "/listings",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
},
{
"key": "Content-Type",
"value": "application/json",
"required": true
}
],
"queryParams": [],
"pathParams": [],
"bodyType": "json",
"requestBody": "{\n \"submissionId\": \"pub_2026_001\",\n \"intent\": \"offer\",\n \"title\": \"Casa en venta\",\n \"meta\": {\n \"entityType\": \"property\",\n \"operationType\": \"sale\",\n \"propertyType\": \"house\"\n },\n \"publication\": {\n \"status\": \"draft\"\n }\n}",
"formData": [],
"rawBody": "",
"responses": {
"201": {
"description": "Created",
"body": "{\n \"success\": true,\n \"message\": \"Listing creado correctamente.\",\n \"data\": {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\"\n }\n}"
},
"200": {
"description": "Idempotent retry",
"body": "{\n \"success\": true,\n \"message\": \"Listing ya creado para este env\u00edo.\",\n \"idempotent\": true,\n \"data\": {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\"\n }\n}"
},
"400": {
"description": "Invalid payload or related listing id",
"body": "{\n \"success\": false,\n \"message\": \"Payload inv\u00e1lido.\"\n}"
},
"401": {
"description": "Not authenticated or not verified",
"body": "{\n \"success\": false,\n \"message\": \"Se requiere autenticaci\u00f3n.\"\n}"
},
"403": {
"description": "Plan limit or seeker restriction",
"body": "{\n \"success\": false,\n \"code\": \"PLAN_LIMIT_REACHED\",\n \"plan\": \"free\",\n \"intent\": \"offer\",\n \"limit\": 5,\n \"message\": \"El plan gratuito permite hasta 5 anuncios activos. Actualiza a Plan Profesional o Plan Creador para publicar m\u00e1s.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudo crear el listing.\"\n}"
}
}
}
PATCH /api/listings/:id
Este endpoint edita un listing propio. Fusiona meta, requirement y location con el documento existente, controla la revisión de medios y vuelve a validar el payload completo antes de escribir.
{
"title": "Update listing",
"description": "Updates an owned listing with partial payloads, media revision checks and referral validation.",
"method": "PATCH",
"baseUrl": "/api",
"endpoint": "/listings/:id",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
},
{
"key": "Content-Type",
"value": "application/json",
"required": true
}
],
"queryParams": [],
"pathParams": [
{
"key": "id",
"value": "Listing id",
"required": true
}
],
"bodyType": "json",
"requestBody": "{\n \"title\": \"Casa en venta actualizada\",\n \"location\": {\n \"declaration\": \"not_declared\"\n }\n}",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Success",
"body": "{\n \"success\": true,\n \"message\": \"Listing actualizado correctamente.\",\n \"data\": {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\"\n }\n}"
},
"400": {
"description": "Invalid payload or immutable intent",
"body": "{\n \"success\": false,\n \"code\": \"INTENT_IMMUTABLE\",\n \"message\": \"No puedes cambiar si una publicaci\u00f3n es oferta o requerimiento despu\u00e9s de crearla.\"\n}"
},
"401": {
"description": "Not authenticated or not verified",
"body": "{\n \"success\": false,\n \"message\": \"Se requiere autenticaci\u00f3n.\"\n}"
},
"403": {
"description": "Seeker cannot edit offer",
"body": "{\n \"success\": false,\n \"code\": \"OFFER_NOT_ALLOWED_FOR_SEEKER\",\n \"message\": \"Tu cuenta est\u00e1 configurada para buscar, no para publicar ofertas.\"\n}"
},
"404": {
"description": "Not found or no permission",
"body": "{\n \"success\": false,\n \"message\": \"Listing no encontrado o sin permisos.\"\n}"
},
"409": {
"description": "Deletion pending or edit conflict",
"body": "{\n \"success\": false,\n \"code\": \"LISTING_EDIT_CONFLICT\",\n \"message\": \"Este anuncio cambi\u00f3 mientras guardabas. Recarga e intenta de nuevo.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudo actualizar el listing.\"\n}"
}
}
}
PATCH /api/listings/:id/status
Este endpoint cambia solo publication.status. Acepta published, draft y archived. Si el listing sale de archived, la ruta vuelve a revisar la cuota del plan, porque esa transición puede volver a activar el conteo de anuncios activos.
{
"title": "Change listing status",
"description": "Changes publication.status for an owned listing and rechecks plan limits when leaving archived.",
"method": "PATCH",
"baseUrl": "/api",
"endpoint": "/listings/:id/status",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
},
{
"key": "Content-Type",
"value": "application/json",
"required": true
}
],
"queryParams": [],
"pathParams": [
{
"key": "id",
"value": "Listing id",
"required": true
}
],
"bodyType": "json",
"requestBody": "{\n \"status\": \"draft\"\n}",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Success",
"body": "{\n \"success\": true,\n \"message\": \"Listing draft.\",\n \"data\": {\n \"_id\": \"66c5f2f9e6c1c1d4b3a8f901\"\n }\n}"
},
"400": {
"description": "Invalid status",
"body": "{\n \"success\": false,\n \"message\": \"Estado inv\u00e1lido. Usa: published, draft, archived.\"\n}"
},
"403": {
"description": "Plan limit reached",
"body": "{\n \"success\": false,\n \"code\": \"PLAN_LIMIT_REACHED\",\n \"plan\": \"free\",\n \"intent\": \"offer\",\n \"limit\": 5,\n \"message\": \"El plan gratuito permite hasta 5 anuncios activos. Actualiza a Plan Profesional o Plan Creador para publicar m\u00e1s.\"\n}"
},
"404": {
"description": "Not found or no permission",
"body": "{\n \"success\": false,\n \"message\": \"Listing no encontrado o sin permisos.\"\n}"
},
"409": {
"description": "Deletion pending",
"body": "{\n \"success\": false,\n \"code\": \"LISTING_DELETION_PENDING\",\n \"message\": \"Esta publicaci\u00f3n se est\u00e1 eliminando y ya no se puede modificar.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudo actualizar el estado.\"\n}"
}
}
}
DELETE /api/listings/:id
Este endpoint elimina un listing propio. Si el listing es requirement, delega en deleteRequirement; si es offer, primero borra los assets de ImageKit que ya no quedan referenciados y luego elimina el documento.
{
"title": "Delete listing",
"description": "Deletes an owned listing. Requirements use the safe deletion orchestrator and offers remove their media first.",
"method": "DELETE",
"baseUrl": "/api",
"endpoint": "/listings/:id",
"headers": [
{
"key": "Authorization",
"value": "Bearer <token>",
"required": true
}
],
"queryParams": [],
"pathParams": [
{
"key": "id",
"value": "Listing id",
"required": true
}
],
"bodyType": "none",
"requestBody": "",
"formData": [],
"rawBody": "",
"responses": {
"200": {
"description": "Deleted",
"body": "{\n \"success\": true,\n \"message\": \"Listing eliminado correctamente.\"\n}"
},
"404": {
"description": "Not found or no permission",
"body": "{\n \"success\": false,\n \"message\": \"Listing no encontrado o sin permisos.\"\n}"
},
"502": {
"description": "Partial media deletion failure or retryable requirement deletion failure",
"body": "{\n \"success\": false,\n \"code\": \"LISTING_DELETION_PENDING\",\n \"retryable\": true,\n \"message\": \"La eliminaci\u00f3n qued\u00f3 pendiente. Vuelve a intentarlo.\"\n}"
},
"500": {
"description": "Internal error",
"body": "{\n \"success\": false,\n \"message\": \"No se pudo eliminar el listing.\"\n}"
}
}
}
Workflows
La ruta tiene dos flujos principales: lectura pública y escritura protegida. La creación concentra la mayoría de dependencias, porque mezcla validación, cuotas, medios y referral.
sequenceDiagram
participant C as Client
participant R as listings routes
participant A as requireAuth
participant V as validateListingPayload
participant M as Listing
participant P as media claims service
participant F as referral service
C->>R: POST api listings
R->>A: verify auth and verified email
R->>R: sanitize payload and resolve intent
R->>R: check plan limit
R->>V: validate payload
R->>P: resolve media and claim files
R->>F: validate related offer
R->>M: create listing
M-->>R: created document
R-->>C: 201 response
El flujo de lectura es más corto. La colección consulta listings, compone el resultado público y añade la marca de like. El detalle consulta un solo documento, comprueba visibilidad pública u ownership, y devuelve el ensamblado público final.
Gotchas & edge cases
[!WARNING]
mine=trueexige un encabezadoAuthorizationcon formato Bearer. Si falta, la ruta responde401.
[!WARNING]
GET /api/listingssolo admitelimithasta50. Si el valor no es válido, usa10.
[!WARNING]
GET /api/listingsresponde401si se pide unstatusdistinto depublishedsinmine=true.
[!WARNING]
GET /api/listings/:idocultadraftyarchiveda usuarios que no son el dueño. También devuelve404para listings purgados.
[!WARNING]
POST /api/listingsaplica cuotas por plan antes de crear. El límite depende deUser.plany deintent.
[!CAUTION]
PATCH /api/listings/:idno permite cambiarintentdespués de creado. Si el payload intenta modificarlo, responde400conINTENT_IMMUTABLE.
[!IMPORTANT]
location.declaration = not_declaredborra los datos exactos de ubicación. La regla vive en el backend, incluso si el cliente manda esos campos o los hereda por merge.
[!WARNING] Las escrituras fallidas con
mediapueden devolver409 LISTING_EDIT_CONFLICT. Cuando eso ocurre, la ruta libera solo los reclamos que ya no quedan referenciados por el listing vigente.
[!NOTE]
LISTING_DELETION_PENDINGse usa como respuesta común cuando un listing ya entró en eliminación pendiente. En ese estado, el documento no debe modificarse.
[!IMPORTANT] Un
seekerpuede crear o editar requirements, pero no offers. La ruta lo bloquea antes de persistir cambios.
[!CAUTION]
deleteRequirementpuede devolver una eliminación reintentable. El router traduce ese caso aretryable: truepara que el cliente sepa que puede volver a intentar.
Related
Este router se apoya en módulos cercanos del backend y en varios consumidores del frontend. La documentación previa del proyecto ya lo ubica como la superficie de consultas públicas y mutaciones protegidas de listings .
services/api/src/models/Listing.js— modelo principal leído y escrito por este router.services/api/src/models/User.js— fuente delplanusado encheckListingLimit.services/api/src/modules/listings/listings.validators.js— valida el payload antes de crear o editar.services/api/src/modules/auth/auth.middleware.js— aportarequireAuthyrequireVerifiedEmail.services/api/src/modules/media/media.service.js— resuelve medios confiables antes de persistir.services/api/src/modules/listings/listing-write-sanitizer.js— filtra el payload del cliente.services/api/src/modules/listings/listing-public-assembly.js— construye la forma pública del listing.services/api/src/modules/listings/listing-public-view.js— elimina campos internos de la respuesta.services/api/src/modules/listings/listing-referral.js— valida y sincroniza referrals.services/api/src/modules/listings/requirement-deletion.service.js— orquesta la eliminación segura de requirements.services/api/src/modules/media/media-claims.service.js— reclama, libera y borra archivos de ImageKit.services/api/src/modules/listings/source-documents.routes.js— subrouter montado en/:id/source-documents.
[!NOTE] El archivo también reutiliza el patrón de ensamblado público compartido para la lista y el detalle. Esa decisión mantiene la forma pública coherente entre
GET /api/listingsyGET /api/listings/:id.