candidate-generation.js
Overview
services/api/src/modules/match/candidate-generation.js construye el contrato determinista de un candidato entre un Requirement y un Offer. El archivo orquesta comparadores puros, pero no hace scoring, ranking, ni persistencia. Tampoco crea Lead, ListingEvent, Activity, Notification, Email, ni Campaign, y no toca Mongo; esa frontera queda en candidate-retrieval.js. fileciteturn0file0turn0file7turn0file12
Este módulo también integra la fase M2.2 de geografía por radio. location se trata como un gate, no como una preferencia, y usa la misma semántica de pending_gate que propertyType cuando el dato no se puede resolver. La geografía textual sigue fuera de alcance. fileciteturn0file0turn0file8
[!NOTE] La dirección canónica aquí es
Requirementcomo sujeto yOffercomo candidato. El archivo no comparaOffercontraOffer, niRequirementcontraRequirement. fileciteturn0file0turn0file7
How it works
El flujo empieza con la compatibilidad estructural del par. Después, el módulo construye señales para precio, plazo, propertyType, ubicación, campos físicos, infraestructura y amenidades. Al final, deriva blocked, pending o eligible desde los buckets de señales y la elegibilidad previa. fileciteturn0file7turn0file8
flowchart TB
Req[Requirement] --> Eval[evaluateCandidatePair]
Off[Offer] --> Eval
Eval --> Pair[getListingPairMatchCompatibility]
Eval --> Type[computePropertyType]
Type --> Gate[buildPropertyTypeSignal]
Eval --> Price[buildPriceSignal]
Eval --> Term[buildTermSignal]
Eval --> Location[buildLocationSignal]
Type --> Physical[buildPhysicalSignals]
Type --> Infra[buildInfrastructureSignals]
Eval --> Amenities[buildAmenitiesSignal]
Pair --> Status[blocked pending eligible]
Gate --> Status
Price --> Status
Term --> Status
Location --> Status
Physical --> Status
Infra --> Status
Amenities --> Status
Primero se resuelve la elegibilidad y compatibilidad del par. Luego se generan señales individuales con comparadores puros. Finalmente, el módulo agrupa esas señales en buckets y calcula el estado agregado del candidato. Esa salida es channel-agnostic y no contiene score, rank, ni porcentaje. fileciteturn0file0turn0file7
Key components
La función pública del archivo es una sola. El resto de helpers son internos y sirven para normalizar ids, construir señales y aplicar gates de dominio.
{
"title": "evaluateCandidatePair",
"content": "Orquesta la compatibilidad del par y construye el contrato determinista del candidato. Devuelve elegibilidad, compatibilidad del par, se\u00f1ales y buckets derivados.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-generation.js",
"badges": [
"public"
]
}
Data & fields
El archivo trabaja sobre listings ya resueltos por el caller. Las estructuras de entrada usan meta, requirement, price, location y, según el caso, createdBy. La salida principal es un objeto de candidato con ids normalizados, estado agregado, elegibilidad y buckets de señales.
[
{
"name": "requirement",
"type": "Listing",
"required": true,
"description": "Listing con intent requirement. Es el lado sujeto del match."
},
{
"name": "offer",
"type": "Listing",
"required": true,
"description": "Listing con intent offer. Es el lado candidato del match."
},
{
"name": "context",
"type": "object",
"description": "Estados de cuenta opcionales usados por la elegibilidad de ambos lados.",
"children": [
{
"name": "requirementOwnerAccountStatus",
"type": "string",
"description": "Estado de cuenta del dueño del Requirement."
},
{
"name": "offerOwnerAccountStatus",
"type": "string",
"description": "Estado de cuenta del dueño del Offer."
}
]
}
]
Constantes internas
PHYSICAL_FIELDS[badge:read-only]string[]— Los ocho campos físicos comparables:bedrooms,bathrooms,halfBathrooms,parkingSpots,constructionArea,landArea,frontageydepth.INFRASTRUCTURE_FIELDS[badge:read-only]Array<object>— Mapeawater,electricityydrainagea sus llaves de declaración enmeta.RENTAL_OPERATION_TYPES[badge:read-only]string[]—rentytemporary_rent.
Resultado de evaluateCandidatePair
[
{
"name": "requirementId",
"type": "string | null",
"required": true,
"description": "Identificador normalizado del Requirement."
},
{
"name": "offerId",
"type": "string | null",
"required": true,
"description": "Identificador normalizado del Offer."
},
{
"name": "status",
"type": "eligible | pending | blocked",
"required": true,
"description": "Estado agregado del candidato."
},
{
"name": "eligibility",
"type": "object",
"required": true,
"description": "Resultado de elegibilidad por lado.",
"children": [
{
"name": "requirement",
"type": "object",
"required": true,
"description": "Elegibilidad del Requirement."
},
{
"name": "offer",
"type": "object",
"required": true,
"description": "Elegibilidad del Offer."
}
]
},
{
"name": "pairCompatibility",
"type": "object",
"required": true,
"description": "Compatibilidad estructural del par.",
"children": [
{
"name": "compatible",
"type": "boolean",
"required": true,
"description": "Indica si el par pasa los gates estructurales."
},
{
"name": "reasons",
"type": "string[]",
"required": true,
"description": "Razones acumuladas por incompatibilidad."
}
]
},
{
"name": "signals",
"type": "object[]",
"required": true,
"description": "Señales evaluadas para precio, plazo, gates, campos físicos, infraestructura y amenidades."
},
{
"name": "blockers",
"type": "object[]",
"required": true,
"description": "Señales con outcome fail."
},
{
"name": "pendingRequired",
"type": "object[]",
"required": true,
"description": "Criterios con outcome pending_unknown."
},
{
"name": "pendingGates",
"type": "object[]",
"required": true,
"description": "Gates con outcome pending_gate."
},
{
"name": "unknowns",
"type": "object[]",
"required": true,
"description": "Señales con compatibilidad unknown que no son pending_unknown ni pending_gate."
},
{
"name": "notApplicable",
"type": "object[]",
"required": true,
"description": "Señales con compatibilidad not_applicable."
}
]
Señal individual
Cada señal comparte el mismo sobre de datos. level puede ser nulo en gates y amenidades. evidence viaja solo cuando un comparador la produce.
[
{
"name": "field",
"type": "string",
"required": true,
"description": "Nombre lógico del criterio evaluado."
},
{
"name": "level",
"type": "string | null",
"required": true,
"description": "Nivel de preferencia o null en gates y señales informativas."
},
{
"name": "compatibility",
"type": "compatible | incompatible | not_applicable | unknown",
"required": true,
"description": "Resultado crudo del comparador."
},
{
"name": "outcome",
"type": "pass | fail | excluded | pending_unknown | pending_gate",
"required": true,
"description": "Resultado normalizado para el contrato de Match."
},
{
"name": "hardFail",
"type": "boolean",
"required": true,
"description": "Indica si la señal bloquea el candidato."
},
{
"name": "contributesToRanking",
"type": "boolean",
"required": true,
"description": "Este módulo no ordena ni puntúa, pero conserva esta bandera del contrato común."
},
{
"name": "requirementValue",
"type": "any",
"required": true,
"description": "Valor crudo del Requirement usado por el comparador."
},
{
"name": "offerValue",
"type": "any",
"required": true,
"description": "Valor crudo del Offer usado por el comparador."
},
{
"name": "reason",
"type": "string",
"required": true,
"description": "Código explicativo devuelto por el comparador o por la capa de gate."
},
{
"name": "evidence",
"type": "any",
"description": "Evidencia estructurada cuando el comparador la produce."
}
]
Behaviors
isRentalOperationType— Devuelvetruepararentytemporary_rent.resolveRawListingId— Devuelve el_idcrudo del listing onull.resolveRawOwnerId— DevuelvecreatedBycomo está, o_idsi el owner está poblado sin.equals.idsAreEqual— Compara ids con.equalscuando existe, y usaString()como respaldo.buildPreferenceSignal— EnvuelveevaluatePreferenceSignaly añade valores, razón y evidencia.buildGateSignal— Normaliza gates enfail,excluded,pending_gateopass.computePropertyType— ResuelveentityTypeypropertyType, luego llamacomparePropertyType.buildPropertyTypeSignal— Convierte el resultado decomparePropertyTypeen una señal de gate.resolvePhysicalApplicability— Determina si un campo físico o de infraestructura puede evaluarse.buildPhysicalFieldSignal— Evalúa un umbral mínimo físico contra el valor del Offer.buildPhysicalSignals— Construye las ocho señales físicas en orden fijo.buildInfrastructureFieldSignal— Evalúa una declaración de infraestructura para un campo específico.buildInfrastructureSignals— Construye las tres señales de infraestructura.buildAmenitiesSignal— ComparadesiredAmenitiesconmeta.amenitiesy conserva evidencia estructurada.buildLocationSignal— EvalúasearchZonescontralocation.latitudeylocation.longitude.mapPriceResultToCompatibility— Reduceincompatibleeincompatible_unita la misma compatibilidad de bloqueo.buildPriceSignal— Evalúa presupuesto solo si el Requirement declaróminomax.buildTermSignal— Evalúa plazo de renta solo cuando alguno de los lados es rental.evaluateCandidatePair[badge:public] — Orquesta elegibilidad, gates, señales y buckets del candidato final.
Workflows
1. Validar el par | `evaluateCandidatePair` llama primero a `getListingPairMatchCompatibility` y conserva sus razones.
2. Bloquear o continuar | Si hay incompatibilidad estructural, self-match o mismo dueño, el candidato queda bloqueado.
3. Construir señales | El módulo evalúa price, term, location, propertyType, campos físicos, infraestructura y amenidades.
4. Clasificar el resultado | `blocked` gana sobre todo; si no hay bloqueos, `pending` depende de `pendingRequired` y `pendingGates`; en otro caso queda `eligible`.
Gotchas & edge cases
[!WARNING]
propertyTypeylocationson gates, no preferencias. Cuando no pueden resolverse, el resultado cae enpending_gate, no en una señal informativa común.
[!WARNING] El módulo no infiere
not_applicabledesde valoresnullo ausentes. Primero resuelve aplicabilidad confield-applicability.js. Luego compara el valor del campo. fileciteturn0file0turn0file16
[!NOTE] Si
budget.minybudget.maxestán ausentes,pricese marca comonot_applicable. El requisito no está pidiendo precio en ese caso.
[!NOTE] Si la operación no es de renta,
termse marca comonot_applicable. El archivo no fabrica incertidumbre de plazo para ventas o inversiones. fileciteturn0file0turn0file7
[!CAUTION] La comparación de ids usa
.equalscuando existe. Esto evita errores con instancias distintas deObjectIdque representan el mismo documento.
[!WARNING]
evaluateCandidatePairrefuerza la regla de mismo dueño y la de self-comparison, aunque la capa de retrieval ya las filtre antes. fileciteturn0file0turn0file14
Related
services/api/src/modules/listings/listing-match-eligibility.jsaportagetListingPairMatchCompatibility, que este archivo usa como primera autoridad de elegibilidad y compatibilidad de par.services/api/src/modules/match/comparison-contract.jsaporta los comparadores puros reutilizados aquí, incluidosevaluatePreferenceSignal,compareOfferPriceToRequirementBudget,compareRentalTerm,comparePropertyType,comparePhysicalMinimum,compareInfrastructureDeclaration,evaluateAmenitiesEvidenceycompareSearchZonesToOfferLocation.services/api/src/modules/match/field-applicability.jsdefine la aplicabilidad de campos porpropertyType, y este módulo la consume antes de comparar campos físicos o de infraestructura. fileciteturn0file4turn0file16services/api/src/modules/match/candidate-retrieval.jses la única capa de Match que toca Mongo y alimenta este evaluador con datos ya filtrados.candidate-generation.jspermanece libre de base de datos. fileciteturn0file12turn0file14services/api/src/modules/match/candidate-presentation.jsconsume el contrato generado aquí y lo transforma para presentación, sin modificar esta capa de generación.