candidate-presentation.js
Overview
Este módulo implementa la capa de presentación de Match para la fase M2.3a. Consume candidatos ya evaluados por evaluateCandidatePair() y generateCandidatesForRequirement() sin mutarlos, sin tocar Mongo y sin calcular score, porcentaje ni rank numérico .
Su propósito es doble. Primero, traduce el contrato de candidato a un presentationState determinista. Segundo, ordena los candidatos presentables con reglas puras y predecibles, usando offerId como desempate técnico .
[!IMPORTANT]
candidate.statusypresentationStateson conceptos distintos. Un candidato puede seguir siendostatus: "blocked"y, aun así, mostrarse comobudget_warning.
[!NOTE] Este archivo no lee señales de preferencia
preferredpara ordenar. Tampoco usa monto, distancia geográfica, amenidades, completitud ni timestamps .
How it works
flowchart TD
A[Candidate evaluado] --> B[getCandidatePresentationState]
A --> C[isApprovedBudgetWarningCandidate]
A --> D[isApprovedAlternativeCandidate]
E[APPROVED_BUDGET_WARNING_REASONS] --> C
F[APPROVED_PROPERTY_TYPE_ALTERNATIVES] --> D
G[isEligiblePair] --> C
G --> D
C --> B
D --> B
B --> H{hidden}
H -->|yes| I[Excluded from output]
H -->|no| J[Kept for presentation]
J --> K[orderCandidatesForPresentation]
K --> L[eligible]
K --> M[pending]
K --> N[budget_warning]
K --> O[alternative]
M --> P[count unresolved declared constraints]
P --> Q[offerId asc]
L --> Q
N --> Q
O --> Q
Q --> R[Ordered candidates]
La entrada es un arreglo de candidatos ya evaluados. El módulo deriva el estado de presentación con reglas cerradas. Después excluye hidden y ordena el resto por prioridad de estado. Dentro de pending, usa la cantidad de criterios declarados aún sin resolver. En los demás grupos, usa offerId ascendente como único desempate .
Key components
{
"title": "isApprovedBudgetWarningCandidate",
"content": "Valida si un candidato bloqueado puede mostrarse como advertencia de presupuesto. Exige un solo blocker de precio, razones aprobadas y elegibilidad del par.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
{
"title": "isApprovedAlternativeCandidate",
"content": "Valida si un candidato bloqueado puede mostrarse como alternativa aprobada por tipo de inmueble. Usa una tabla sim\u00e9trica de propertyType y exige elegibilidad del par.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
{
"title": "getCandidatePresentationState",
"content": "Deriva el estado visible del candidato. Convierte status de dominio en eligible, pending, budget_warning, alternative o hidden sin modificar el candidato original.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
{
"title": "isPrimaryMatchCandidate",
"content": "Indica si el candidato cuenta como coincidencia principal. Solo eligible y pending consumen cupo principal; budget_warning y alternative quedan como secundarias.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
{
"title": "isCandidatePresentable",
"content": "Indica si el candidato puede llegar a un consumidor final. Equivale a verificar que su estado de presentaci\u00f3n no sea hidden.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
{
"title": "orderCandidatesForPresentation",
"content": "Ordena candidatos presentables de forma determinista. Respeta el orden de estados, resuelve pending por evidencia faltante y usa offerId ascendente como desempate final.",
"type": "function",
"filePath": "services/api/src/modules/match/candidate-presentation.js",
"badges": [
"public"
]
}
Data & fields
[
{
"name": "candidate.status",
"type": "string",
"required": true,
"description": "Estado de dominio leído por la capa de presentación. Los valores observados aquí son eligible, pending y blocked."
},
{
"name": "candidate.blockers",
"type": "Array<object>",
"description": "Solo importa para budget_warning y alternative. En ambos casos debe contener exactamente un blocker."
},
{
"name": "candidate.eligibility.requirement.eligible",
"type": "boolean",
"required": true,
"description": "Debe ser true para que una excepción de presentación sea válida."
},
{
"name": "candidate.eligibility.offer.eligible",
"type": "boolean",
"required": true,
"description": "Debe ser true para que una excepción de presentación sea válida."
},
{
"name": "candidate.pairCompatibility.compatible",
"type": "boolean",
"required": true,
"description": "Debe ser true para que una excepción de presentación sea válida."
},
{
"name": "candidate.pendingRequired",
"type": "Array<object>",
"description": "Se cuenta solo cuando el candidato está pending. Se suma con pendingGates para el orden interno."
},
{
"name": "candidate.pendingGates",
"type": "Array<object>",
"description": "Se cuenta solo cuando el candidato está pending. Se suma con pendingRequired para el orden interno."
},
{
"name": "candidate.offerId",
"type": "string",
"description": "Desempate técnico final. Se compara como cadena ascendente."
}
]
[
{
"name": "blocker.field",
"type": "string",
"required": true,
"description": "Debe ser price para budget_warning o propertyType para alternative."
},
{
"name": "blocker.reason",
"type": "string",
"required": true,
"description": "Debe ser ABOVE_BUDGET_MAX o BELOW_BUDGET_MIN para budget_warning."
},
{
"name": "blocker.requirementValue",
"type": "string",
"required": true,
"description": "Valor del Requirement usado para validar la alternativa aprobada."
},
{
"name": "blocker.offerValue",
"type": "string",
"required": true,
"description": "Valor del Offer usado para validar la alternativa aprobada."
}
]
Constants
APPROVED_BUDGET_WARNING_REASONS— lista inmutable conABOVE_BUDGET_MAXyBELOW_BUDGET_MIN. Estas son las únicas razones aceptadas parabudget_warning.APPROVED_PROPERTY_TYPE_ALTERNATIVES— tabla inmutable y simétrica. Aceptaapartment↔housecomo alternativa presentable .PRESENTATION_STATE_ORDER— orden de prioridad. El orden eseligible,pending,budget_warning,alternative.
Behaviors
isApprovedBudgetWarningCandidate(candidate)— devuelvetruesolo si el candidato estáblocked, tiene un solo blocker de precio y pertenece a una pareja elegible .isApprovedAlternativeCandidate(candidate)— devuelvetruesolo si el candidato estáblocked, tiene un solo blocker depropertyTypey el par está aprobado por la tabla de alternativas .getCandidatePresentationState(candidate)— devuelveeligible,pending,budget_warning,alternativeohidden, en ese orden de decisión .isPrimaryMatchCandidate(candidate)— devuelvetruesolo paraeligibleypending. Las otras dos presentaciones son visibles, pero secundarias .isCandidatePresentable(candidate)— devuelvetruecuando el estado de presentación no eshidden.orderCandidatesForPresentation(candidates)— filtrahidden, agrupa por estado y ordena de forma estable y determinista .
Internal helper behavior
isEligiblePair(candidate)— comprueba tres condiciones juntas:eligibility.requirement.eligible === trueeligibility.offer.eligible === truepairCompatibility.compatible === true
countUnresolvedDeclaredConstraints(candidate)— sumapendingRequired.lengthypendingGates.length.compareOfferIdAscending(a, b)— comparaofferIdcomo cadena. Si falta, usa cadena vacía.
Estas funciones no se exponen fuera del módulo, pero sostienen el orden final .
Workflows
1. Recibir candidatos evaluados | El módulo asume que el candidato ya trae status, blockers, eligibility, pairCompatibility y buckets de pendientes.
2. Derivar el estado | `getCandidatePresentationState` clasifica el candidato sin tocar `candidate.status`.
3. Filtrar ocultos | `hidden` no sale del resultado final.
4. Ordenar por estado | Primero van `eligible`, luego `pending`, luego `budget_warning`, y al final `alternative`.
5. Resolver pending | Dentro de `pending`, los candidatos con menos evidencia faltante van primero.
6. Aplicar desempate final | En los demás grupos, el orden final usa `offerId` ascendente.
Gotchas & edge cases
[!WARNING]
candidate.statusypresentationStatepueden divergir deliberadamente. Un candidato puede seguir siendoblockedy mostrarse comobudget_warning.
[!WARNING]
CURRENCY_MISMATCHyPERIODICITY_MISMATCHnunca se convierten enbudget_warning. Esos casos quedanhidden, porque no significan “fuera de presupuesto” .
[!CAUTION] Las excepciones de
budget_warningyalternativeexigen exactamente un blocker. Si hay más de uno, el candidato se oculta .
[!NOTE] Si faltan
eligibilityopairCompatibility, la validación falla cerrado. El candidato no califica para ninguna excepción presentable .
[!IMPORTANT] Este módulo no reimplementa ni recalcula la lógica de Match. Solo consume el contrato de candidato ya producido por la capa anterior .
Related
services/api/src/modules/match/candidate-generation.js produce el contrato que esta capa consume. Su salida incluye status, eligibility, pairCompatibility, signals, blockers, pendingRequired, pendingGates, unknowns y notApplicable .
services/api/src/scripts/previewMatchForRequirement.js usa getCandidatePresentationState y orderCandidatesForPresentation para anotar el preview de QA. Ese script conserva candidatos blocked para explicabilidad, pero no recalcula la lógica de presentación .
La cobertura de pruebas vive en services/api/test/match-m2.3a-candidate-presentation.test.js. Ese archivo valida estados base, excepciones de presupuesto, alternativas de tipo, orden determinista y garantías estructurales, como la ausencia de campos de score o rank .
[!TIP] La relación más importante de este archivo es con la fase de generación previa. Aquí se decide cómo presentar el candidato, no cómo calcularlo .