services/api/src/modules/listings/requirement-refinement.js

Overview

Este módulo define la autoridad estructurada para refinar un Requirement propio ya existente. Su función es distinguir entre cambios que refinan la misma búsqueda y cambios que pivotan la intención, y luego aplicar el cambio con validación reusada del dominio. El archivo se presenta como la única fuente de verdad para esa clasificación, y evita que otros módulos repliquen la lógica de decisión.

También mantiene el refinamiento dentro de un alcance conservador. Solo expone campos cuya validación ya existe en listings.validators.js, y reserva algunos campos como pivotes de intención o como no soportados. El flujo está pensado para ser usado por conversations.routes.js hoy, y por un futuro RequirementInterpreter o agente autorizado después.

[!IMPORTANT] Este módulo no traduce el resultado a HTTP. Devuelve un outcome estructurado y deja esa traducción al caller.

How it works

El proceso sigue una secuencia corta y determinista. Primero clasifica todos los campos propuestos. Después consulta Mongo solo si no hay pivotes ni campos desconocidos. Luego valida el nuevo requirement con validateListingPayload, y finalmente persiste el cambio si el documento sigue siendo elegible para edición.

sequenceDiagram
    participant C as Caller
    participant R as Requirement refinement
    participant L as Listing model
    participant V as validateListingPayload

    C->>R: applyRequirementRefinement
    R->>R: classify all fields

    alt no changes
        R-->>C: empty
    else pivot field
        R-->>C: pivot
    else unsupported field
        R-->>C: unsupported
    else continue
        R->>L: findOne
        L-->>R: requirement or none

        alt not found
            R-->>C: not_found
        else deletion pending
            R-->>C: deletion_pending
        else valid candidate
            R->>V: validateListingPayload
            V-->>R: valid or invalid

            alt invalid
                R-->>C: invalid
            else write update
                R->>L: findOneAndUpdate
                L-->>R: updated or none

                alt updated
                    R-->>C: applied
                else conflict
                    R->>L: findOne
                    L-->>R: deletion status
                    R-->>C: deletion_pending or not_found
                end
            end
        end
    end

El diagrama refleja el orden real del código. La clasificación ocurre antes de tocar Mongo. La lectura usa un filtro de propiedad y vida editable. La escritura repite el filtro y exige que deletion siga en null. Si esa condición falla, el módulo reconsulta el estado actual para decidir entre deletion_pending y not_found.

Key components

Este archivo exporta dos funciones públicas. También define tres conjuntos de apoyo para clasificar campos y filtrar documentos editables. La lógica está orientada a listas de campos planas, no a rutas anidadas.

{
    "title": "classifyRefinementField",
    "content": "Clasifica un campo como supported, pivot o unsupported seg\u00fan listas fijas del m\u00f3dulo.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/requirement-refinement.js",
    "badges": [
        "public"
    ]
}
{
    "title": "applyRequirementRefinement",
    "content": "Aplica un refinamiento estructurado sobre un Requirement propio, validando, leyendo y escribiendo el documento en una sola ruta de dominio.",
    "type": "function",
    "filePath": "services/api/src/modules/listings/requirement-refinement.js",
    "badges": [
        "public"
    ]
}

Constantes de clasificación

Estas constantes no se exportan como API pública adicional, pero sí definen el comportamiento del módulo. Su contenido está fijado con Object.freeze, así que no cambia en tiempo de ejecución.

  • REFINEMENT_SUPPORTED_FIELDS [badge:public] readonly string[] — Campos que refinan la misma búsqueda.

    • budget
    • desiredAmenities
    • preferences
    • hasPets
    • petFriendlyRequired
    • maximumAcceptedTermValue
    • maximumAcceptedTermUnit
    • searchZones searchZones se añadió en M3.3b y se reemplaza completo, no por índice. La validación sigue siendo la misma de validateListingPayload.
  • REFINEMENT_PIVOT_FIELDS [badge:read-only] readonly string[] — Campos que cambian qué se busca.

    • intent
    • entityType
    • operationType
    • propertyType
    • serviceType
    • investmentType Estos campos viven fuera de requirement.* porque afectan el universo de ofertas comparables.
  • EDITABLE_LIFECYCLE_FILTER [badge:read-only] object — Filtro de vida editable.

    • Excluye protection
    • Excluye anonymized
    • Excluye purged El módulo permite refinar requisitos en borrador o archivados, siempre que no estén en esos estados.

Data & fields

Este módulo trabaja con un objeto de cambios plano. Cada llave entra en una clasificación previa, y luego se aplica como reemplazo de un solo nivel sobre requirement. Los campos no mencionados en changes se conservan intactos.

[
    {
        "name": "requirementId",
        "type": "string",
        "required": true,
        "description": "ID del Requirement propio que se quiere refinar."
    },
    {
        "name": "ownerId",
        "type": "string",
        "required": true,
        "description": "ID del propietario. Debe coincidir con createdBy."
    },
    {
        "name": "changes",
        "type": "object",
        "required": true,
        "description": "Mapa plano de campos de refinamiento. Cada llave se clasifica antes de leer o escribir Mongo.",
        "children": [
            {
                "name": "budget",
                "type": "object",
                "description": "Reemplaza el objeto completo de budget."
            },
            {
                "name": "desiredAmenities",
                "type": "array",
                "description": "Reemplaza el arreglo completo de amenidades deseadas."
            },
            {
                "name": "preferences",
                "type": "object",
                "description": "Reemplaza el objeto completo de preferencias."
            },
            {
                "name": "hasPets",
                "type": "boolean|null",
                "description": "Actualiza el indicador de mascotas permitido."
            },
            {
                "name": "petFriendlyRequired",
                "type": "boolean|null",
                "description": "Actualiza la preferencia de que el inmueble sea pet friendly."
            },
            {
                "name": "maximumAcceptedTermValue",
                "type": "number|null",
                "description": "Actualiza el valor máximo aceptado del plazo."
            },
            {
                "name": "maximumAcceptedTermUnit",
                "type": "string|null",
                "description": "Actualiza la unidad del plazo aceptado."
            },
            {
                "name": "searchZones",
                "type": "array",
                "description": "Reemplaza el arreglo completo de zonas de búsqueda. Usa la validación existente de listings.validators.js."
            }
        ]
    }
]

La función devuelve un objeto con una de estas formas. No lanza para casos de negocio esperados. Solo puede lanzar por errores reales de infraestructura, como fallos de Mongo.

  • { outcome: "empty" } — No había cambios útiles.
  • { outcome: "pivot", field } — El conjunto incluía un campo de intención.
  • { outcome: "unsupported", field } — El conjunto incluía un campo no soportado.
  • { outcome: "invalid", errors } — La validación del payload falló.
  • { outcome: "not_found" } — El requisito no era editable o no pertenecía al usuario.
  • { outcome: "deletion_pending" } — El requisito tenía eliminación pendiente.
  • { outcome: "applied", requirement, changedFields } — El cambio se aplicó con éxito. changedFields conserva las llaves recibidas en changes.

Behaviors

Estas son las responsabilidades observables del archivo. Cada comportamiento está respaldado por el código o por su cobertura de prueba.

  • classifyRefinementField(field) — Devuelve supported, pivot o unsupported.
  • applyRequirementRefinement({ requirementId, ownerId, changes }) — Orquesta lectura, clasificación, validación y escritura del refinamiento.
  • Clasificación total antes de persistencia — Evalúa todas las llaves antes de tocar Mongo.
  • Prioridad de pivote — Un campo de pivote gana aunque también exista un desconocido.
  • Prioridad de rechazo — Un campo desconocido devuelve unsupported sin aplicar cambios.
  • Lectura con propiedad y vida editable — Busca solo requisitos del dueño y con estado editable.
  • Rechazo de eliminación pendiente — Devuelve deletion_pending si el documento ya está marcado.
  • Merge superficial de un nivel — Sustituye el subobjeto o arreglo completo del campo afectado.
  • Validación reusada — Pasa el payload final por validateListingPayload.
  • Escritura condicional — Actualiza solo si el documento sigue sin deletion.
  • Resolución de carreras — Si la escritura no coincide, reconsulta para decidir el resultado.

[!NOTE] preferences conserva su semántica real de triestado por campo. No se colapsa a un booleano en este módulo.

[!WARNING] searchZones se reemplaza completo. No existe mutación parcial por índice en este archivo.

Workflows

Este flujo resume la secuencia de ejecución que implementa el módulo. El comportamiento es conservador y no intenta resolver interpretación libre.

1. Recibir cambios | El caller envía `requirementId`, `ownerId` y un objeto `changes`.
2. Clasificar llaves | Cada llave se evalúa como `supported`, `pivot` o `unsupported`.
3. Cortar temprano | Si hay pivote, campo desconocido o cambios vacíos, el módulo responde sin tocar Mongo.
4. Leer el Requirement | Se busca el documento propio con filtro de propietario, intención y vida editable.
5. Verificar estado | Si no existe o tiene eliminación pendiente, se devuelve un outcome negativo.
6. Fusionar y validar | Se aplica un merge superficial y se reusa `validateListingPayload`.
7. Escribir condicionalmente | Se actualiza solo si el documento sigue vigente y sin eliminación.
8. Resolver conflicto | Si la actualización no coincide, se reconsulta el estado actual y se decide el outcome final.

Gotchas & edge cases

Este módulo evita varios errores de integración comunes. Las condiciones esperadas se transforman en outcome, no en excepciones.

[!WARNING] intent, entityType, operationType, propertyType, serviceType e investmentType nunca son refinamiento. Siempre se tratan como pivote.

[!CAUTION] La validación de requirement.* no se reimplementa aquí. La autoridad sigue siendo validateListingPayload.

[!IMPORTANT] El módulo no aplica cambios parciales cuando el objeto mezcla un campo soportado con un pivote o desconocido. La clasificación previa bloquea toda la operación.

[!NOTE] Un Requirement en borrador o archivado puede seguir siendo refinable. No se exige publication.status = published.

[!WARNING] Si el documento cambia entre lectura y escritura, el resultado puede degradarse a not_found o deletion_pending. Esa lógica está prevista por el módulo.

Este módulo se conecta con varias piezas del dominio de listings y conversaciones. Las relaciones están explícitas en los comentarios del archivo y en la cobertura de pruebas.

  • services/api/src/models/Listing.js — Fuente del schema que distingue requirement.* de meta.*.
  • services/api/src/modules/listings/listings.validators.js — Validador reutilizado para todos los cambios.
  • services/api/src/modules/conversations/conversations.routes.js — Orquestación actual del refinamiento.
  • services/api/src/modules/listings/candidate-retrieval.js — Usa meta.* para definir el universo de ofertas.
  • services/api/src/modules/listings/listing-match-eligibility.js — Participa en la decisión de comparación inicial.
  • services/api/test/requirement-refinement.test.js — Verifica clasificación, outcomes y reuso de validación.

[!TIP] El contrato del módulo está alineado con la fase M3.3 y su extensión M3.3b para searchZones.