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
partialsolo 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 trastrim.intent[badge:optional]string— si viene, solo aceptaofferorequirement.description.full[badge:optional]string— solo se limita cuandointentesrequirement.
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 solopayload.intent.REQUIREMENT_TITLE_MAX_LENGTH—90.REQUIREMENT_DESCRIPTION_MAX_WORDS—250.
[!WARNING]
resolveIntentno consulta el documento existente. En unPATCHparcial sinintent, la validación parte deoffer.
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 a0.max[badge:optional]number— debe ser finito y mayor o igual a0.currency[badge:optional]string— si existe y no es vacío, debe ser texto.periodicity[badge:optional]string— debe pertenecer aPERIODICITY_VALUES.
Además, min no puede ser mayor que max cuando ambos existen.
maximumAcceptedTermValue[badge:optional]number— debe ser mayor que0.maximumAcceptedTermUnit[badge:optional]string— debe pertenecer aRENTAL_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— incluyebedrooms,bathrooms,halfBathrooms,parkingSpots,constructionArea,landArea,frontage,depth,water,electricity,drainage,term.REQUIREMENT_PREFERENCE_VALUES— solorequiredypreferred.
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 | nullpetFriendlyRequired[badge:optional]boolean | nulldesiredAmenities[badge:optional]string[]legalEntityRequirement[badge:optional]stringlocationFlexibility[badge:optional]stringdesiredBy[badge:optional]date stringreferral[badge:optional]objectsearchZones[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 aREFERRAL_TARGET_VALUES.compensationCondition[badge:required]string— debe pertenecer aREFERRAL_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áximo3zonas.label[badge:required]stringcenter.lat[badge:required]number— entre-90y90.center.lng[badge:required]number— entre-180y180.radiusKm[badge:required]number— mayor que0y menor o igual que100.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,serviceoinvestment.operationType[badge:required]string— debe pertenecer aOPERATION_TYPES.propertyType[badge:required when entityType=property]stringserviceType[badge:required when entityType=service]stringinvestmentType[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.
bedroomsbathroomshalfBathroomsparkingSpotsconstructionArealandAreamaintenanceFeeagefrontagedepth
Todos aceptan solo números finitos mayores o iguales a 0 cuando aparecen.
amenities[badge:optional]string[]— usaAMENITIES_ALLOWLIST.waterDeclaration[badge:optional]stringelectricityDeclaration[badge:optional]stringdrainageDeclaration[badge:optional]stringminimumTermValue[badge:optional]number— debe ser mayor que0.minimumTermUnit[badge:optional]string— debe pertenecer aRENTAL_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 a0.price.periodicity[badge:optional]string— debe pertenecer aPERIODICITY_VALUES.contact.method[badge:optional]string— debe pertenecer aCONTACT_METHODS.publication.status[badge:optional]string— debe pertenecer aPUBLICATION_STATUS.publication.role[badge:optional]string— debe pertenecer aPUBLICATION_ROLES.media[badge:optional]array— cada elemento debe ser un objeto consrcno vacío.location.latitude[badge:optional]numberlocation.longitude[badge:optional]numberlocation.declaration[badge:optional]string— debe pertenecer aLOCATION_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)— devuelvetruecuandovaluees un número finito.isNonEmptyString(value)— devuelvetruecuandovaluees texto no vacío trastrim.countWords(value)— cuenta palabras separadas por espacios en blanco y omite vacíos.resolveIntent(payload)— devuelverequirementsolo sipayload.intentvale exactamenterequirement; en otro caso devuelveoffer.validateListingPayload(payload = {}, { partial = false } = {})— valida todo el payload y devuelve{ valid, errors }.
Gotchas & edge cases ⚠️
[!WARNING]
meta.entityTypecae apropertycuando no viene en el payload. Esa decisión cambia qué campo se vuelve obligatorio.
[!WARNING]
validateListingPayloadno infiereprice.periodicitynirequirement.budget.periodicitydesdeoperationType. Solo valida el valor explícito recibido.
[!CAUTION]
requirement.referralsolo es válido en unrequirementdeserviceconserviceType=referral. Fuera de esa combinación, se rechaza.
[!IMPORTANT]
requirement.searchZonespermite como máximo tres zonas. Cada zona también queda limitada a un radio de100km.
[!NOTE]
RENTAL_OPERATION_TYPESexiste como vocabulario compartido. El validador no lo usa para bloquear campos.
[!NOTE]
MEDIA_CONFIG.maxImagesPerListingcontrola el máximo de imágenes por anuncio. El archivo solo consume ese límite.
Related
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 derequirement.