listing-match-eligibility.js

Overview

Este archivo define el contrato de elegibilidad para Match en services/api/src/modules/listings/listing-match-eligibility.js. No implementa Match, ni scoring, ni ranking, ni digest. Su función es decidir qué Listing puede participar en un futuro Match y qué par de Listing tiene sentido comparar.

El archivo trabaja con reglas puras y deterministas. No consulta Mongo, no muta argumentos, y no crea Lead ni ListingEvent. También conserva compatibilidad con valores heredados, como intent, lifecycle.status y ownerAccountStatus ausentes.

[!IMPORTANT] Este archivo no implementa Match. Solo define elegibilidad y compatibilidad previa para un motor futuro.

How it works

flowchart TD
    Caller[Caller] --> Single[getListingMatchEligibility]
    Caller --> Pair[getListingPairMatchCompatibility]

    Single --> Intent[Resolve intent]
    Single --> Life[Resolve lifecycle status]
    Single --> Pub[Resolve publication status]
    Single --> Owner[Resolve owner status]
    Intent --> SingleResult[Eligibility result]
    Life --> SingleResult
    Pub --> SingleResult
    Owner --> SingleResult

    Pair --> OfferCheck[Check offer candidate]
    Pair --> ReqCheck[Check requirement candidate]
    OfferCheck --> OfferSingle[getListingMatchEligibility]
    ReqCheck --> ReqSingle[getListingMatchEligibility]
    OfferSingle --> PairRules[Pair rules]
    ReqSingle --> PairRules
    PairRules --> PairResult[Compatible result]

    ReqCheck --> Map[getCompatibleOfferOperationTypes]
    Map --> PairRules

El flujo empieza con un Listing individual. Primero se resuelven sus valores efectivos, incluso cuando faltan campos. Luego, el archivo puede comparar dos Listing entre sí. Esa comparación añade reglas de par, como intent opuesto, entityType igual y compatibilidad de operationType.

Key components

{
    "title": "getListingMatchEligibility",
    "content": "Eval\u00faa si un Listing puede participar, en principio, en un futuro Match. Devuelve banderas derivadas, razones de rechazo y estado normalizado, sin efectos secundarios.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-match-eligibility.js",
    "badges": [
        "public"
    ]
}
{
    "title": "getCompatibleOfferOperationTypes",
    "content": "Devuelve los operationType v\u00e1lidos para un Offer candidato, a partir del operationType de un Requirement. La salida deriva de OPERATION_TYPE_EQUIVALENCE.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-match-eligibility.js",
    "badges": [
        "public"
    ]
}
{
    "title": "getListingPairMatchCompatibility",
    "content": "Compara dos Listings y decide si forman un par compatible para un futuro Match. Reutiliza la elegibilidad individual y agrega reglas de comparaci\u00f3n entre ambos candidatos.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-match-eligibility.js",
    "badges": [
        "public"
    ]
}

Data & fields

  • OPERATION_TYPE_EQUIVALENCE [badge:read-only] object — Mapa congelado de equivalencias entre operationType de oferta y requerimiento.
  • listing [badge:required] object — Documento de Listing. Puede ser lean u objeto plano.
  • ownerAccountStatus [badge:optional] string — Estado de User.accountStatus ya resuelto por el caller. Si falta, se asume active.
  • meta.entityType [badge:optional] string — Tipo de entidad del Listing. En comparación de pares, falta por defecto se interpreta como property.
  • meta.operationType [badge:optional] string — Tipo operativo del Listing. Se usa para compatibilidad entre oferta y requerimiento.
  • intent [badge:optional] string — Si no es requirement, se resuelve como offer.
  • lifecycle.status [badge:optional] string — Si falta, se resuelve como normal.
  • publication.status [badge:optional] string — Si falta, se resuelve como draft.
[
    {
        "name": "eligible",
        "type": "boolean",
        "required": true,
        "description": "Indica si el Listing puede participar en principio en un futuro Match."
    },
    {
        "name": "reasons",
        "type": "string[]",
        "required": true,
        "description": "Lista de códigos de rechazo acumulados."
    },
    {
        "name": "isPublished",
        "type": "boolean",
        "required": true,
        "description": "Indica si publication.status es published."
    },
    {
        "name": "isPubliclyVisible",
        "type": "boolean",
        "required": true,
        "description": "Indica si el Listing está publicado y en lifecycle normal."
    },
    {
        "name": "lifecycleStatus",
        "type": "string | null",
        "required": true,
        "description": "Estado normalizado del lifecycle del Listing."
    },
    {
        "name": "intent",
        "type": "\"offer\" | \"requirement\" | null",
        "required": true,
        "description": "Intent normalizado del Listing."
    },
    {
        "name": "ownerAvailable",
        "type": "boolean",
        "required": true,
        "description": "Indica si el owner está disponible según ownerAccountStatus."
    }
]
[
    {
        "name": "compatible",
        "type": "boolean",
        "required": true,
        "description": "Indica si ambos Listings pueden compararse en un futuro Match."
    },
    {
        "name": "reasons",
        "type": "string[]",
        "required": true,
        "description": "Lista de códigos de rechazo acumulados por incompatibilidad de par."
    },
    {
        "name": "offerEligibility",
        "type": "object",
        "required": true,
        "description": "Resultado de getListingMatchEligibility para el candidato offer."
    },
    {
        "name": "requirementEligibility",
        "type": "object",
        "required": true,
        "description": "Resultado de getListingMatchEligibility para el candidato requirement."
    }
]
  • LISTING_MISSING — No se recibió listing.
  • NOT_PUBLISHED — publication.status no es published.
  • LIFECYCLE_NOT_NORMAL — lifecycle.status no es normal.
  • OWNER_UNAVAILABLE — ownerAccountStatus no es active.
  • OFFER_NOT_ELIGIBLE — El candidato offer no pasó su elegibilidad individual.
  • REQUIREMENT_NOT_ELIGIBLE — El candidato requirement no pasó su elegibilidad individual.
  • OFFER_CANDIDATE_IS_NOT_OFFER — El offer candidato no tiene intent resuelto como offer.
  • REQUIREMENT_CANDIDATE_IS_NOT_REQUIREMENT — El requirement candidato no tiene intent resuelto como requirement.
  • ENTITY_TYPE_MISMATCH — Los meta.entityType no coinciden.
  • OPERATION_TYPE_MISMATCH — Los meta.operationType no son compatibles.

Behaviors

  • resolveListingIntent — Normaliza listing.intent. Devuelve requirement solo cuando el valor es exactamente requirement. En cualquier otro caso, devuelve offer.
  • resolveLifecycleStatus — Normaliza listing.lifecycle.status. Si no existe, devuelve normal.
  • resolvePublicationStatus — Normaliza listing.publication.status. Si no existe, devuelve draft.
  • operationTypesAreCompatible — Compara dos operationType. Devuelve false si falta alguno. Devuelve true si son iguales. También acepta la equivalencia definida por OPERATION_TYPE_EQUIVALENCE.
  • getListingMatchEligibility — Evalúa un solo Listing. Calcula eligible, reasons, isPublished, isPubliclyVisible, lifecycleStatus, intent y ownerAvailable.
  • getCompatibleOfferOperationTypes — Devuelve un arreglo con los operationType compatibles para un Offer candidato, a partir del operationType de un Requirement.
  • getListingPairMatchCompatibility — Evalúa dos Listing y decide si forman un par compatible. Reusa la elegibilidad individual y agrega validaciones de par.

Workflows

Evaluación de un solo Listing

sequenceDiagram
    participant Caller
    participant Eligibility as getListingMatchEligibility

    Caller->>Eligibility: listing y ownerAccountStatus
    Eligibility-->>Caller: eligible, reasons y banderas derivadas

La evaluación individual solo mira el estado del Listing y el estado del owner. La función calcula valores normalizados y agrega razones de rechazo cuando corresponda. No consulta persistencia, no hace escrituras y no depende de servicios externos.

Comparación de un par

sequenceDiagram
    participant Caller
    participant Pair as getListingPairMatchCompatibility
    participant Single as getListingMatchEligibility

    Caller->>Pair: offerCandidate y requirementCandidate
    Pair->>Single: offerCandidate
    Single-->>Pair: offerEligibility
    Pair->>Single: requirementCandidate
    Single-->>Pair: requirementEligibility
    Pair-->>Caller: compatible y reasons

La comparación de par aplica primero la elegibilidad individual. Luego exige intent opuesto, mismo meta.entityType y compatibilidad de meta.operationType. Si cualquiera falla, el resultado final acumula razones.

Gotchas & edge cases

[!WARNING] getListingMatchEligibility trata un Listing sin intent como offer. Ese comportamiento es intencional y legado-safe.

[!WARNING] getListingMatchEligibility trata un Listing sin lifecycle.status como normal. Ese default mantiene compatibilidad con datos antiguos.

[!IMPORTANT] ownerAccountStatus no se resuelve dentro del archivo. El caller debe pasarlo ya calculado.

[!NOTE] isPubliclyVisible no significa “visible para Match”. Solo exige published y lifecycle normal.

[!CAUTION] El archivo no usa publication.verified, phoneVerifiedAt, plan ni role como evidencia de elegibilidad. Esos campos quedan fuera por diseño.

[!WARNING] getListingPairMatchCompatibility exige que el caller haya separado correctamente offerCandidate y requirementCandidate. Si no, devuelve razones como OFFER_CANDIDATE_IS_NOT_OFFER.

Este archivo comparte criterio con getListingInteractionPolicy en listing-interaction-policy.js. El comentario del archivo también lo alinea con DEFAULT_OWNER_PREFERENCES para el default permisivo de owner activo. Además, getCompatibleOfferOperationTypes deriva su lógica de OPERATION_TYPE_EQUIVALENCE y del patrón descrito en listings.validators.js.

También hace referencia a docs/01-product/future-roadmap.md, donde se sitúa Match como una capacidad futura todavía no diseñada.