openapi: 3.1.0
info:
  title: Predio — API de datos catastrales (REST /v1)
  version: "1.1.0"
  summary: Resuelve y enriquece inmuebles españoles y devuelve JSON estructurado y versionado.
  description: |
    Espejo REST del servicio MCP de **Predio** (prediohq.com). Devuelve los datos catastrales no protegidos
    de un inmueble como JSON denso y versionado, desde una **referencia catastral** o desde unas
    **coordenadas** (lat/lon). Mismo contrato que las herramientas MCP `consultar_inmueble_catastro` y
    `consultar_inmueble_por_coordenadas`.

    - **Autenticación:** cabecera `x-api-key` en cada petición.
    - **Precios:** 1 crédito por respuesta con éxito; los errores de negocio (404/422) no cobran. Ver
      `https://prediohq.com/pricing.json`.
    - **Errores:** siempre JSON `{ "error": { "code", "message" } }`, nunca XML ni status ambiguo.
    - **Fuente:** Dirección General del Catastro, reutilizada conforme a su licencia. Servicio no oficial.
    - **Cobertura:** territorio común de España (excluye País Vasco y Navarra, con catastro foral propio).
  contact:
    name: Predio
    url: https://prediohq.com
    email: hola@prediohq.com
  license:
    name: MIT
    url: https://opensource.org/license/mit
termsOfService: https://prediohq.com/terminos

servers:
  - url: https://api.prediohq.com
    description: Producción

security:
  - ApiKeyAuth: []

tags:
  - name: inmueble
    description: Consulta de inmuebles por referencia catastral o por coordenadas.
  - name: operación
    description: Salud y descubrimiento.

paths:
  /v1/inmueble/{referenciaCatastral}:
    get:
      tags: [inmueble]
      operationId: getInmueblePorReferencia
      summary: Consulta un inmueble por su referencia catastral.
      description: |
        Resuelve la referencia (14 = parcela, 18 o 20 = inmueble) y devuelve los datos normalizados. Si una
        referencia de 14 caracteres contiene varios inmuebles, responde `300` con una lista de candidatos.
      parameters:
        - $ref: "#/components/parameters/ReferenciaCatastral"
      responses:
        "200": { $ref: "#/components/responses/Inmueble" }
        "300": { $ref: "#/components/responses/MultiplesCandidatos" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "402": { $ref: "#/components/responses/PagoRequerido" }
        "404": { $ref: "#/components/responses/NoEncontrado" }
        "422": { $ref: "#/components/responses/ReferenciaInvalida" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/CatastroNoDisponible" }
        "503": { $ref: "#/components/responses/BackendNoDisponible" }

  /v1/inmueble:
    post:
      tags: [inmueble]
      operationId: postInmueble
      summary: Consulta un inmueble (referencia en el cuerpo).
      description: Variante POST; misma semántica que el GET. Cuerpo máximo 256 KB.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [referenciaCatastral]
              properties:
                referenciaCatastral:
                  $ref: "#/components/schemas/Referencia"
            example:
              referenciaCatastral: "9872023VH5797S0001WX"
      responses:
        "200": { $ref: "#/components/responses/Inmueble" }
        "300": { $ref: "#/components/responses/MultiplesCandidatos" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "402": { $ref: "#/components/responses/PagoRequerido" }
        "404": { $ref: "#/components/responses/NoEncontrado" }
        "413": { $ref: "#/components/responses/PayloadDemasiadoGrande" }
        "422": { $ref: "#/components/responses/ReferenciaInvalida" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/CatastroNoDisponible" }
        "503": { $ref: "#/components/responses/BackendNoDisponible" }

  /v1/inmueble/by-coords:
    get:
      tags: [inmueble]
      operationId: getInmueblePorCoordenadas
      summary: Resuelve un inmueble por coordenadas (lat/lon).
      description: |
        Resuelve el inmueble que contiene el punto (latitud/longitud) y devuelve los datos normalizados —el
        MISMO contrato que la consulta por referencia. SRS soportados: `EPSG:4326` (WGS84, por defecto) y
        `EPSG:4258` (ETRS89). Si el punto cae en una parcela con varios inmuebles, responde `300` con la
        lista de candidatos. Cobertura: territorio común (excluye País Vasco y Navarra).
      parameters:
        - $ref: "#/components/parameters/Lat"
        - $ref: "#/components/parameters/Lon"
        - $ref: "#/components/parameters/Srs"
      responses:
        "200": { $ref: "#/components/responses/Inmueble" }
        "300": { $ref: "#/components/responses/MultiplesCandidatos" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "402": { $ref: "#/components/responses/PagoRequerido" }
        "404": { $ref: "#/components/responses/NoEncontrado" }
        "422": { $ref: "#/components/responses/CoordenadasInvalidas" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/CatastroNoDisponible" }
        "503": { $ref: "#/components/responses/BackendNoDisponible" }
    post:
      tags: [inmueble]
      operationId: postInmueblePorCoordenadas
      summary: Resuelve un inmueble por coordenadas (cuerpo).
      description: Variante POST; misma semántica que el GET. Cuerpo máximo 256 KB.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [lat, lon]
              properties:
                lat: { $ref: "#/components/schemas/Lat" }
                lon: { $ref: "#/components/schemas/Lon" }
                srs: { $ref: "#/components/schemas/Srs" }
            example: { lat: 41.3915, lon: 2.1620, srs: "EPSG:4326" }
      responses:
        "200": { $ref: "#/components/responses/Inmueble" }
        "300": { $ref: "#/components/responses/MultiplesCandidatos" }
        "401": { $ref: "#/components/responses/NoAutenticado" }
        "402": { $ref: "#/components/responses/PagoRequerido" }
        "404": { $ref: "#/components/responses/NoEncontrado" }
        "413": { $ref: "#/components/responses/PayloadDemasiadoGrande" }
        "422": { $ref: "#/components/responses/CoordenadasInvalidas" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/CatastroNoDisponible" }
        "503": { $ref: "#/components/responses/BackendNoDisponible" }

  /health:
    get:
      tags: [operación]
      operationId: getHealth
      summary: Liveness/health del servicio.
      security: []
      responses:
        "200":
          description: El servicio responde.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  service: { type: string, example: catastro-mcp }
                  transport:
                    type: array
                    items: { type: string }
                    example: ["mcp:/mcp", "rest:/v1/inmueble"]

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: API key del tenant (test/prod). Una key ausente o desconocida responde 401.

  parameters:
    ReferenciaCatastral:
      name: referenciaCatastral
      in: path
      required: true
      description: Referencia catastral de 14 (parcela), 18 o 20 (inmueble) caracteres alfanuméricos.
      schema:
        $ref: "#/components/schemas/Referencia"
      example: "9872023VH5797S0001WX"
    Lat:
      name: lat
      in: query
      required: true
      description: Latitud en grados decimales (WGS84/ETRS89).
      schema:
        $ref: "#/components/schemas/Lat"
      example: 41.3915
    Lon:
      name: lon
      in: query
      required: true
      description: Longitud en grados decimales (WGS84/ETRS89).
      schema:
        $ref: "#/components/schemas/Lon"
      example: 2.1620
    Srs:
      name: srs
      in: query
      required: false
      description: "Sistema de referencia geográfico (EPSG). Por defecto EPSG:4326."
      schema:
        $ref: "#/components/schemas/Srs"

  headers:
    X-Credit-Cost:
      description: Créditos cobrados por esta respuesta (0 si no se cobró).
      schema: { type: integer, minimum: 0 }
    X-Credits-Remaining:
      description: Saldo de créditos del tenant tras la petición.
      schema: { type: integer, minimum: 0 }
    X-Cache:
      description: Procedencia del dato. `MISS` = en vivo del OVC; `HIT` = caché fresca; `STALE` = serve-stale (OVC caído, el dato puede estar desactualizado).
      schema: { type: string, enum: [HIT, MISS, STALE] }
    X-Trace-Id:
      description: Identificador de traza distribuida para correlacionar la petición en el observability stack.
      schema: { type: string }

  responses:
    Inmueble:
      description: Inmueble resuelto (respuesta con éxito; consume 1 crédito).
      headers:
        X-Credit-Cost: { $ref: "#/components/headers/X-Credit-Cost" }
        X-Credits-Remaining: { $ref: "#/components/headers/X-Credits-Remaining" }
        X-Cache: { $ref: "#/components/headers/X-Cache" }
        X-Trace-Id: { $ref: "#/components/headers/X-Trace-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ConsultaOk" }
          example:
            status: ok
            property:
              reference:
                full: "9872023VH5797S0001WX"
                parcel: "9872023VH5797S"
                parts: { pc1: "9872023", pc2: "VH5797S", car: "0001", cc1: "W", cc2: "X" }
              class: urban
              use: Residencial
              builtAreaM2: 88
              yearBuilt: 1979
              participationPct: 3.5
              address:
                full: null
                province: Madrid
                municipality: Madrid
                postalCode: null
                streetType: CL
                streetName: Ejemplo
                streetNumber: "00"
                block: null
                polygon: null
                plot: null
                place: null
              constructions:
                - { use: VIVIENDA, floor: "03", door: B, areaM2: 76 }
              cultivations: []
            source:
              provider: "Direccion General del Catastro (OVC) - Consulta_DNPRC"
              reference: "9872023VH5797S0001WX"
              retrievedAt: "2026-06-25T10:00:00.000Z"
              cached: false
              stale: false

    MultiplesCandidatos:
      description: La referencia (14) resuelve a varios inmuebles; lista de candidatos para desambiguar (consume 1 crédito).
      headers:
        X-Credit-Cost: { $ref: "#/components/headers/X-Credit-Cost" }
        X-Credits-Remaining: { $ref: "#/components/headers/X-Credits-Remaining" }
        X-Cache: { $ref: "#/components/headers/X-Cache" }
        X-Trace-Id: { $ref: "#/components/headers/X-Trace-Id" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ConsultaMultiple" }
          example:
            status: multiple
            message: "La referencia corresponde a 2 inmuebles."
            candidates:
              - reference:
                  full: "9872023VH5797S0001WX"
                  parcel: "9872023VH5797S"
                  parts: { pc1: "9872023", pc2: "VH5797S", car: "0001", cc1: "W", cc2: "X" }
                use: Residencial
                builtAreaM2: 88
                address: "CL Ejemplo 00, Madrid"
              - reference:
                  full: "9872023VH5797S0002EQ"
                  parcel: "9872023VH5797S"
                  parts: { pc1: "9872023", pc2: "VH5797S", car: "0002", cc1: "E", cc2: "Q" }
                use: Almacén
                builtAreaM2: 30
                address: "CL Ejemplo 00, Madrid"
            source:
              provider: "Direccion General del Catastro (OVC) - Consulta_DNPRC"
              reference: "9872023VH5797S"
              retrievedAt: "2026-06-25T10:00:00.000Z"
              cached: false
              stale: false

    NoAutenticado:
      description: Falta la cabecera `x-api-key` o la key es desconocida. No consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            faltaKey: { value: { error: { code: MISSING_API_KEY, message: "No autenticado." } } }
            keyInvalida: { value: { error: { code: INVALID_API_KEY, message: "API key inválida." } } }

    PagoRequerido:
      description: Créditos agotados. No consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: PAYMENT_REQUIRED, message: "Créditos agotados." } }

    NoEncontrado:
      description: El inmueble no existe. Error de negocio — no consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: NOT_FOUND, message: "El inmueble no existe." } }

    ReferenciaInvalida:
      description: Referencia mal formada o con dígitos de control inválidos. Error de negocio — no consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: INVALID_REFERENCE, message: "Referencia catastral inválida." } }

    CoordenadasInvalidas:
      description: Coordenadas inválidas (SRS no soportado, o lat/lon ausentes o fuera de rango). Error de negocio — no consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: INVALID_COORDINATES, message: "SRS no soportado: \"EPSG:25830\". Soportados (geograficos): EPSG:4326, EPSG:4258." } }

    RateLimited:
      description: Límite de peticiones superado (por key/tenant/IP). No consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: RATE_LIMITED, message: "Demasiadas peticiones." } }

    PayloadDemasiadoGrande:
      description: Cuerpo de la petición mayor que 256 KB.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: PAYLOAD_TOO_LARGE, message: "Cuerpo de la petición demasiado grande." } }

    CatastroNoDisponible:
      description: La fuente (OVC del Catastro) no respondió o devolvió un error de infraestructura. No consume crédito. Reintento recomendado con backoff.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: CATASTRO_UNAVAILABLE, message: "La fuente de datos no respondió." } }

    BackendNoDisponible:
      description: Una dependencia interna (auth/billing) no está disponible temporalmente. No consume crédito.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          example: { error: { code: AUTH_BACKEND_UNAVAILABLE, message: "No disponible temporalmente; reintenta." } }

  schemas:
    Referencia:
      type: string
      minLength: 14
      maxLength: 20
      pattern: "^[0-9A-Za-z]{14}(?:[0-9A-Za-z]{4}(?:[0-9A-Za-z]{2})?)?$"
      description: 14 (parcela), 18 o 20 (inmueble) caracteres alfanuméricos.
      example: "9872023VH5797S0001WX"

    Lat:
      type: number
      minimum: -90
      maximum: 90
      description: Latitud en grados decimales (WGS84/ETRS89).
      example: 41.3915

    Lon:
      type: number
      minimum: -180
      maximum: 180
      description: Longitud en grados decimales (WGS84/ETRS89).
      example: 2.1620

    Srs:
      type: string
      enum: ["EPSG:4326", "EPSG:4258"]
      default: "EPSG:4326"
      description: "Sistema de referencia geográfico. EPSG:4326 (WGS84, por defecto) o EPSG:4258 (ETRS89)."

    Error:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code: { type: string, description: Código estable del error. }
            message: { type: string, description: Mensaje legible (no usar para lógica; usa `code`). }
            ovcCode:
              type: string
              description: 'Código de negocio crudo de la OVC (p. ej. "4"), presente solo en algunos 404/422.'

    # El contrato REST /v1 es una UNIÓN DISCRIMINADA por `status`. NO es la forma aplanada del tool MCP
    # (que añade property/candidates/message siempre, con null, por compatibilidad de su `outputSchema`).
    # El endpoint serializa EXACTAMENTE uno de los dos miembros:
    #   - 200 OK              -> ConsultaOk        { status:"ok",       property, source }
    #   - 300 MultipleChoices -> ConsultaMultiple  { status:"multiple", message, candidates, source }
    # Fuente de verdad: apps/api/src/catastro/types.ts (`consultaResultSchema`). El test de contrato
    # cruzado apps/api/test/openapi-rest-contract.test.ts valida respuestas REST reales contra ESTE doc.
    ConsultaResult:
      oneOf:
        - $ref: "#/components/schemas/ConsultaOk"
        - $ref: "#/components/schemas/ConsultaMultiple"
      discriminator:
        propertyName: status
        mapping:
          ok: "#/components/schemas/ConsultaOk"
          multiple: "#/components/schemas/ConsultaMultiple"

    ConsultaOk:
      type: object
      additionalProperties: false
      required: [status, property, source]
      properties:
        status: { type: string, const: ok, description: "Un único inmueble resuelto (HTTP 200)." }
        property: { $ref: "#/components/schemas/Property" }
        source: { $ref: "#/components/schemas/Source" }

    ConsultaMultiple:
      type: object
      additionalProperties: false
      required: [status, message, candidates, source]
      properties:
        status: { type: string, const: multiple, description: "La referencia (14) resuelve a varios inmuebles (HTTP 300)." }
        message: { type: string }
        candidates:
          type: array
          items: { $ref: "#/components/schemas/Candidate" }
        source: { $ref: "#/components/schemas/Source" }

    Reference:
      type: object
      additionalProperties: false
      required: [full, parcel, parts]
      properties:
        full: { type: string, description: Referencia completa (20) si está disponible. }
        parcel: { type: string, description: "14 primeros caracteres: la parcela/finca." }
        parts:
          type: object
          additionalProperties: false
          description: Descomposición tal cual la devuelve la OVC.
          properties:
            pc1: { type: string }
            pc2: { type: string }
            car: { type: string }
            cc1: { type: string }
            cc2: { type: string }

    Address:
      type: object
      additionalProperties: false
      required: [full, province, municipality, postalCode, streetType, streetName, streetNumber, block, polygon, plot, place]
      properties:
        full: { type: [string, "null"], description: Domicilio descriptivo completo (`ldt`). }
        province: { type: [string, "null"] }
        municipality: { type: [string, "null"] }
        postalCode: { type: [string, "null"] }
        streetType: { type: [string, "null"], description: "Tipo de vía (CL, AV, PZ...)." }
        streetName: { type: [string, "null"] }
        streetNumber: { type: [string, "null"] }
        block: { type: [string, "null"], description: Bloque/escalera/planta/puerta si aplica. }
        polygon: { type: [string, "null"], description: Polígono (solo rústico). }
        plot: { type: [string, "null"], description: Parcela dentro del polígono (solo rústico). }
        place: { type: [string, "null"], description: Paraje (solo rústico). }

    Construction:
      type: object
      additionalProperties: false
      required: [use, floor, door, areaM2]
      properties:
        use: { type: [string, "null"], description: "Uso del local (VIVIENDA, ALMACEN...)." }
        floor: { type: [string, "null"] }
        door: { type: [string, "null"] }
        areaM2: { type: [number, "null"], description: Superficie del local en m² (`stl`). }

    Cultivation:
      type: object
      additionalProperties: false
      required: [code, description, intensity, areaM2]
      properties:
        code: { type: [string, "null"], description: Código de clase de cultivo (`ccc`). }
        description: { type: [string, "null"], description: Descripción del cultivo (`dcc`). }
        intensity: { type: [string, "null"], description: Intensidad productiva (`ip`). }
        areaM2: { type: [number, "null"], description: Superficie de la subparcela en m² (`ssp`). }

    Property:
      type: object
      additionalProperties: false
      required: [reference, class, use, builtAreaM2, yearBuilt, participationPct, address, constructions, cultivations]
      properties:
        reference: { $ref: "#/components/schemas/Reference" }
        class:
          type: string
          enum: [urban, rustic]
          description: "Clase: urbano (UR) o rústico (RU)."
        use: { type: [string, "null"], description: "Uso principal (`luso`): Residencial, Agrario, Industrial..." }
        builtAreaM2: { type: [number, "null"], description: "Superficie construida total en m² (`sfc`). 0/null en rústico sin construcción." }
        yearBuilt: { type: [number, "null"], description: "Año de construcción (`ant`). null en rústico." }
        participationPct: { type: [number, "null"], description: "Coeficiente de participación en % (`cpt`)." }
        address: { $ref: "#/components/schemas/Address" }
        constructions:
          type: array
          description: Desglose de construcciones (urbano).
          items: { $ref: "#/components/schemas/Construction" }
        cultivations:
          type: array
          description: Desglose de cultivos/subparcelas (rústico).
          items: { $ref: "#/components/schemas/Cultivation" }

    Candidate:
      type: object
      additionalProperties: false
      required: [reference, use, builtAreaM2, address]
      properties:
        reference: { $ref: "#/components/schemas/Reference" }
        use: { type: [string, "null"] }
        builtAreaM2: { type: [number, "null"] }
        address: { type: [string, "null"], description: Domicilio resumido del candidato. }

    Source:
      type: object
      additionalProperties: false
      required: [provider, reference, retrievedAt, cached, stale]
      properties:
        provider: { type: string }
        reference: { type: string }
        retrievedAt: { type: string, format: date-time }
        cached: { type: boolean, description: true si se sirvió de caché (hit fresco o serve-stale). }
        stale: { type: boolean, description: "true si el OVC no respondió y se sirvió un valor cacheado, posiblemente caducado (serve-stale-on-error)." }
