Listing model
Overview
Este archivo define el modelo Mongoose Listing para la plataforma. El documento concentra la información principal de un anuncio, junto con estados editoriales, de ciclo de vida, medios, contacto, métricas y datos de búsqueda. También soporta dos usos de negocio distintos con el mismo modelo: intent = "offer" y intent = "requirement".
El esquema usa subdocumentos separados para mantener vocabularios claros y evitar cruces semánticos. Varias partes del documento tienen defaults explícitos, enums cerrados y validaciones locales. Eso permite conservar compatibilidad con documentos legados sin migraciones forzosas.
[!IMPORTANT]
publication.status,lifecycle.statusydeletion.statusrepresentan ciclos distintos. El estado editorial no se usa para el cierre de cuenta ni para la eliminación recuperable.
How it works
flowchart TB
L[ListingSchema] --> T[title]
L --> I[intent]
L --> P[price]
L --> Lo[location]
L --> D[description]
L --> C[contact]
L --> M[media]
L --> Pdf[pdf]
L --> Sd[sourceDocuments]
L --> Meta[meta]
L --> Req[requirement]
L --> Pub[publication]
L --> Life[lifecycle]
L --> Del[deletion]
L --> MR[mediaRevision]
L --> CR[conversationReservations]
L --> Met[metrics]
L --> CB[createdBy]
L --> Sub[submissionId]
L --> Idx[indexes]
P --> PS[PriceSchema]
Lo --> LS[LocationSchema]
D --> DS[DescriptionSchema]
C --> CS[ContactSchema]
M --> MIS[MediaItemSchema]
Pdf --> PDFS[PdfSchema]
Sd --> SDoc[SourceDocumentSchema]
Meta --> MS[MetaSchema]
Req --> RS[RequirementSchema]
Pub --> PubS[PublicationSchema]
Life --> LifeS[LifecycleSchema]
Del --> DelS[DeletionSchema]
CR --> CRS[ConversationReservationSchema]
El flujo principal es simple: ListingSchema reúne los subdocumentos y valida sus valores. Luego, mongoose.model("Listing", ListingSchema) registra el modelo y lo exporta por defecto como Listing. Los índices del final del archivo soportan consultas frecuentes, control de cuotas e idempotencia de creación.
Key components
{
"title": "ListingSchema",
"content": "Esquema principal de Mongoose. Agrupa los subdocumentos del anuncio, sus estados y sus \u00edndices de persistencia.",
"type": "class",
"filePath": "services/api/src/models/Listing.js",
"badges": [
"public"
]
}
{
"title": "Listing",
"content": "Modelo Mongoose exportado por defecto. Representa un documento Listing persistido en MongoDB.",
"type": "class",
"filePath": "services/api/src/models/Listing.js",
"badges": [
"public"
]
}
{
"title": "RequirementSchema",
"content": "Subdocumento de b\u00fasqueda. Modela lo que el usuario busca, con presupuesto, zona, preferencias y referencia opcional.",
"type": "class",
"filePath": "services/api/src/models/Listing.js",
"badges": [
"public"
]
}
{
"title": "LifecycleSchema",
"content": "Subdocumento del ciclo de cierre de cuenta. Separa protecci\u00f3n, anonimizaci\u00f3n y purga del estado editorial.",
"type": "class",
"filePath": "services/api/src/models/Listing.js",
"badges": [
"public"
]
}
Data & fields
Top-level ListingSchema
Este bloque define los campos visibles en la raíz del documento. Varios campos son subdocumentos con su propio esquema interno. Otros campos, como mediaRevision y conversationReservations, son internos y tienen comportamiento especial.
[
{
"name": "title",
"type": "string",
"required": true,
"description": "Título principal del Listing."
},
{
"name": "intent",
"type": "string enum",
"description": "Discriminador transversal. Valores: offer o requirement. Default: offer. Es immutable."
},
{
"name": "price",
"type": "object",
"description": "Precio normalizado de la publicación. Usa PriceSchema.",
"children": [
{
"name": "amount",
"type": "number",
"description": "Monto numérico."
},
{
"name": "currency",
"type": "string",
"description": "Moneda ISO o texto equivalente. Default: MXN."
},
{
"name": "text",
"type": "string",
"description": "Representación textual del precio."
},
{
"name": "periodicity",
"type": "string enum",
"description": "Periodicidad compartida con RequirementBudgetSchema. Default: unspecified."
}
]
},
{
"name": "location",
"type": "object",
"description": "Ubicación principal del anuncio. Usa LocationSchema.",
"children": [
{
"name": "country",
"type": "string",
"description": "País de la ubicación."
},
{
"name": "state",
"type": "string",
"description": "Estado o entidad federativa."
},
{
"name": "city",
"type": "string",
"description": "Ciudad."
},
{
"name": "neighborhood",
"type": "string",
"description": "Colonia o barrio."
},
{
"name": "declaration",
"type": "string enum",
"description": "Estado de declaración de ubicación. Default: unspecified."
}
]
},
{
"name": "description",
"type": "object",
"description": "Texto descriptivo. Usa DescriptionSchema.",
"children": [
{
"name": "short",
"type": "string",
"description": "Resumen breve."
},
{
"name": "full",
"type": "string",
"description": "Descripción completa."
},
{
"name": "caption",
"type": "string",
"description": "Leyenda corta."
}
]
},
{
"name": "contact",
"type": "object",
"description": "Datos de contacto. Usa ContactSchema.",
"children": [
{
"name": "name",
"type": "string",
"description": "Nombre visible del contacto."
},
{
"name": "phone",
"type": "string",
"description": "Teléfono de contacto."
},
{
"name": "method",
"type": "string enum",
"description": "Canal preferido de contacto."
},
{
"name": "link",
"type": "string",
"description": "Enlace de contacto."
},
{
"name": "message",
"type": "string",
"description": "Mensaje sugerido."
}
]
},
{
"name": "media",
"type": "array",
"description": "Lista de medios del anuncio. Cada elemento sigue MediaItemSchema."
},
{
"name": "pdf",
"type": "object",
"description": "Metadatos del PDF generado por Plano. Usa PdfSchema.",
"children": [
{
"name": "url",
"type": "string",
"description": "URL del PDF generado."
},
{
"name": "fileName",
"type": "string",
"description": "Nombre de archivo del PDF."
},
{
"name": "generatedAt",
"type": "date",
"description": "Fecha de generación."
}
]
},
{
"name": "sourceDocuments",
"type": "array",
"description": "Documentos fuente cargados por el anunciante. Cada elemento sigue SourceDocumentSchema."
},
{
"name": "meta",
"type": "object",
"required": true,
"description": "Bloque principal de clasificación y características. Usa MetaSchema.",
"children": [
{
"name": "entityType",
"type": "string enum",
"required": true,
"description": "Tipo de entidad del Listing."
},
{
"name": "operationType",
"type": "string enum",
"required": true,
"description": "Operación principal del Listing."
},
{
"name": "propertyType",
"type": "string enum",
"description": "Tipo de inmueble, cuando entityType es property."
}
]
},
{
"name": "requirement",
"type": "object",
"description": "Subdocumento de búsqueda. Siempre existe como objeto, pero solo tiene sentido con intent=requirement.",
"children": [
{
"name": "budget",
"type": "object",
"description": "Presupuesto del Requirement."
},
{
"name": "desiredAmenities",
"type": "array",
"description": "Amenidades deseadas."
},
{
"name": "preferences",
"type": "object",
"description": "Preferencias comparables de la búsqueda."
}
]
},
{
"name": "publication",
"type": "object",
"description": "Estado editorial de la publicación. Usa PublicationSchema.",
"children": [
{
"name": "status",
"type": "string enum",
"description": "Estado editorial. Default: published."
},
{
"name": "verified",
"type": "boolean",
"description": "Marca de verificación editorial."
},
{
"name": "role",
"type": "string enum",
"description": "Rol editorial del publicador."
}
]
},
{
"name": "lifecycle",
"type": "object",
"description": "Estado del ciclo de cierre de cuenta. Usa LifecycleSchema.",
"children": [
{
"name": "status",
"type": "string enum",
"description": "Estado del lifecycle. Default: normal."
},
{
"name": "previousPublicationStatus",
"type": "string enum",
"description": "Estado editorial previo, capturado al entrar en protection."
},
{
"name": "publicOwnerLabel",
"type": "string",
"description": "Etiqueta pública del titular."
}
]
},
{
"name": "deletion",
"type": "object",
"description": "Reserva de eliminación recuperable. Solo existe mientras la eliminación está pendiente."
},
{
"name": "mediaRevision",
"type": "number",
"description": "Versión de medios controlada por el servidor. No tiene default."
},
{
"name": "conversationReservations",
"type": "array",
"description": "Reservas internas de Conversation -> Requirement. Se oculta por defecto con select:false."
},
{
"name": "metrics",
"type": "object",
"description": "Contadores de interacción y sentimiento agregado."
},
{
"name": "createdBy",
"type": "objectId",
"description": "Referencia a User."
},
{
"name": "submissionId",
"type": "string",
"description": "Idempotency token del envío de publicación. No tiene default."
}
]
MediaItemSchema
Este subdocumento describe cada elemento de media. Los subdocumentos tienen _id: false, así que no generan identificador propio de MongoDB.
[
{
"name": "type",
"type": "string enum",
"description": "Tipo de medio. Valores: image, video, map. Default: image."
},
{
"name": "src",
"type": "string",
"required": true,
"description": "Origen principal del medio."
},
{
"name": "alt",
"type": "string",
"description": "Texto alternativo."
},
{
"name": "poster",
"type": "string",
"description": "Imagen poster, cuando aplica."
},
{
"name": "title",
"type": "string",
"description": "Título del medio."
},
{
"name": "provider",
"type": "string enum",
"description": "Proveedor del archivo. Valores: local, imagekit, external. Default: local."
},
{
"name": "fileId",
"type": "string",
"description": "Identificador interno del archivo."
},
{
"name": "name",
"type": "string",
"description": "Nombre original."
},
{
"name": "filePath",
"type": "string",
"description": "Ruta local o lógica del archivo."
},
{
"name": "url",
"type": "string",
"description": "URL pública o resoluble."
},
{
"name": "thumbnailUrl",
"type": "string",
"description": "URL de miniatura."
},
{
"name": "width",
"type": "number",
"description": "Ancho del recurso."
},
{
"name": "height",
"type": "number",
"description": "Alto del recurso."
},
{
"name": "size",
"type": "number",
"description": "Tamaño en bytes."
},
{
"name": "mime",
"type": "string",
"description": "Tipo MIME."
},
{
"name": "status",
"type": "string enum",
"description": "Estado del medio. Valores: active, deleted, error. Default: active."
},
{
"name": "uploadedAt",
"type": "date",
"description": "Fecha de subida."
}
]
PriceSchema y RequirementBudgetSchema
Estos dos esquemas comparten vocabulario de periodicidad. El archivo usa PRICE_PERIODICITY_VALUES para evitar que ambos enums se separen con el tiempo.
[
{
"name": "amount",
"type": "number",
"description": "Monto numérico."
},
{
"name": "currency",
"type": "string",
"description": "Código o texto de moneda. Default: MXN en `price`, vacío en `requirement.budget`."
},
{
"name": "text",
"type": "string",
"description": "Texto legible del importe."
},
{
"name": "periodicity",
"type": "string enum",
"description": "Valores compartidos: unspecified, day, week, month. Default: unspecified."
}
]
LocationSchema
Este subdocumento representa la ubicación de la oferta. También separa la declaración de ubicación de la confirmación de coordenadas.
[
{
"name": "country",
"type": "string",
"description": "País. Default: México."
},
{
"name": "state",
"type": "string",
"description": "Estado o entidad federativa."
},
{
"name": "city",
"type": "string",
"description": "Ciudad."
},
{
"name": "neighborhood",
"type": "string",
"description": "Colonia o barrio."
},
{
"name": "address",
"type": "string",
"description": "Dirección textual."
},
{
"name": "postalCode",
"type": "string",
"description": "Código postal."
},
{
"name": "latitude",
"type": "number",
"description": "Latitud confirmada o declarada."
},
{
"name": "longitude",
"type": "number",
"description": "Longitud confirmada o declarada."
},
{
"name": "googleMapsUrl",
"type": "string",
"description": "Enlace de Google Maps."
},
{
"name": "mapSnapshotUrl",
"type": "string",
"description": "Imagen estática del mapa."
},
{
"name": "declaration",
"type": "string enum",
"description": "Estado de declaración. Valores: unspecified, declared, not_declared. Default: unspecified."
}
]
DescriptionSchema
Este bloque guarda tres variantes de texto descriptivo. Todas son opcionales y recortan espacios.
[
{
"name": "short",
"type": "string",
"description": "Resumen breve."
},
{
"name": "full",
"type": "string",
"description": "Descripción extensa."
},
{
"name": "caption",
"type": "string",
"description": "Leyenda corta."
}
]
ContactSchema
Este esquema define la forma de contacto visible en el anuncio. El campo method limita el canal de contacto a valores conocidos.
[
{
"name": "name",
"type": "string",
"description": "Nombre del contacto."
},
{
"name": "phone",
"type": "string",
"description": "Teléfono de contacto."
},
{
"name": "method",
"type": "string enum",
"description": "Canal preferido. Valores: whatsapp, phone, email, telegram, internal. Default: whatsapp."
},
{
"name": "link",
"type": "string",
"description": "Enlace asociado al contacto."
},
{
"name": "telegramUsername",
"type": "string",
"description": "Usuario de Telegram."
},
{
"name": "message",
"type": "string",
"description": "Mensaje sugerido."
}
]
SourceDocumentSchema
Este subdocumento modela documentos fuente subidos por el anunciante, como fichas técnicas o brochures. Su _id sí existe y funciona como identificador público. El campo fileId es interno y no se expone públicamente.
[
{
"name": "fileId",
"type": "string",
"required": true,
"description": "Identificador interno del archivo."
},
{
"name": "url",
"type": "string",
"required": true,
"description": "URL del documento fuente."
},
{
"name": "name",
"type": "string",
"description": "Nombre visible del documento. Máximo 160 caracteres."
},
{
"name": "mimeType",
"type": "string enum",
"description": "Tipo MIME permitido. Solo application/pdf."
},
{
"name": "sizeBytes",
"type": "number",
"description": "Tamaño en bytes."
},
{
"name": "uploadedAt",
"type": "date",
"description": "Fecha de carga."
}
]
[!NOTE]
SourceDocumentSchemay
MetaSchema
meta concentra la clasificación principal del Listing. También contiene varios campos físicos y de operación que dependen del tipo de entidad.
[
{
"name": "entityType",
"type": "string enum",
"required": true,
"description": "Tipo de entidad. Valores: property, service, investment."
},
{
"name": "operationType",
"type": "string enum",
"required": true,
"description": "Operación del Listing. Incluye valores para ofertas y requerimientos."
},
{
"name": "propertyType",
"type": "string enum",
"description": "Tipo de propiedad. Default vacío."
},
{
"name": "serviceType",
"type": "string enum",
"description": "Tipo de servicio. Default vacío."
},
{
"name": "investmentType",
"type": "string enum",
"description": "Tipo de inversión. Default vacío."
},
{
"name": "bedrooms",
"type": "number",
"description": "Número de recámaras. Null significa no declarado."
},
{
"name": "bathrooms",
"type": "number",
"description": "Número de baños."
},
{
"name": "halfBathrooms",
"type": "number",
"description": "Número de medios baños."
},
{
"name": "parkingSpots",
"type": "number",
"description": "Cajones de estacionamiento."
},
{
"name": "constructionArea",
"type": "number",
"description": "Área construida."
},
{
"name": "landArea",
"type": "number",
"description": "Área de terreno."
},
{
"name": "maintenanceFee",
"type": "number",
"description": "Cuota de mantenimiento."
},
{
"name": "negotiable",
"type": "boolean",
"description": "Indica si el precio es negociable."
},
{
"name": "age",
"type": "number",
"description": "Antigüedad. Null significa no declarado."
},
{
"name": "frontage",
"type": "number",
"description": "Frente del lote en metros."
},
{
"name": "depth",
"type": "number",
"description": "Fondo aproximado del lote en metros."
},
{
"name": "coverImage",
"type": "string",
"description": "Imagen de portada."
},
{
"name": "amenities",
"type": "array of string",
"description": "Amenidades del lado Offer. Usa el mismo allowlist que Requirement.desiredAmenities."
},
{
"name": "waterDeclaration",
"type": "string enum",
"description": "Declaración de infraestructura hídrica. Valores: not_declared, declared_unverified."
},
{
"name": "electricityDeclaration",
"type": "string enum",
"description": "Declaración de infraestructura eléctrica. Valores: not_declared, declared_unverified."
},
{
"name": "drainageDeclaration",
"type": "string enum",
"description": "Declaración de drenaje. Valores: not_declared, declared_unverified."
},
{
"name": "minimumTermValue",
"type": "number",
"description": "Plazo mínimo de renta exigido por el anunciante. Debe ser mayor que 0 cuando existe."
},
{
"name": "minimumTermUnit",
"type": "string enum",
"description": "Unidad del plazo mínimo. Valores: day, week, month, year."
}
]
RequirementBudgetSchema
Este esquema modela el presupuesto de un Requirement. Es pequeño a propósito y no reutiliza campos de oferta con semánticas diferentes.
[
{
"name": "min",
"type": "number",
"description": "Valor mínimo esperado."
},
{
"name": "max",
"type": "number",
"description": "Valor máximo esperado."
},
{
"name": "currency",
"type": "string",
"description": "Moneda del presupuesto."
},
{
"name": "periodicity",
"type": "string enum",
"description": "Vocabulario compartido con Offer.price.periodicity. Default: unspecified."
}
]
RequirementSearchZoneSchema
Este subdocumento modela una zona aproximada de búsqueda. No guarda un domicilio real ni un lugar exacto del usuario.
[
{
"name": "label",
"type": "string",
"description": "Etiqueta geográfica elegida por el usuario."
},
{
"name": "country",
"type": "string",
"description": "País de la zona."
},
{
"name": "state",
"type": "string",
"description": "Estado de la zona."
},
{
"name": "city",
"type": "string",
"description": "Ciudad de la zona."
},
{
"name": "neighborhood",
"type": "string",
"description": "Colonia o barrio."
},
{
"name": "center",
"type": "object",
"description": "Centro aproximado de la zona.",
"children": [
{
"name": "lat",
"type": "number",
"description": "Latitud central."
},
{
"name": "lng",
"type": "number",
"description": "Longitud central."
}
]
},
{
"name": "radiusKm",
"type": "number",
"description": "Radio aproximado en kilómetros."
}
]
RequirementPreferencesSchema
Este esquema guarda preferencias comparables de la búsqueda. Cada llave usa solo required o preferred.
[
{
"name": "bedrooms",
"type": "string enum",
"description": "Preferencia sobre recámaras."
},
{
"name": "bathrooms",
"type": "string enum",
"description": "Preferencia sobre baños."
},
{
"name": "halfBathrooms",
"type": "string enum",
"description": "Preferencia sobre medios baños."
},
{
"name": "parkingSpots",
"type": "string enum",
"description": "Preferencia sobre cajones."
},
{
"name": "constructionArea",
"type": "string enum",
"description": "Preferencia sobre área construida."
},
{
"name": "landArea",
"type": "string enum",
"description": "Preferencia sobre área de terreno."
},
{
"name": "frontage",
"type": "string enum",
"description": "Preferencia sobre frente."
},
{
"name": "depth",
"type": "string enum",
"description": "Preferencia sobre fondo."
},
{
"name": "water",
"type": "string enum",
"description": "Preferencia sobre agua declarada."
},
{
"name": "electricity",
"type": "string enum",
"description": "Preferencia sobre electricidad declarada."
},
{
"name": "drainage",
"type": "string enum",
"description": "Preferencia sobre drenaje declarado."
},
{
"name": "term",
"type": "string enum",
"description": "Preferencia sobre plazo."
}
]
RequirementReferralSchema
Este esquema representa un Requirement de tipo referido. Conserva solo la asociación necesaria y un resumen seguro.
[
{
"name": "target",
"type": "string enum",
"required": true,
"description": "Destino del referido."
},
{
"name": "compensationCondition",
"type": "string enum",
"description": "Condición de compensación. Default: unspecified."
},
{
"name": "relatedListingId",
"type": "objectId",
"description": "Referencia al Listing relacionado."
},
{
"name": "relatedListingUnpublishedAt",
"type": "date",
"description": "Fecha en que el Listing relacionado dejó de estar publicado."
}
]
RequirementSchema
Este subdocumento agrupa lo que el usuario busca. Incluye presupuesto, preferencias, zonas y límites de plazo.
[
{
"name": "budget",
"type": "object",
"description": "Presupuesto del Requirement. Usa RequirementBudgetSchema."
},
{
"name": "desiredBy",
"type": "date",
"description": "Fecha deseada."
},
{
"name": "hasPets",
"type": "boolean",
"description": "Indica si el buscador tiene mascota."
},
{
"name": "petFriendlyRequired",
"type": "boolean",
"description": "Indica si aceptar mascotas es una condición."
},
{
"name": "desiredAmenities",
"type": "array of string",
"description": "Amenidades deseadas. Usa el mismo allowlist que meta.amenities."
},
{
"name": "legalEntityRequirement",
"type": "string enum",
"description": "Requisito sobre entidad legal de la contraparte."
},
{
"name": "locationFlexibility",
"type": "string enum",
"description": "Flexibilidad de ubicación. Valores: strict, flexible, any, unspecified."
},
{
"name": "searchZones",
"type": "array",
"description": "Zonas aproximadas de búsqueda. Usa RequirementSearchZoneSchema."
},
{
"name": "referral",
"type": "object",
"description": "Referencia segura a un Listing relacionado. Usa RequirementReferralSchema."
},
{
"name": "preferences",
"type": "object",
"description": "Preferencias comparables. Usa RequirementPreferencesSchema."
},
{
"name": "maximumAcceptedTermValue",
"type": "number",
"description": "Techo de plazo aceptable para la búsqueda. Debe ser mayor que 0 cuando existe."
},
{
"name": "maximumAcceptedTermUnit",
"type": "string enum",
"description": "Unidad del plazo máximo aceptable. Valores: day, week, month, year."
}
]
PublicationSchema
Este bloque describe el estado editorial del Listing. No debe confundirse con el lifecycle de cierre de cuenta.
[
{
"name": "status",
"type": "string enum",
"description": "Estado editorial. Valores: draft, published, archived. Default: published."
},
{
"name": "verified",
"type": "boolean",
"description": "Marca de verificación editorial."
},
{
"name": "role",
"type": "string enum",
"description": "Rol del publicador. Valores: inmobiliaria, propietario, agente."
}
]
LifecycleSchema
Este bloque gestiona la protección, anonimización y purga asociadas al cierre de cuenta. Guarda también el estado editorial previo cuando entra en protección.
[
{
"name": "status",
"type": "string enum",
"description": "Estado del lifecycle. Valores: normal, protection, anonymized, purged. Default: normal."
},
{
"name": "previousPublicationStatus",
"type": "string enum",
"description": "Estado editorial previo guardado al entrar en protection."
},
{
"name": "protectionStartedAt",
"type": "date",
"description": "Fecha de inicio de la protección."
},
{
"name": "anonymizedAt",
"type": "date",
"description": "Fecha de anonimización."
},
{
"name": "purgeScheduledAt",
"type": "date",
"description": "Fecha programada de purga."
},
{
"name": "purgedAt",
"type": "date",
"description": "Fecha real de purga."
},
{
"name": "publicOwnerLabel",
"type": "string",
"description": "Etiqueta pública del titular."
}
]
DeletionSchema
Este subdocumento solo existe mientras la eliminación recuperable está pendiente. No tiene default, así que los documentos históricos pueden no incluirlo.
[
{
"name": "status",
"type": "string enum",
"required": true,
"description": "Estado de eliminación. Solo admite pending."
},
{
"name": "requestedAt",
"type": "date",
"required": true,
"description": "Fecha de solicitud."
},
{
"name": "attempts",
"type": "number",
"description": "Cantidad de intentos. Default: 1."
}
]
ConversationReservationSchema
Este subdocumento guarda una reserva interna entre Conversation y Requirement. El servidor de MongoDB calcula expiresAt.
[
{
"name": "conversationId",
"type": "objectId",
"required": true,
"description": "Identificador de Conversation."
},
{
"name": "token",
"type": "objectId",
"required": true,
"description": "Token de reserva."
},
{
"name": "expiresAt",
"type": "date",
"required": true,
"description": "Fecha de expiración de la reserva."
}
]
metrics
Este bloque registra interacción agregada sobre el Listing. Todos los contadores comienzan en cero.
[
{
"name": "views",
"type": "number",
"description": "Vistas totales."
},
{
"name": "detailOpens",
"type": "number",
"description": "Aperturas de detalle."
},
{
"name": "likes",
"type": "number",
"description": "Likes."
},
{
"name": "comments",
"type": "number",
"description": "Comentarios."
},
{
"name": "contactClicks",
"type": "number",
"description": "Clics de contacto."
},
{
"name": "pdfDownloads",
"type": "number",
"description": "Descargas de PDF."
},
{
"name": "qrScans",
"type": "number",
"description": "Escaneos de QR."
},
{
"name": "trendScore",
"type": "number",
"description": "Puntaje de tendencia."
},
{
"name": "sentimentSummary",
"type": "object",
"description": "Resumen de sentimiento.",
"children": [
{
"name": "positive",
"type": "number",
"description": "Conteo positivo."
},
{
"name": "neutral",
"type": "number",
"description": "Conteo neutral."
},
{
"name": "negative",
"type": "number",
"description": "Conteo negativo."
},
{
"name": "dominant",
"type": "string enum",
"description": "Sentimiento dominante. Valores: positive, neutral, negative, none."
}
]
},
{
"name": "lastInteractionAt",
"type": "date",
"description": "Fecha de la última interacción."
}
]
Behaviors
El archivo no define métodos de negocio propios. Su comportamiento está en la definición del esquema, en sus defaults y en sus índices.
mongoose.model("Listing", ListingSchema)registra el modelo y lo exporta comoListing.timestamps: trueagrega marcas automáticas de creación y actualización.intentusaimmutable: true, así que el valor no cambia después de crearse.metaes obligatorio en toda instancia deListing.requirementexiste como objeto por defecto, aunque solo tenga sentido paraintent = "requirement".deletionno tiene default, así que los documentos históricos pueden no tenerlo.mediaRevisiontampoco tiene default, porque es una versión controlada por el servidor.conversationReservationsusaselect: false, así que no aparece en consultas normales.SourceDocumentSchemano define{ _id: false }, por lo que su_idactúa como identificador público.fileIdenSourceDocumentSchemaes interno, según el comentario del archivo.publication.statusylifecycle.statusse mantienen separados por diseño.
{"lifecycle.status": 1, "lifecycle.purgeScheduledAt": 1}soporta consultas del ciclo de purga.{"createdBy": 1}acelera la lectura de todos los listings de un usuario.{"createdBy": 1, "intent": 1, "publication.status": 1}soporta el conteo de cuota por intención.{"createdBy": 1, "submissionId": 1}conunique: trueysparse: trueevita duplicados de idempotencia.
minimumTermValueymaximumAcceptedTermValuesolo aceptan valores mayores que 0 cuando existen.- La coherencia entre valor y unidad se valida fuera del schema, en
validateListingPayload(). amenitiesydesiredAmenitiescomparten allowlist, pero no comparten el mismo campo.waterDeclaration,electricityDeclarationydrainageDeclarationsolo guardan declaración del anunciante.location.declarationusaunspecified,declaredynot_declared.not_applicableno se persiste enlocation.declaration.
Workflows
1. Crear el documento | `title` y `meta` son obligatorios. El resto usa defaults o subdocumentos vacíos.
2. Clasificar la intención | `intent` separa `offer` y `requirement`. El valor queda fijo después de crearse.
3. Persistir relaciones internas | `createdBy`, `submissionId`, `mediaRevision` y `conversationReservations` soportan ownership, idempotencia y control interno.
4. Consultar y serializar | Los campos internos se ocultan según el selector del schema y el comportamiento de lectura de la capa superior.
Gotchas & edge cases
[!WARNING]
SourceDocumentSchemano es lo mismo que
[!WARNING]
intentes inmutable. Una vez creado el documento, el valor no debe cambiar.
[!NOTE]
conversationReservationses un campo interno. El schema lo oculta por defecto conselect: false.
[!IMPORTANT]
submissionIdusa un índice único y disperso. Los documentos sinsubmissionIdpueden coexistir. Los documentos con el mismosubmissionIdycreatedByno pueden duplicarse.
[!CAUTION]
mediaRevisionpertenece al servidor. El comentario del archivo indica que un PATCH con lectura obsoleta puede terminar en conflicto.
[!NOTE]
publication.statusno expresa el cierre de cuenta. Ese ciclo vive enlifecycle.status.
Related
Este modelo se conecta con varias piezas que el archivo menciona en sus comentarios.
listings.validators.jsdefine reglas cruzadas, allowlists y vocabulario compartido.listings.routes.jsaplica restricciones comoINTENT_IMMUTABLE, cuotas y conflictos de edición.listing-write-sanitizer.jsexcluye campos internos de los datos escritos por el cliente.listing-public-view.jsexcluye campos internos de la vista pública.docs/03-architecture/data-models.mddocumenta el marco arquitectónico que estos comentarios referencian.publish-wizard/lib/generateSubmissionId.jsgenerasubmissionIdpara idempotencia de publicación.
[!NOTE] El archivo también referencia una subruta owner-only para documentos fuente. Esa referencia explica el uso de
SourceDocumentSchema, pero la lógica no vive aquí.