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_m2
  • count
  • currency
  • date
  • distance_km

PREFERENCE_LEVELS [badge:read-only]

string[] con semántica interna de preferencia para Match.

  • required
  • preferred
  • irrelevant
  • unspecified

[!NOTE] Esta lista es interna de Match. No sustituye legalEntityRequirement ni sus etiquetas de producto.

Este módulo usa otro vocabulario interno para evaluar compatibilidad antes de aplicar preferencia.

  • compatible
  • incompatible
  • unknown
  • not_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 — convierte day y week a días.
  • MONTH_BASED_UNIT_TO_MONTHS — convierte month y year a meses.
  • SEARCH_ZONE_RADIUS_KM_MAX — límite superior de radio de zona.
  • COMPATIBILITY_VALUES — lista válida para evaluatePreferenceSignal.

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_signal o ranking_signal.
  • hardFail — indica fallo duro.
  • blocksPass — indica si bloquea el pase.
  • contributesToRanking — indica si aporta señal al ranking.

Reglas clave:

  • not_applicable siempre produce excluded.
  • irrelevant y unspecified siempre producen no_contribution.
  • required + incompatible produce fail.
  • required + unknown produce pending_unknown.
  • preferred + compatible produce positive_signal.
  • preferred + incompatible produce ranking_signal.
  • preferred + unknown produce no_contribution.

[!IMPORTANT] Un required con compatibilidad unknown nunca 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, unknown o incompatible_unit.
  • reason — código de motivo o null.

Reglas clave:

  • Si falta amount, devuelve unknown.
  • Si el presupuesto no tiene min ni max, devuelve unknown.
  • 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, devuelve incompatible.
  • Si amount > max, devuelve incompatible.
  • En otro caso, devuelve compatible.

[!WARNING] La función no convierte entre periodicidades. day, week y month no 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, incompatible o unknown.
  • reason — código de motivo o null.

Reglas clave:

  • Si falta el valor del Offer, devuelve unknown.
  • Si falta el valor del Requirement, devuelve unknown.
  • day y week forman una familia exacta.
  • month y year forman 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, unknown o not_applicable.
  • reason — código de motivo o null.

Reglas clave:

  • Si entityType no es property, devuelve not_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, unknown o not_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, devuelve compatible.
  • Si offerValue < requirementValue, devuelve incompatible.

[!IMPORTANT] 0 es 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 — compatible o unknown.
  • reason — código de motivo.

Reglas clave:

  • declared_unverified devuelve compatible.
  • not_declared devuelve unknown.
  • Ausente o no reconocido también devuelve unknown.

[!WARNING] not_declared no significa incompatible. 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, unknown o compatible.
  • reason — código de motivo.
  • evidence — objeto con desired, matched y missing, o null.

Reglas clave:

  • Si desired está vacío, devuelve not_applicable.
  • Si offered está vacío, devuelve unknown.
  • Si hay datos en ambos lados, devuelve compatible.
  • Si faltan amenidades, reason indica coincidencia parcial.
  • Si no faltan, reason indica 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, unknown o not_applicable.
  • reason — código de motivo.
  • evidence — objeto con evidencia de la zona, o null.

Reglas clave:

  • Si no hay zonas, devuelve not_applicable.
  • Si todas las zonas son inválidas, devuelve unknown.
  • Si offerDeclaration !== "declared", devuelve unknown.
  • 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:

  • matchedZoneIndex
  • distanceKm
  • radiusKm

La evidencia incompatible contiene:

  • nearestZoneIndex
  • nearestDistanceKm

[!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 solo day, week y month.
  • normalizeRentalTermToComparableUnit — convierte plazos a días o meses exactos.
  • isFiniteNumber — valida números finitos.
  • isValidLatitude — valida latitudes entre -90 y 90.
  • isValidLongitude — valida longitudes entre -180 y 180.
  • isUsableSearchZone — valida zonas con centro y radio correctos.
  • roundDistanceKm — redondea distancia a una decimal.

Gotchas & edge cases

[!WARNING] unknown y not_applicable no significan lo mismo. unknown indica falta de evidencia. not_applicable indica que el criterio no tiene sentido.

[!WARNING] compareOfferPriceToRequirementBudget no inventa compatibilidad. Si faltan datos críticos, devuelve unknown.

[!CAUTION] compareRentalTerm solo admite conversiones exactas. No usa aproximaciones como 1 mes = 30 días.

[!CAUTION] compareSearchZonesToOfferLocation exige offerDeclaration === "declared". Tener coordenadas persistidas no basta por sí solo.

[!IMPORTANT] evaluateAmenitiesEvidence nunca devuelve incompatibilidad. Su salida es evidencia estructurada, no decisión de bloqueo.

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 — aporta haversineDistanceKm.
  • 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 a legalEntityRequirement.
  • 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.js aparece como import real.