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. fileciteturn0file0turn0file7turn0file12

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. fileciteturn0file0turn0file8

[!NOTE] La dirección canónica aquí es Requirement como sujeto y Offer como candidato. El archivo no compara Offer contra Offer, ni Requirement contra Requirement. fileciteturn0file0turn0file7

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. fileciteturn0file7turn0file8

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. fileciteturn0file0turn0file7

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, frontage y depth.
  • INFRASTRUCTURE_FIELDS [badge:read-only] Array<object> — Mapea water, electricity y drainage a sus llaves de declaración en meta.
  • RENTAL_OPERATION_TYPES [badge:read-only] string[] — rent y temporary_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 — Devuelve true para rent y temporary_rent.
  • resolveRawListingId — Devuelve el _id crudo del listing o null.
  • resolveRawOwnerId — Devuelve createdBy como está, o _id si el owner está poblado sin .equals.
  • idsAreEqual — Compara ids con .equals cuando existe, y usa String() como respaldo.
  • buildPreferenceSignal — Envuelve evaluatePreferenceSignal y añade valores, razón y evidencia.
  • buildGateSignal — Normaliza gates en fail, excluded, pending_gate o pass.
  • computePropertyType — Resuelve entityType y propertyType, luego llama comparePropertyType.
  • buildPropertyTypeSignal — Convierte el resultado de comparePropertyType en 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 — Compara desiredAmenities con meta.amenities y conserva evidencia estructurada.
  • buildLocationSignal — Evalúa searchZones contra location.latitude y location.longitude.
  • mapPriceResultToCompatibility — Reduce incompatible e incompatible_unit a la misma compatibilidad de bloqueo.
  • buildPriceSignal — Evalúa presupuesto solo si el Requirement declaró min o max.
  • 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] propertyType y location son gates, no preferencias. Cuando no pueden resolverse, el resultado cae en pending_gate, no en una señal informativa común.

[!WARNING] El módulo no infiere not_applicable desde valores null o ausentes. Primero resuelve aplicabilidad con field-applicability.js. Luego compara el valor del campo. fileciteturn0file0turn0file16

[!NOTE] Si budget.min y budget.max están ausentes, price se marca como not_applicable. El requisito no está pidiendo precio en ese caso.

[!NOTE] Si la operación no es de renta, term se marca como not_applicable. El archivo no fabrica incertidumbre de plazo para ventas o inversiones. fileciteturn0file0turn0file7

[!CAUTION] La comparación de ids usa .equals cuando existe. Esto evita errores con instancias distintas de ObjectId que representan el mismo documento.

[!WARNING] evaluateCandidatePair refuerza la regla de mismo dueño y la de self-comparison, aunque la capa de retrieval ya las filtre antes. fileciteturn0file0turn0file14

  • services/api/src/modules/listings/listing-match-eligibility.js aporta getListingPairMatchCompatibility, que este archivo usa como primera autoridad de elegibilidad y compatibilidad de par.
  • services/api/src/modules/match/comparison-contract.js aporta los comparadores puros reutilizados aquí, incluidos evaluatePreferenceSignal, compareOfferPriceToRequirementBudget, compareRentalTerm, comparePropertyType, comparePhysicalMinimum, compareInfrastructureDeclaration, evaluateAmenitiesEvidence y compareSearchZonesToOfferLocation.
  • services/api/src/modules/match/field-applicability.js define la aplicabilidad de campos por propertyType, y este módulo la consume antes de comparar campos físicos o de infraestructura. fileciteturn0file4turn0file16
  • services/api/src/modules/match/candidate-retrieval.js es la única capa de Match que toca Mongo y alimenta este evaluador con datos ya filtrados. candidate-generation.js permanece libre de base de datos. fileciteturn0file12turn0file14
  • services/api/src/modules/match/candidate-presentation.js consume el contrato generado aquí y lo transforma para presentación, sin modificar esta capa de generación.