Política de interacción de listings
Overview
Este módulo define la política central de interacción pública para un Listing. Decide si un anuncio puede recibir comentarios, likes, eventos y, de forma reservada, leads futuros. La lógica es determinista, pura y no consulta Mongo; el router ya debe pasar listing y ownerPreferences resueltos antes de llamar al helper.
Se usa como punto común de decisión en rutas de comentarios, likes y eventos. Así evita que cada router reconstruya su propia lógica de bloqueo y sus propios ApiError.
[!NOTE]
DEFAULT_OWNER_PREFERENCESes la fuente canónica de los defaults dedeletionGracePreferences. El comentario del archivo indica que debe coincidir conservices/api/src/models/User.js, y que los datos legacy nunca deben quedar bloqueados por una preferencia ausente.
How it works
flowchart TB
A[listing y ownerPreferences] --> B[Resolver estado de ciclo y publicacion]
B --> C[listing ausente]
C --> D[Denial completo LISTING_UNAVAILABLE]
B --> E[Estado purged]
E --> F[Denial completo LISTING_UNAVAILABLE]
B --> G[Publicacion no published]
G --> H[Denial completo LISTING_NOT_PUBLISHED]
B --> I[Publicado y no purged]
I --> J[Estado normal]
J --> K[Permitir comentarios likes eventos y leads]
I --> L[Estado protection o anonymized]
L --> M[Comentarios y leads dependen de ownerPreferences]
L --> N[Likes y eventos permitidos]
O[DEFAULT_OWNER_PREFERENCES] --> M
P[BLANKET_DENIED_POLICY_FIELDS] --> D
P --> F
P --> H
La función primero resuelve el estado del listing. Si el listing no existe, o si está purged, devuelve una denegación global con LISTING_UNAVAILABLE. Si la publicación no es published, devuelve LISTING_NOT_PUBLISHED. Solo cuando el listing está publicado y no purgado evalúa las preferencias del owner.
[!IMPORTANT] La política usa una allowlist explícita. Solo
publication.status = publishedhabilita interacciones públicas. Cualquier otro estado bloquea todo, ypurgedprevalece sobre cualquier otra condición.
Cuando el lifecycle es normal, la función abre todo sin consultar preferencias. Cuando es protection o anonymized, likes y eventos siguen permitidos, pero comentarios y leads dependen de allowComments y allowLeadCapture.
Key components
El módulo expone cuatro funciones y un helper interno. La política principal calcula el contrato de interacción, y los asserts traducen ese contrato en errores concretos para los routers que ya tienen su try catch.
{
"title": "getListingInteractionPolicy",
"content": "Funci\u00f3n p\u00fablica [badge:public] que calcula la pol\u00edtica completa de interacci\u00f3n de un Listing. Devuelve permisos, banderas de estado y una denegaci\u00f3n global cuando el anuncio est\u00e1 purgado o no publicado.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-interaction-policy.js",
"badges": [
"public"
]
}
{
"title": "assertListingCanReceiveComment",
"content": "Funci\u00f3n p\u00fablica [badge:public] que valida si el listing puede recibir comentarios. Lanza ApiError 404 para denegaciones globales y ApiError 403 con COMMENTS_DISABLED cuando solo los comentarios est\u00e1n desactivados.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-interaction-policy.js",
"badges": [
"public"
]
}
{
"title": "assertListingCanReceiveLike",
"content": "Funci\u00f3n p\u00fablica [badge:public] que valida si el listing puede recibir likes. Reutiliza la denegaci\u00f3n global y, si aplica, lanza ApiError 403 con LIKES_DISABLED.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-interaction-policy.js",
"badges": [
"public"
]
}
{
"title": "assertListingCanReceiveEvent",
"content": "Funci\u00f3n p\u00fablica [badge:public] que valida si el listing puede recibir eventos. Reutiliza la denegaci\u00f3n global y, si aplica, lanza ApiError 403 con EVENTS_DISABLED.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-interaction-policy.js",
"badges": [
"public"
]
}
{
"title": "throwBlanketDenialIfAny",
"content": "Helper interno que lanza ApiError 404 cuando policy.denialCode existe. Centraliza la denegaci\u00f3n total para los asserts p\u00fablicos.",
"type": "function",
"filePath": "services/api/src/modules/listings/listing-interaction-policy.js",
"badges": []
}
Data & fields
La entrada del helper es pequeña, pero está muy estructurada. listing aporta el estado que gobierna toda la decisión, y ownerPreferences solo participa cuando el listing está en protection o anonymized.
[
{
"name": "listing",
"type": "object",
"description": "Listing ya resuelto por el router. La función lee lifecycle.status y publication.status.",
"children": [
{
"name": "lifecycle",
"type": "object",
"description": "Contenedor del estado de ciclo de vida del listing.",
"children": [
{
"name": "status",
"type": "string",
"description": "Puede ser normal, protection, anonymized o purged. Si falta, se asume normal."
}
]
},
{
"name": "publication",
"type": "object",
"description": "Contenedor del estado de publicación.",
"children": [
{
"name": "status",
"type": "string",
"description": "Solo published habilita interacciones públicas. Cualquier otro valor bloquea todo."
}
]
}
]
},
{
"name": "ownerPreferences",
"type": "object",
"description": "Preferencias del propietario. Se fusionan con DEFAULT_OWNER_PREFERENCES antes de evaluar protection o anonymized.",
"children": [
{
"name": "keepPublishedListingsVisible",
"type": "boolean",
"description": "Forma parte del contrato canónico de defaults. Esta función no lo consume."
},
{
"name": "allowLeadCapture",
"type": "boolean",
"description": "Controla canReceiveLeads cuando el listing no está en normal."
},
{
"name": "allowComments",
"type": "boolean",
"description": "Controla canReceiveComments cuando el listing no está en normal."
}
]
}
]
DEFAULT_OWNER_PREFERENCES y BLANKET_DENIED_POLICY_FIELDS son objetos de solo lectura. El primero fija defaults permisivos para cuentas legacy, y el segundo predefine el estado de bloqueo total.
DEFAULT_OWNER_PREFERENCES[badge:read-only]object— contienekeepPublishedListingsVisible: true,allowLeadCapture: trueyallowComments: true.BLANKET_DENIED_POLICY_FIELDS[badge:read-only]object— contienecanReceiveComments: false,canReceiveLikes: false,canReceiveEvents: falseycanReceiveLeads: false.
[
{
"name": "canReceiveComments",
"type": "boolean",
"required": true,
"description": "Indica si el listing acepta comentarios."
},
{
"name": "canReceiveLikes",
"type": "boolean",
"required": true,
"description": "Indica si el listing acepta likes."
},
{
"name": "canReceiveEvents",
"type": "boolean",
"required": true,
"description": "Indica si el listing acepta eventos."
},
{
"name": "canReceiveLeads",
"type": "boolean",
"required": true,
"description": "Campo reservado para captura pública de leads. Ninguna ruta lo consume todavía."
},
{
"name": "isNormal",
"type": "boolean",
"required": true,
"description": "True cuando lifecycle.status es normal."
},
{
"name": "isProtection",
"type": "boolean",
"required": true,
"description": "True cuando lifecycle.status es protection."
},
{
"name": "isAnonymized",
"type": "boolean",
"required": true,
"description": "True cuando lifecycle.status es anonymized."
},
{
"name": "isPurged",
"type": "boolean",
"required": true,
"description": "True cuando lifecycle.status es purged."
},
{
"name": "isPublished",
"type": "boolean",
"required": true,
"description": "True cuando publication.status es published."
},
{
"name": "denialCode",
"type": "string | null",
"required": true,
"description": "Código de denegación global. Solo se usa para bloqueos de listing completo."
},
{
"name": "denialMessage",
"type": "string | null",
"required": true,
"description": "Mensaje de denegación global asociado a denialCode."
}
]
Behaviors
getListingInteractionPolicy— construye un objeto de política determinista. Devuelve permisos, banderas de estado y un denial global cuando el listing no puede interactuar en absoluto.assertListingCanReceiveComment— lanzaApiError404 si existe una denegación global. Si no, lanzaApiError403 conCOMMENTS_DISABLEDcuando los comentarios están desactivados.assertListingCanReceiveLike— aplica la misma denegación global y, si corresponde, lanzaApiError403 conLIKES_DISABLED.assertListingCanReceiveEvent— aplica la misma denegación global y, si corresponde, lanzaApiError403 conEVENTS_DISABLED.throwBlanketDenialIfAny— helper interno que conviertedenialCodeen unApiError404 con el mensaje exacto guardado en la política.
Workflow de evaluación
La decisión sigue un orden fijo. Ese orden está codificado en el archivo y también validado por la matriz de pruebas.
- Si
listingno existe, bloquear todo conLISTING_UNAVAILABLE. - Leer
lifecycle.statusypublication.status, con defaultsnormalydraft. - Si
lifecycle.statusespurged, bloquear todo conLISTING_UNAVAILABLE. - Si
publication.statusno espublished, bloquear todo conLISTING_NOT_PUBLISHED. - Si
lifecycle.statusesnormal, permitir comentarios, likes, eventos y leads. - Si
lifecycle.statusesprotectionoanonymized, permitir likes y eventos, y condicionar comentarios y leads a las preferencias del owner.
Gotchas & edge cases
[!WARNING]
denialCodeydenialMessagesolo representan un bloqueo de listing completo. Un bloqueo puntual, como comentarios desactivados, no usa esos campos. En ese caso, el helper lanzaCOMMENTS_DISABLEDcon403.
[!WARNING]
publication.status = draftypublication.status = archivedbloquean por igual. El archivo usa una allowlist explícita, así que cualquier valor distinto depublishedqueda denegado.
[!NOTE] Las preferencias legacy ausentes no bloquean accidentalmente. Si
ownerPreferencesfalta, la fusión conDEFAULT_OWNER_PREFERENCESdejaallowCommentsyallowLeadCaptureentrue.
[!CAUTION]
canReceiveLeadsestá presente en la forma de salida, pero ninguna ruta lo consume todavía. El propio archivo lo reserva para una captura pública futura.
[!IMPORTANT] La función es pura. No muta
listingniownerPreferences, y el mismo input produce siempre el mismo resultado. Las pruebas lo cubren conObject.freezey llamadas repetidas.
Related
services/api/src/modules/listing-likes/listing-likes.routes.js usa getListingInteractionPolicy y assertListingCanReceiveLike antes de alternar likes. El flujo evita depender de deletionGracePreferences para likes y eventos.
services/api/src/modules/listings/listing-comments.routes.js usa getListingInteractionPolicy y assertListingCanReceiveComment para lectura y creación de comentarios. También carga contexto del owner antes de aplicar la política.
services/api/src/modules/listings/listing-events.routes.js usa getListingInteractionPolicy y assertListingCanReceiveEvent para registrar eventos. El comentario del flujo confirma que likes y eventos no dependen de deletionGracePreferences.
services/api/test/listing-interaction-policy.test.js valida la matriz completa. Cubre normal, protection, anonymized, purged, draft, archived, preferencias legacy ausentes, pureza y mapeo de errores.
services/api/src/models/User.js aparece como la referencia canónica que debe coincidir con DEFAULT_OWNER_PREFERENCES. Esa relación está declarada en el comentario del archivo, aunque el modelo no está incluido aquí.