Validador de payloads de listings

Overview 🔎

services/api/src/modules/listings/listings.validators.js define la validación determinista del payload de un listing. Su función pública, validateListingPayload, revisa altas completas y ediciones parciales, y devuelve un resultado binario con una lista de errores. En el contexto del módulo de listings, este archivo actúa como filtro previo antes de persistir o actualizar datos del anuncio.

La validación cubre campos de negocio, metadatos, contacto, publicación, medios, ubicación y reglas específicas de requirement. También aplica restricciones coherentes entre intent, meta.entityType y meta.operationType. El archivo exporta una sola función pública y mantiene el resto de helpers como implementación interna.

[!NOTE] El modo partial solo valida los campos presentes. Si un campo no viene en el payload, no se exige en esa llamada.

How it works

flowchart TD
    P[payload]
    O[partial]
    V[validateListingPayload]
    I[resolveIntent]
    T[title and description]
    R[requirement]
    M[meta]
    S[price contact publication media location]
    E[errors]
    X[result valid and errors]
    C[MEDIA_CONFIG maxImagesPerListing]

    P --> V
    O --> V
    V --> I
    I --> T
    I --> R
    I --> M
    V --> S
    C --> S
    T --> E
    R --> E
    M --> E
    S --> E
    E --> X

La función crea un arreglo errors vacío y valida cada bloque del payload por separado. No corta la ejecución al primer fallo. En su lugar, acumula todos los mensajes detectados y devuelve el conjunto completo al final. Eso facilita mostrar varios errores en una sola respuesta.

La intención se resuelve con resolveIntent(payload). Esa resolución depende solo del payload actual. Después, la validación aplica reglas generales y reglas dependientes de intent. Si intent es requirement, algunas restricciones textuales se vuelven más estrictas.

Key components

{
    "title": "validateListingPayload",
    "content": "Valida un payload de listing y devuelve { valid, errors }.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listings.validators.js",
    "badges": [
        "public"
    ]
}

Data & fields

Entrada principal

[
    {
        "name": "payload",
        "type": "object",
        "required": true,
        "description": "Datos del listing a validar."
    },
    {
        "name": "options.partial",
        "type": "boolean",
        "description": "Activa validación parcial. Solo se revisan los campos presentes."
    }
]

Campos compartidos de nivel superior

El validador revisa campos que pueden aparecer en varias ramas del documento. Algunos son obligatorios solo en ciertos contextos. Otros aceptan null como valor válido de estado.

  • title [badge:required] string — debe existir y no puede quedar vacío tras trim.
  • intent [badge:optional] string — si viene, solo acepta offer o requirement.
  • description.full [badge:optional] string — solo se limita cuando intent es requirement.

intent y reglas base

intent se resuelve con resolveIntent(payload). El valor requirement activa reglas más estrictas sobre texto y requirement. Cualquier otro valor, o la ausencia del campo, se trata como offer.

  • INTENT_VALUES — offer, requirement.
  • resolveIntent — usa solo payload.intent.
  • REQUIREMENT_TITLE_MAX_LENGTH — 90.
  • REQUIREMENT_DESCRIPTION_MAX_WORDS — 250.

[!WARNING] resolveIntent no consulta el documento existente. En un PATCH parcial sin intent, la validación parte de offer.

requirement

El objeto requirement contiene restricciones de búsqueda, preferencias y la lógica del caso de referido. El validador exige que sea un objeto cuando aparece en el payload.

  • min [badge:optional] number — debe ser finito y mayor o igual a 0.
  • max [badge:optional] number — debe ser finito y mayor o igual a 0.
  • currency [badge:optional] string — si existe y no es vacío, debe ser texto.
  • periodicity [badge:optional] string — debe pertenecer a PERIODICITY_VALUES.

Además, min no puede ser mayor que max cuando ambos existen.

  • maximumAcceptedTermValue [badge:optional] number — debe ser mayor que 0.
  • maximumAcceptedTermUnit [badge:optional] string — debe pertenecer a RENTAL_TERM_UNIT_VALUES.
  • Ambos campos deben declararse juntos o ninguno.

Este par usa un vocabulario propio. No comparte enum con PERIODICITY_VALUES.

  • preferences [badge:optional] object — solo acepta llaves cerradas.
  • REQUIREMENT_PREFERENCE_KEYS — incluye bedrooms, bathrooms, halfBathrooms, parkingSpots, constructionArea, landArea, frontage, depth, water, electricity, drainage, term.
  • REQUIREMENT_PREFERENCE_VALUES — solo required y preferred.

Las llaves desconocidas se rechazan. Los valores fuera de ese par también se rechazan. Las llaves ausentes no se inventan.

  • hasPets [badge:optional] boolean | null
  • petFriendlyRequired [badge:optional] boolean | null
  • desiredAmenities [badge:optional] string[]
  • legalEntityRequirement [badge:optional] string
  • locationFlexibility [badge:optional] string
  • desiredBy [badge:optional] date string
  • referral [badge:optional] object
  • searchZones [badge:optional] array

hasPets y petFriendlyRequired aceptan true, false o null. desiredBy debe convertirse en una fecha válida.

  • target [badge:required] string — debe pertenecer a REFERRAL_TARGET_VALUES.
  • compensationCondition [badge:required] string — debe pertenecer a REFERRAL_COMPENSATION_VALUES.
  • relatedListingId [badge:optional] string | null — si no es vacío, debe tener forma de ObjectId.

La presencia de referral solo es válida para un servicio de referido.

  • searchZones [badge:optional] array — máximo 3 zonas.
  • label [badge:required] string
  • center.lat [badge:required] number — entre -90 y 90.
  • center.lng [badge:required] number — entre -180 y 180.
  • radiusKm [badge:required] number — mayor que 0 y menor o igual que 100.
  • country, state, city, neighborhood [badge:optional] string

El validador detecta duplicados exactos por lat, lng y radiusKm.

meta

meta concentra la clasificación del listing y varios atributos físicos o declarativos. Si no llega entityType, el validador usa property como valor por defecto.

  • entityType [badge:optional] string — property, service o investment.
  • operationType [badge:required] string — debe pertenecer a OPERATION_TYPES.
  • propertyType [badge:required when entityType=property] string
  • serviceType [badge:required when entityType=service] string
  • investmentType [badge:required when entityType=investment] string

operationType también debe ser coherente con entityType e intent. El archivo define esa coherencia en OPERATION_TYPES_BY_ENTITY_AND_INTENT.

  • bedrooms
  • bathrooms
  • halfBathrooms
  • parkingSpots
  • constructionArea
  • landArea
  • maintenanceFee
  • age
  • frontage
  • depth

Todos aceptan solo números finitos mayores o iguales a 0 cuando aparecen.

  • amenities [badge:optional] string[] — usa AMENITIES_ALLOWLIST.
  • waterDeclaration [badge:optional] string
  • electricityDeclaration [badge:optional] string
  • drainageDeclaration [badge:optional] string
  • minimumTermValue [badge:optional] number — debe ser mayor que 0.
  • minimumTermUnit [badge:optional] string — debe pertenecer a RENTAL_TERM_UNIT_VALUES.

Las declaraciones de infraestructura solo aceptan not_declared y declared_unverified. No expresan verificación real.

Otros bloques validados

  • price.amount [badge:optional] number — debe ser mayor o igual a 0.
  • price.periodicity [badge:optional] string — debe pertenecer a PERIODICITY_VALUES.
  • contact.method [badge:optional] string — debe pertenecer a CONTACT_METHODS.
  • publication.status [badge:optional] string — debe pertenecer a PUBLICATION_STATUS.
  • publication.role [badge:optional] string — debe pertenecer a PUBLICATION_ROLES.
  • media [badge:optional] array — cada elemento debe ser un objeto con src no vacío.
  • location.latitude [badge:optional] number
  • location.longitude [badge:optional] number
  • location.declaration [badge:optional] string — debe pertenecer a LOCATION_DECLARATION_VALUES.

Vocabulario cerrado del módulo

El archivo declara listas cerradas para mantener consistencia entre validación y consumo posterior.

  • ENTITY_TYPES — property, service, investment.
  • OPERATION_TYPES — sale, rent, temporary_rent, presale, investment_offer, service_offer, exchange, auction, transfer, service_request, investment_request.
  • AMENITIES_ALLOWLIST — pool, gym, event_room, elevator, security, green_areas, roof_garden, visitor_parking, storage, pet_friendly_common_area.
  • LEGAL_ENTITY_REQUIREMENT_VALUES — required, preferred, not_required, unspecified.
  • LOCATION_FLEXIBILITY_VALUES — strict, flexible, any, unspecified.
  • LOCATION_DECLARATION_VALUES — unspecified, declared, not_declared.
  • PERIODICITY_VALUES — unspecified, day, week, month.
  • INFRASTRUCTURE_DECLARATION_VALUES — not_declared, declared_unverified.
  • RENTAL_TERM_UNIT_VALUES — day, week, month, year.
  • RENTAL_OPERATION_TYPES — rent, temporary_rent.
  • PROPERTY_TYPES — house, apartment, land, office, warehouse, commercial, industrial, building, other.
  • SERVICE_TYPES — legal, notarial, architecture, construction, structural_steel, construction_engineering, insurance, mortgage, financial_advisory, valuation, property_management, photography, referral, other_service.
  • REFERRAL_TARGET_VALUES — tenant, buyer, franchisee, business, investor, provider, other.
  • REFERRAL_COMPENSATION_VALUES — on_close, negotiable, none, unspecified.
  • INVESTMENT_TYPES — fibra, fractional, development.
  • CONTACT_METHODS — whatsapp, phone, email, telegram, internal.
  • PUBLICATION_STATUS — draft, published, archived.
  • PUBLICATION_ROLES — inmobiliaria, propietario, agente.
  • MEDIA_TYPES — image, video, map.

Behaviors

  • isFiniteNumber(value) — devuelve true cuando value es un número finito.
  • isNonEmptyString(value) — devuelve true cuando value es texto no vacío tras trim.
  • countWords(value) — cuenta palabras separadas por espacios en blanco y omite vacíos.
  • resolveIntent(payload) — devuelve requirement solo si payload.intent vale exactamente requirement; en otro caso devuelve offer.
  • validateListingPayload(payload = {}, { partial = false } = {}) — valida todo el payload y devuelve { valid, errors }.

Gotchas & edge cases ⚠️

[!WARNING] meta.entityType cae a property cuando no viene en el payload. Esa decisión cambia qué campo se vuelve obligatorio.

[!WARNING] validateListingPayload no infiere price.periodicity ni requirement.budget.periodicity desde operationType. Solo valida el valor explícito recibido.

[!CAUTION] requirement.referral solo es válido en un requirement de service con serviceType=referral. Fuera de esa combinación, se rechaza.

[!IMPORTANT] requirement.searchZones permite como máximo tres zonas. Cada zona también queda limitada a un radio de 100 km.

[!NOTE] RENTAL_OPERATION_TYPES existe como vocabulario compartido. El validador no lo usa para bloquear campos.

[!NOTE] MEDIA_CONFIG.maxImagesPerListing controla el máximo de imágenes por anuncio. El archivo solo consume ese límite.

services/api/src/modules/listings/listings.routes.js importa validateListingPayload y lo usa como filtro antes de crear o editar listings. Ese archivo también se apoya en este contrato para las rutas POST y PATCH.

El archivo también depende de ../../config/media.js para leer MEDIA_CONFIG.maxImagesPerListing. Esa dependencia mantiene el límite de imágenes fuera del validador mismo.

[!NOTE] En las pruebas del dominio de listings, este validador se consume directamente para validar coherencia entre intent, meta, límites de texto y reglas de requirement.