comparison-contract.js
Overview
Este archivo define el contrato de comparación para Match en su fase M0 y posteriores ajustes de reglas. No implementa scoring, ranking ni digest. Su función es fijar vocabulario, semántica y resultados deterministas que un motor de Match futuro debe reutilizar.
Todas las funciones son puras. No consultan Mongo, no mutan argumentos, no usan Date.now() y no dependen de I/O. El único helper externo usado aquí es haversineDistanceKm desde ./geo-distance.js.
[!IMPORTANT] Este módulo no es Match. Solo define reglas mínimas de comparación y vocabularios estables para reutilización futura.
How it works
flowchart TD
M[comparison-contract js]
G[haversineDistanceKm]
P[evaluatePreferenceSignal]
B[compareOfferPriceToRequirementBudget]
T[compareRentalTerm]
R[comparePropertyType]
F[comparePhysicalMinimum]
I[compareInfrastructureDeclaration]
A[evaluateAmenitiesEvidence]
Z[compareSearchZonesToOfferLocation]
M --> P
M --> B
M --> T
M --> R
M --> F
M --> I
M --> A
M --> Z
Z --> G
El archivo separa las comparaciones por tipo de criterio. Primero define vocabularios compartidos. Luego aplica reglas específicas para precio, plazo, tipo de propiedad, campos físicos, infraestructura, amenidades y geografía por radio.
Cada función devuelve objetos con resultados deterministas y motivos explícitos. Eso permite que orquestación posterior tome decisiones sin inventar reglas nuevas.
Key components
comparison-contract.js
{
"title": "comparison-contract.js",
"content": "M\u00f3dulo de contrato de comparaci\u00f3n para Match. Define vocabularios, reglas puras y resultados deterministas reutilizables por un motor futuro.",
"type": "class",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
evaluatePreferenceSignal
{
"title": "evaluatePreferenceSignal",
"content": "Combina un nivel de preferencia con una compatibilidad previa y devuelve una se\u00f1al determinista para pass, fail, pendiente, exclusi\u00f3n o contribuci\u00f3n al ranking.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
compareOfferPriceToRequirementBudget
{
"title": "compareOfferPriceToRequirementBudget",
"content": "Compara `Offer.price` con `Requirement.budget` usando moneda y periodicidad. Devuelve compatibilidad, incompatibilidad, inc\u00f3gnita o conflicto de unidad.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
compareRentalTerm
{
"title": "compareRentalTerm",
"content": "Compara el plazo m\u00ednimo del Offer con el plazo m\u00e1ximo aceptado del Requirement. Solo usa conversiones exactas entre unidades compatibles.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
comparePropertyType
{
"title": "comparePropertyType",
"content": "Valida compatibilidad de tipo de propiedad solo para `entityType === property`. Fuera de ese tipo, el criterio no aplica.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
comparePhysicalMinimum
{
"title": "comparePhysicalMinimum",
"content": "Compara un valor f\u00edsico del Offer contra un umbral m\u00ednimo del Requirement. Preserva la diferencia entre `null` y `0`.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
compareInfrastructureDeclaration
{
"title": "compareInfrastructureDeclaration",
"content": "Traduce la declaraci\u00f3n de infraestructura del Offer a compatibilidad o desconocimiento. Nunca convierte `not_declared` en incompatibilidad.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
evaluateAmenitiesEvidence
{
"title": "evaluateAmenitiesEvidence",
"content": "Eval\u00faa evidencia de amenidades deseadas y ofrecidas. Nunca produce hard fail y devuelve evidencia estructurada cuando hay datos suficientes.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
compareSearchZonesToOfferLocation
{
"title": "compareSearchZonesToOfferLocation",
"content": "Compara zonas de b\u00fasqueda del Requirement con la ubicaci\u00f3n declarada del Offer usando distancia Haversine. Aplica sem\u00e1ntica OR entre zonas v\u00e1lidas.",
"type": "function",
"filePath": "services/api/src/modules/match/comparison-contract.js",
"badges": [
"public"
]
}
Data & fields
Este módulo define varios vocabularios fijos. Todos funcionan como contratos de comparación, no como reglas de UI. Las listas están congeladas con Object.freeze.
FIELD_KINDS [badge:read-only]
string[] con el vocabulario de un campo antes de compararlo.
fact— valor declarado por el propio Listing.requirement— criterio exigido por el otro lado.preference— criterio preferido, pero no obligatorio.unknown— dato no declarado.not_applicable— el criterio no tiene sentido para ese par.
COMPARATOR_TYPES [badge:read-only]
string[] con los tipos de comparador soportados.
equality— el valor debe coincidir exactamente.minimum— el valor del candidato debe ser mayor o igual.maximum— el valor del candidato debe ser menor o igual.range— el valor debe caer dentro de un rango.oneOf— el valor debe pertenecer a un conjunto permitido.
UNIT_KINDS [badge:read-only]
string[] con unidades comparables por magnitud.
area_m2countcurrencydatedistance_km
PREFERENCE_LEVELS [badge:read-only]
string[] con semántica interna de preferencia para Match.
requiredpreferredirrelevantunspecified
[!NOTE] Esta lista es interna de Match. No sustituye
legalEntityRequirementni sus etiquetas de producto.
Este módulo usa otro vocabulario interno para evaluar compatibilidad antes de aplicar preferencia.
compatibleincompatibleunknownnot_applicable
Ese vocabulario alimenta evaluatePreferenceSignal. También aparece en varios comparadores directos.
Estas constantes no se exportan, pero sí gobiernan reglas exactas del módulo.
DAY_BASED_UNIT_TO_DAYS— conviertedayyweeka días.MONTH_BASED_UNIT_TO_MONTHS— conviertemonthyyeara meses.SEARCH_ZONE_RADIUS_KM_MAX— límite superior de radio de zona.COMPATIBILITY_VALUES— lista válida paraevaluatePreferenceSignal.
Behaviors
evaluatePreferenceSignal
Esta función combina un nivel de preferencia con una compatibilidad previa. Devuelve una señal útil para decisión, ranking o exclusión.
[
{
"name": "level",
"type": "required | preferred | irrelevant | unspecified",
"description": "Nivel de preferencia interno. Si falta o no es válido, se normaliza a `unspecified`."
},
{
"name": "compatibility",
"type": "compatible | incompatible | unknown | not_applicable",
"required": true,
"description": "Resultado previo de la comparación del criterio."
}
]
Devuelve un objeto con estas claves:
outcome—excluded,no_contribution,fail,pending_unknown,pass,positive_signaloranking_signal.hardFail— indica fallo duro.blocksPass— indica si bloquea el pase.contributesToRanking— indica si aporta señal al ranking.
Reglas clave:
not_applicablesiempre produceexcluded.irrelevantyunspecifiedsiempre producenno_contribution.required + incompatibleproducefail.required + unknownproducepending_unknown.preferred + compatibleproducepositive_signal.preferred + incompatibleproduceranking_signal.preferred + unknownproduceno_contribution.
[!IMPORTANT] Un
requiredcon compatibilidadunknownnunca pasa. Queda pendiente, pero no aprobado.
compareOfferPriceToRequirementBudget
Esta función compara Offer.price con Requirement.budget. No compara silenciosamente monedas distintas. Tampoco convierte periodicidades de forma implícita.
[
{
"name": "price",
"type": "{ amount?: number | null, currency?: string, periodicity?: string }",
"description": "Precio del Offer. `amount` es el importe comparado. `currency` y `periodicity` se validan contra el presupuesto."
},
{
"name": "budget",
"type": "{ min?: number | null, max?: number | null, currency?: string, periodicity?: string }",
"description": "Presupuesto del Requirement. `min` y `max` son límites independientes."
}
]
Devuelve un objeto con:
result—compatible,incompatible,unknownoincompatible_unit.reason— código de motivo onull.
Reglas clave:
- Si falta
amount, devuelveunknown. - Si el presupuesto no tiene
minnimax, devuelveunknown. - Si ambas monedas existen y difieren, devuelve
incompatible_unit. - Si una periodicidad es conocida y la otra no, devuelve
unknown. - Si ambas periodicidades son conocidas y difieren, devuelve
incompatible_unit. - Si
amount < min, devuelveincompatible. - Si
amount > max, devuelveincompatible. - En otro caso, devuelve
compatible.
[!WARNING] La función no convierte entre periodicidades.
day,weekymonthno se mezclan con aproximaciones.
compareRentalTerm
Esta función compara el plazo mínimo exigido por el Offer con el plazo máximo aceptado por el Requirement. Solo usa conversiones exactas.
[
{
"name": "offerMinimumTerm",
"type": "{ value?: number | null, unit?: string | null }",
"description": "Plazo mínimo declarado por el Offer. Usa `meta.minimumTermValue` y `meta.minimumTermUnit`."
},
{
"name": "requirementMaximumAcceptedTerm",
"type": "{ value?: number | null, unit?: string | null }",
"description": "Plazo máximo tolerado por el Requirement. Usa `maximumAcceptedTermValue` y `maximumAcceptedTermUnit`."
}
]
Devuelve:
result—compatible,incompatibleounknown.reason— código de motivo onull.
Reglas clave:
- Si falta el valor del Offer, devuelve
unknown. - Si falta el valor del Requirement, devuelve
unknown. dayyweekforman una familia exacta.monthyyearforman otra familia exacta.- Cruzar familias distintas devuelve
unknown. - Si el plazo del Offer es menor o igual al del Requirement, devuelve
compatible. - Si lo excede, devuelve
incompatible.
[!CAUTION] No existe conversión exacta entre semanas y meses. La función conserva ese límite de forma conservadora.
comparePropertyType
Esta función aplica un gate de compatibilidad para entityType === "property". Fuera de ese tipo, el campo no aplica.
[
{
"name": "entityType",
"type": "string",
"description": "Tipo de entidad compartido por Offer y Requirement."
},
{
"name": "offerPropertyType",
"type": "string",
"description": "Tipo de propiedad del Offer."
},
{
"name": "requirementPropertyType",
"type": "string",
"description": "Tipo de propiedad del Requirement."
}
]
Devuelve:
result—compatible,incompatible,unknownonot_applicable.reason— código de motivo onull.
Reglas clave:
- Si
entityTypeno esproperty, devuelvenot_applicable. - Si falta cualquiera de los dos tipos, devuelve
unknown. - Si los tipos difieren, devuelve
incompatible. - Si coinciden, devuelve
compatible.
comparePhysicalMinimum
Esta función compara un valor físico del Offer contra un umbral mínimo del Requirement. Es agnóstica al nombre del campo.
[
{
"name": "offerValue",
"type": "number | null",
"description": "Valor físico declarado por el Offer."
},
{
"name": "requirementValue",
"type": "number | null",
"description": "Umbral mínimo pedido por el Requirement."
}
]
Devuelve:
result—compatible,incompatible,unknownonot_applicable.reason— código de motivo.
Reglas clave:
- Si el Requirement no declara umbral, devuelve
not_applicable. - Si el Offer no declara valor, devuelve
unknown. - Si
offerValue >= requirementValue, devuelvecompatible. - Si
offerValue < requirementValue, devuelveincompatible.
[!IMPORTANT]
0es un valor válido. No se trata como ausencia.
compareInfrastructureDeclaration
Esta función reutiliza la semántica aprobada para infraestructura declarada. Solo distingue entre declaración explícita y ausencia de evidencia.
[
{
"name": "declaration",
"type": "string",
"description": "Valor de `meta.waterDeclaration`, `electricityDeclaration` o `drainageDeclaration`."
}
]
Devuelve:
result—compatibleounknown.reason— código de motivo.
Reglas clave:
declared_unverifieddevuelvecompatible.not_declareddevuelveunknown.- Ausente o no reconocido también devuelve
unknown.
[!WARNING]
not_declaredno significaincompatible. Significa solo que no hay evidencia suficiente.
evaluateAmenitiesEvidence
Esta función evalúa amenidades deseadas y ofrecidas. No produce hard fail. Solo devuelve evidencia estructurada.
[
{
"name": "desired",
"type": "string[]",
"description": "Lista de amenidades deseadas por el Requirement."
},
{
"name": "offered",
"type": "string[]",
"description": "Lista de amenidades declaradas por el Offer."
}
]
Devuelve:
compatibility—not_applicable,unknownocompatible.reason— código de motivo.evidence— objeto condesired,matchedymissing, onull.
Reglas clave:
- Si
desiredestá vacío, devuelvenot_applicable. - Si
offeredestá vacío, devuelveunknown. - Si hay datos en ambos lados, devuelve
compatible. - Si faltan amenidades,
reasonindica coincidencia parcial. - Si no faltan,
reasonindica coincidencia total.
La evidencia se estructura así:
desired— amenidades pedidas.matched— coincidencias encontradas.missing— amenidades no cubiertas.
[!NOTE] Esta función no define una regla ALL-vs-ANY de producto. Solo produce evidencia informativa.
compareSearchZonesToOfferLocation
Esta función compara zonas de búsqueda con la ubicación declarada del Offer. Usa distancia de gran círculo con Haversine. Aplica lógica OR entre zonas válidas.
[
{
"name": "searchZones",
"type": "Array<{ center?: { lat?: number | null, lng?: number | null }, radiusKm?: number | null }>",
"description": "Zonas del Requirement. Cada zona contiene centro y radio."
},
{
"name": "offerDeclaration",
"type": "string",
"description": "Declaración de ubicación del Offer. Solo `declared` habilita el uso de coordenadas."
},
{
"name": "offerLatitude",
"type": "number | null",
"description": "Latitud persistida del Offer."
},
{
"name": "offerLongitude",
"type": "number | null",
"description": "Longitud persistida del Offer."
}
]
Devuelve:
result—compatible,incompatible,unknownonot_applicable.reason— código de motivo.evidence— objeto con evidencia de la zona, onull.
Reglas clave:
- Si no hay zonas, devuelve
not_applicable. - Si todas las zonas son inválidas, devuelve
unknown. - Si
offerDeclaration !== "declared", devuelveunknown. - Si latitud o longitud no son válidas, devuelve
unknown. - Si el Offer cae dentro de una zona válida, devuelve
compatible. - El borde cuenta como dentro.
- Si no cae en ninguna zona, devuelve
incompatible.
La evidencia compatible contiene:
matchedZoneIndexdistanceKmradiusKm
La evidencia incompatible contiene:
nearestZoneIndexnearestDistanceKm
[!CAUTION] Las zonas malformadas se ignoran silenciosamente. No se les asigna radio ni centro por defecto.
Internal helpers
El archivo usa helpers privados para mantener las reglas puras y repetibles.
isKnownPeriodicity— acepta soloday,weekymonth.normalizeRentalTermToComparableUnit— convierte plazos a días o meses exactos.isFiniteNumber— valida números finitos.isValidLatitude— valida latitudes entre-90y90.isValidLongitude— valida longitudes entre-180y180.isUsableSearchZone— valida zonas con centro y radio correctos.roundDistanceKm— redondea distancia a una decimal.
Gotchas & edge cases
[!WARNING]
unknownynot_applicableno significan lo mismo.unknownindica falta de evidencia.not_applicableindica que el criterio no tiene sentido.
[!WARNING]
compareOfferPriceToRequirementBudgetno inventa compatibilidad. Si faltan datos críticos, devuelveunknown.
[!CAUTION]
compareRentalTermsolo admite conversiones exactas. No usa aproximaciones como1 mes = 30 días.
[!CAUTION]
compareSearchZonesToOfferLocationexigeofferDeclaration === "declared". Tener coordenadas persistidas no basta por sí solo.
[!IMPORTANT]
evaluateAmenitiesEvidencenunca devuelve incompatibilidad. Su salida es evidencia estructurada, no decisión de bloqueo.
Related
Este módulo se conecta con otros archivos solo por lo que aparece en su código y comentarios.
services/api/src/modules/match/geo-distance.js— aportahaversineDistanceKm.docs/01-product/future-roadmap.md— se menciona como el documento donde se ubica el siguiente foco activo de Match.services/api/src/modules/listings/Listing.js— aparece en comentarios al explicar semánticas de producto ya publicadas.services/api/src/modules/listings/requirementOptions.js— aparece en comentarios junto alegalEntityRequirement.services/api/src/modules/match/candidate-generation.js— citado en comentarios como parte de la orquestación donde vive la aplicabilidad de campos.services/api/src/modules/match/field-applicability.js— citado en comentarios como contexto de aplicabilidad por campo.services/api/src/modules/listings/listings.validators.js— citado en comentarios para validaciones de escritura y límites.services/api/src/modules/match/match-m1-requirement-preferences.test.js— citado en comentarios como evidencia de la semántica de infraestructura.
[!NOTE] Los archivos anteriores se mencionan en comentarios del módulo. Solo
./geo-distance.jsaparece como import real.