listing-referral

Overview

Este módulo gestiona la relación entre un anuncio principal y un anuncio relacionado dentro de services/api/src/modules/listings/listing-referral.js. Su foco está en validar enlaces entre anuncios, sincronizar un periodo de gracia cuando cambia la publicación, y enriquecer listados con un resumen seguro del anuncio relacionado.

También aplica reglas deterministas sobre propiedad, publicación, tipo de anuncio y caducidad. Eso permite mantener la relación sin depender de lógica externa o de un modelo de IA.

[!NOTE] El módulo trabaja sobre la estructura requirement.referral y usa Listing como fuente de verdad para validar y limpiar relaciones.

How it works

sequenceDiagram
    participant Caller as Caller
    participant Module as listing-referral.js
    participant Listing as Listing model
    participant Mongo as mongoose

    Caller->>Module: validateRelatedOffer referral ownerId
    alt relatedListingId missing
        Module-->>Caller: null
    else invalid ObjectId
        Module-->>Caller: INVALID_RELATED_LISTING_ID
    else valid ObjectId
        Module->>Mongo: Types ObjectId isValid
        Module->>Listing: findById select fields lean
        alt related missing or purged
            Module-->>Caller: RELATED_LISTING_UNAVAILABLE
        else owner differs
            Module-->>Caller: RELATED_LISTING_NOT_OWNED
        else intent is requirement
            Module-->>Caller: RELATED_LISTING_MUST_BE_OFFER
        else not published
            Module-->>Caller: RELATED_LISTING_NOT_PUBLISHED
        else valid relation
            Module-->>Caller: null
        end
    end

La validación primero descarta relaciones vacías. Después verifica que el identificador sea válido y que el anuncio exista. Luego aplica reglas de propiedad, tipo e ինտención, y por último confirma que esté publicado.

Key components

{
    "title": "REFERRAL_RELATED_OFFER_GRACE_DAYS",
    "content": "Constante p\u00fablica que define el periodo de gracia para relaciones con un anuncio relacionado no publicado. Su valor es 10 d\u00edas.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "REFERRAL_RELATED_OFFER_GRACE_MS",
    "content": "Constante p\u00fablica que convierte el periodo de gracia de 10 d\u00edas a milisegundos. Se usa para calcular cortes de expiraci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "validateRelatedOffer",
    "content": "Funci\u00f3n p\u00fablica as\u00edncrona que valida si un anuncio relacionado puede aceptarse para un referral. Devuelve null o un objeto de error con code y message.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "cleanupExpiredReferralRelations",
    "content": "Funci\u00f3n p\u00fablica as\u00edncrona que limpia relaciones caducadas en Listing. Borra relatedListingId y relatedListingUnpublishedAt cuando superan el corte calculado.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "syncReferralGraceForOffer",
    "content": "Funci\u00f3n p\u00fablica as\u00edncrona que sincroniza el periodo de gracia cuando cambia el estado de publicaci\u00f3n de una oferta. Marca o limpia relatedListingUnpublishedAt seg\u00fan el cambio de estado.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "attachRelatedOfferSummaries",
    "content": "Funci\u00f3n p\u00fablica as\u00edncrona que adjunta un resumen seguro del anuncio relacionado a una lista de listings. Tambi\u00e9n limpia relaciones expiradas y oculta datos privados cuando corresponde.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": [
        "public"
    ]
}
{
    "title": "safeRelatedOfferSummary",
    "content": "Funci\u00f3n interna que construye un resumen reducido del anuncio relacionado. Expone t\u00edtulo, metadatos, estado de publicaci\u00f3n y ubicaci\u00f3n, con control sobre la visibilidad de la ubicaci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/listing-referral.js",
    "badges": []
}

Data & fields

[
    {
        "name": "referral.relatedListingId",
        "type": "string | null",
        "description": "Identificador del anuncio relacionado. Si no existe, la validación no aplica."
    },
    {
        "name": "referral.relatedListingUnpublishedAt",
        "type": "Date | string | null",
        "description": "Marca temporal de cuándo el anuncio relacionado dejó de estar publicado."
    },
    {
        "name": "listing.createdBy",
        "type": "string | object",
        "required": true,
        "description": "Autor del anuncio. Se compara con ownerId y con viewerUserId."
    },
    {
        "name": "listing.intent",
        "type": "string",
        "required": true,
        "description": "Tipo de anuncio. La validación exige que el relacionado no tenga intent igual a requirement."
    },
    {
        "name": "listing.publication.status",
        "type": "string",
        "description": "Estado de publicación. La validación exige published."
    },
    {
        "name": "listing.lifecycle.status",
        "type": "string",
        "description": "Estado del ciclo de vida. Si vale purged, el anuncio se considera no disponible."
    },
    {
        "name": "listing.requirement.referral.relatedListing",
        "type": "object | null",
        "description": "Resumen adjunto del anuncio relacionado para consumo de lectura.",
        "children": [
            {
                "name": "_id",
                "type": "string",
                "required": true,
                "description": "Identificador del anuncio relacionado."
            },
            {
                "name": "title",
                "type": "string",
                "description": "Título del anuncio relacionado."
            },
            {
                "name": "meta",
                "type": "object",
                "required": true,
                "description": "Metadatos resumidos del anuncio.",
                "children": [
                    {
                        "name": "entityType",
                        "type": "string",
                        "description": "Tipo de entidad, con valor por defecto property."
                    },
                    {
                        "name": "operationType",
                        "type": "string",
                        "description": "Tipo de operación del anuncio."
                    },
                    {
                        "name": "propertyType",
                        "type": "string",
                        "description": "Tipo de propiedad."
                    },
                    {
                        "name": "serviceType",
                        "type": "string",
                        "description": "Tipo de servicio."
                    },
                    {
                        "name": "investmentType",
                        "type": "string",
                        "description": "Tipo de inversión."
                    }
                ]
            },
            {
                "name": "publication",
                "type": "object",
                "required": true,
                "description": "Estado de publicación resumido.",
                "children": [
                    {
                        "name": "status",
                        "type": "string",
                        "required": true,
                        "description": "Estado de publicación. Usa published como valor principal."
                    }
                ]
            },
            {
                "name": "location",
                "type": "object",
                "required": true,
                "description": "Ubicación resumida del anuncio.",
                "children": [
                    {
                        "name": "country",
                        "type": "string",
                        "description": "País visible según las reglas de exposición."
                    },
                    {
                        "name": "state",
                        "type": "string",
                        "description": "Estado o provincia visible según las reglas de exposición."
                    },
                    {
                        "name": "city",
                        "type": "string",
                        "description": "Ciudad visible según las reglas de exposición."
                    },
                    {
                        "name": "neighborhood",
                        "type": "string",
                        "description": "Barrio visible según las reglas de exposición."
                    }
                ]
            }
        ]
    }
]

Constants

  • REFERRAL_RELATED_OFFER_GRACE_DAYS [badge:read-only] number — Define una ventana de 10 días para mantener referencias a una oferta no publicada.
  • REFERRAL_RELATED_OFFER_GRACE_MS [badge:read-only] number — Convierte esa ventana a milisegundos para cálculos de corte.

Behaviors

  • validateRelatedOffer(referral, ownerId) — Valida la relación propuesta y devuelve null o un error estructurado con code y message.
  • cleanupExpiredReferralRelations(now) — Limpia en base de datos las relaciones cuya marca relatedListingUnpublishedAt ya caducó.
  • syncReferralGraceForOffer({ offerId, previousStatus, nextStatus, now }) — Actualiza marcas de gracia cuando una oferta cambia de publicada a no publicada, o al revés.
  • attachRelatedOfferSummaries(listings, { viewerUserId, now }) — Devuelve una lista enriquecida con relatedListing, o elimina la referencia cuando ya no puede mostrarse.
  • safeRelatedOfferSummary(offer, options) — Construye un resumen de lectura con campos controlados por el estado de publicación y el contexto del visor.

[!IMPORTANT] validateRelatedOffer no lanza errores. En su lugar, devuelve un objeto con code y message cuando falla la validación.

Workflows

1. Validar relación propuesta | `validateRelatedOffer` revisa el `relatedListingId`, el propietario y el estado del anuncio.
2. Sincronizar cambio de publicación | `syncReferralGraceForOffer` marca o limpia `relatedListingUnpublishedAt` cuando cambia `publication.status`.
3. Limpiar relaciones vencidas | `cleanupExpiredReferralRelations` elimina vínculos fuera del periodo de gracia.
4. Enriquecer listings de lectura | `attachRelatedOfferSummaries` añade `relatedListing` y oculta datos privados cuando corresponde.

Gotchas & edge cases

[!WARNING] Si relatedListingId está vacío, la validación devuelve null. Eso significa que la relación queda sin comprobación.

[!WARNING] cleanupExpiredReferralRelations actúa sobre registros cuyo corte ya fue superado. El cálculo usa la hora actual menos REFERRAL_RELATED_OFFER_GRACE_MS.

[!CAUTION] attachRelatedOfferSummaries borra la relación si la oferta no es visible para el visor. Esto ocurre cuando no hay propiedad compartida o la oferta no está publicada y el visor no es el dueño.

[!NOTE] safeRelatedOfferSummary solo expone ubicación completa cuando la oferta está publicada. También la expone si includePrivateLocation es verdadero.

Este módulo depende directamente de mongoose y del modelo Listing importado desde ../../models/Listing.js. También se integra con los campos publication.status, lifecycle.status, intent y requirement.referral de los documentos de anuncios.

  • Listing.findById se usa para validar el anuncio relacionado.
  • Listing.find se usa para recuperar ofertas válidas y construir resúmenes.
  • Listing.updateMany se usa para limpiar o sincronizar relaciones de referral.

El módulo usa dos niveles de visibilidad para el anuncio relacionado.

  • Si el anuncio está publicado, el resumen puede incluir ubicación.
  • Si no está publicado, solo el dueño puede conservar el vínculo visible.
  • Si la relación no cumple la propiedad o visibilidad, el módulo la elimina del resultado.