openapi: "3.1.0"

info:
  title: NORA Fiscal API
  version: "1.0.0"
  description: |
    ## Motor fiscal e inmobiliario para España

    La API de NORA calcula impuestos y costes de compraventa de inmuebles en todas las
    comunidades autónomas españolas: ITP, AJD, IVA, IGIC, IPSI, retención art. 211 LIRNR,
    gastos de notaría, registro y gestoría, y beneficios fiscales autonómicos.

    ### Autenticación

    Todas las rutas autenticadas usan **Bearer token** con tu API key de partner:

    ```
    Authorization: Bearer nora_live_XXXXXXXXXXXXXXXXXXXX
    ```

    Contacta con api@noraapp.es para obtener tu API key.

    ### Idempotencia

    Los endpoints `POST /v1/tax/calculate` y `POST /v1/pdf/render` requieren la cabecera
    `Idempotency-Key`. Usa un UUID v4 único por operación. Repitiendo la misma key en los
    próximos 5 minutos devolverá la misma respuesta cacheada sin volver a calcular.

    ### Rate limiting

    60 requests por minuto por partner. La respuesta `429` incluye cuándo puedes reintentar.

    ### Webhooks

    Configura una URL en tu partner config para recibir eventos `calculation.completed`,
    `pdf.ready` y `pdf.failed` firmados con HMAC-SHA256.

    ### Integración con CRMs

    La API es REST puro: cualquier sistema que pueda hacer una llamada HTTP POST puede
    integrarse. Para HubSpot existe una integración nativa vía `/v1/integrations/hubspot/`.
    Para Salesforce, Pipedrive, Zoho o cualquier otro CRM usa el endpoint genérico
    `/v1/tax/calculate` directamente.

  contact:
    name: NORA API Support
    email: api@noraapp.es
    url: https://app.noraapp.es/api-docs/

  license:
    name: Propietario — uso bajo contrato de partner
    url: https://app.noraapp.es/legal.html

servers:
  - url: https://api.noraapp.es
    description: Producción
  - url: http://localhost:4545
    description: Desarrollo local

tags:
  - name: Sistema
    description: Health, versión y estado del servidor
  - name: Motor fiscal
    description: Validación y cálculo de impuestos inmobiliarios
  - name: PDF
    description: Generación y descarga de informes PDF
  - name: Cuenta
    description: Uso y cuota del partner
  - name: HubSpot
    description: Integración nativa con HubSpot CRM
  - name: Admin
    description: Gestión de partners y claves (solo administradores)

# ─────────────────────────── PATHS ───────────────────────────────────────────

paths:

  /health:
    get:
      operationId: getHealth
      summary: Estado del servidor
      description: Comprueba si el servidor está en marcha. No requiere autenticación.
      tags: [Sistema]
      responses:
        "200":
          description: Servidor operativo
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
              example:
                ok: true
                service: nora-api
                api_version: v1
                engine_version: compute_v1.0.0
                pdf_renderer:
                  available: true
                  mode: playwright
                  reason: null

  /v1/version:
    get:
      operationId: getVersion
      summary: Versión del motor
      description: Devuelve la versión del motor fiscal, la API y el estado del renderizador de PDF.
      tags: [Sistema]
      responses:
        "200":
          description: Información de versión
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionResponse"
              example:
                api_version: v1
                engine_version: compute_v1.0.0
                auth_model: partner_api_keys_separate_from_spa_users
                pdf_renderer:
                  available: true
                  mode: playwright
                  reason: null

  /v1/regions:
    get:
      operationId: listRegions
      summary: Listado de comunidades autónomas
      description: |
        Devuelve el catálogo completo de regiones soportadas por el motor fiscal.
        Úsalo para poblar selectores en tu CRM.

        Scope requerido: `regions:read`
      tags: [Motor fiscal]
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Catálogo de regiones
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegionsResponse"
              example:
                regions:
                  - code: ES-AN
                    name: Andalucia
                    scope_ccaa: ccaa
                  - code: ES-MD
                    name: Comunidad de Madrid
                    scope_ccaa: ccaa
                  - code: ES-CN
                    name: Canarias
                    scope_ccaa: ccaa
                  - code: ES-CT
                    name: Cataluna
                    scope_ccaa: ccaa
                  - code: ES-VC
                    name: Comunitat Valenciana
                    scope_ccaa: ccaa
                  - code: ES-BI
                    name: Bizkaia
                    scope_ccaa: foral
                  - code: ES-SS
                    name: Gipuzkoa
                    scope_ccaa: foral
                  - code: ES-VI
                    name: Araba
                    scope_ccaa: foral
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/tax/validate:
    post:
      operationId: validateTaxInput
      summary: Validar datos de entrada
      description: |
        Valida el objeto `input` sin ejecutar el cálculo. Útil para dar feedback
        inmediato al agente en el CRM antes de llamar a `calculate`.

        Devuelve:
        - `ok: true` si el input es válido y el cálculo puede ejecutarse
        - `ok: false` con lista de errores, campos que faltan y las siguientes preguntas
          que el usuario debería responder (`next_questions`)

        Scope requerido: `tax:validate`
      tags: [Motor fiscal]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaxValidateRequest"
            example:
              input:
                region_code: ES-AN
                transmission: used
                property_type: vivienda
                price: 250000
      responses:
        "200":
          description: Resultado de validación (ok o con errores)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaxValidateResponse"
              examples:
                valid:
                  summary: Input válido
                  value:
                    ok: true
                    errors: []
                    missing_fields:
                      blocking: []
                      recommended: [is_habitual, hasMortgage, age_years, seller_non_resident]
                      conditional: []
                    next_questions:
                      - field: is_habitual
                        level: recommended
                        label: Vivienda habitual
                        question: Indica si sera vivienda habitual del comprador.
                    requirements:
                      blocking: []
                      recommended: []
                      conditional: []
                invalid:
                  summary: Input con errores
                  value:
                    ok: false
                    errors:
                      - path: region_code
                        code: ERR_REQUIRED
                        message: region_code es obligatorio
                      - path: price
                        code: ERR_REQUIRED
                        message: price es obligatorio
                    missing_fields:
                      blocking: [region_code, transmission, price]
                      recommended: []
                      conditional: []
                    next_questions:
                      - field: region_code
                        level: blocking
                        label: Comunidad autonoma
                        question: Indica la comunidad autonoma donde se firma la compraventa.
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/tax/calculate:
    post:
      operationId: calculateTax
      summary: Calcular impuestos y costes
      description: |
        **Endpoint principal.** Recibe los datos de la operación inmobiliaria y devuelve
        todos los impuestos y costes desglosados.

        Calcula:
        - **ITP** (Impuesto de Transmisiones Patrimoniales) para vivienda usada
        - **IVA + AJD** para obra nueva
        - **IGIC** para Canarias
        - **IPSI** para Ceuta y Melilla
        - **Retención art. 211 LIRNR** si el vendedor es no residente
        - **Gastos** de notaría, registro, gestoría y tasación
        - **Beneficios autonómicos**: tipos reducidos por edad, familia numerosa,
          discapacidad, vivienda habitual, VPO, etc. en todas las comunidades
        - **Hipoteca**: costes financieros, intereses totales, AJD hipotecario (pagado
          por el banco desde 2018)

        Requiere cabecera `Idempotency-Key` (UUID v4). El resultado se cachea 5 minutos.

        Si pasas `output.pdf: true`, también genera el informe PDF en la misma llamada
        (requiere scope `pdf:render` y cuota disponible).

        Scope requerido: `tax:calculate`
      tags: [Motor fiscal]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TaxCalculateRequest"
            examples:
              minimal:
                summary: Mínimo obligatorio (vivienda usada en Madrid)
                value:
                  input:
                    region_code: ES-MD
                    transmission: used
                    property_type: vivienda
                    price: 350000
              completo_con_hipoteca:
                summary: Con hipoteca y beneficios (Andalucía)
                value:
                  input:
                    region_code: ES-AN
                    transmission: used
                    property_type: vivienda
                    price: 250000
                    is_habitual: true
                    is_young: true
                    age_years: 32
                    is_large_family: false
                    has_disc_33: false
                    seller_non_resident: false
                    hasMortgage: true
                    mortgageState: "yes"
                    appraisal: 270000
                    downpayment: 50000
                    loan_amount: 200000
                    ltv_pct: 80
                    fin_term: 360
                    rate_type: fixed
                    fixed_tin_pct: 3.2
                    opening_fee_pct: 0
                    linked_products_bonus: 0.3
                  output:
                    pdf: false
              obra_nueva_canarias:
                summary: Obra nueva en Canarias (IGIC 7%)
                value:
                  input:
                    region_code: ES-CN
                    transmission: new
                    property_type: vivienda
                    price: 300000
                    is_habitual: true
                    island_code: LP
              con_pdf:
                summary: Con generación de PDF incluida
                value:
                  input:
                    region_code: ES-CT
                    transmission: used
                    property_type: vivienda
                    price: 420000
                    is_habitual: true
                    seller_non_resident: false
                  output:
                    pdf: true
                    filename: estudio-fiscal-cliente.pdf
      responses:
        "200":
          description: Resultado del cálculo fiscal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaxCalculateResponse"
              examples:
                sin_pdf:
                  summary: Respuesta sin PDF
                  value:
                    result:
                      region:
                        code: ES-AN
                        name: Andalucia
                        scope_ccaa: ccaa
                      price: 250000
                      transmission: used
                      property_type: vivienda
                      vpo: null
                      taxes:
                        itp: 17500
                        ajd: 0
                      totals:
                        impuestos: 17500
                        gastos: 2850
                        downpayment: 50000
                        coste_adquisicion: 20350
                        fondos_totales: 70350
                      costs:
                        notaria: 1150
                        registro: 580
                        gestoria: 620
                        tasacion: 500
                      notes:
                        itp_rate: 0.07
                        tax_note: Tipo reducido 3.5% jóvenes <35 años VH ≤150.000 €
                      meta:
                        engine_version: compute_v1.0.0
                        calculation_hash: a1b2c3d4e5f6...
                        region_version: "2025-01"
                        rules_version: "2025-01"
                        benefits_version: "2025-01"
                        as_of: "2026-03-28"
                con_pdf:
                  summary: Respuesta con PDF generado
                  value:
                    result:
                      region:
                        code: ES-AN
                        name: Andalucia
                        scope_ccaa: ccaa
                      price: 250000
                      transmission: used
                      property_type: vivienda
                      taxes:
                        itp: 8750
                      totals:
                        impuestos: 8750
                        gastos: 2850
                        coste_adquisicion: 11600
                      meta:
                        calculation_hash: a1b2c3d4e5f6...
                        as_of: "2026-03-28"
                    pdf:
                      requested: true
                      status: ready
                      billable: true
                      usage_event_id: evt_abc123
                      filename: estudio-fiscal-cliente.pdf
                      job_id: job_xyz789
                      content_type: application/pdf
                      size_bytes: 142350
                      expires_at: "2026-03-28T14:30:00.000Z"
                      download_url: /v1/pdf/jobs/job_xyz789/file
                      public_download_url: /v1/pdf/public/job_xyz789?token=eyJ...
                      persistent_file_id: file_pqr456
                      persistent_download_url: /v1/pdf/files/file_pqr456
        "422":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimit"

  /v1/pdf/render:
    post:
      operationId: renderPdf
      summary: Generar informe PDF
      description: |
        Genera el informe PDF de un estudio fiscal. Acepta tanto un `input` para calcular
        en el momento, como un `calculation_hash` para regenerar el PDF de un cálculo
        previo almacenado.

        Requiere cabecera `Idempotency-Key`.

        Scopes requeridos: `tax:calculate` + `pdf:render`
      tags: [PDF]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PdfRenderRequest"
            examples:
              desde_input:
                summary: PDF desde input nuevo
                value:
                  input:
                    region_code: ES-MD
                    transmission: used
                    property_type: vivienda
                    price: 350000
                    is_habitual: true
                  output:
                    filename: informe-cliente-madrid.pdf
                    lang: es
                    pdf_options:
                      headerRightLogoUrl: https://mi-inmobiliaria.com/logo.png
                      headerRightLogoH: 24
              desde_hash:
                summary: PDF desde cálculo almacenado
                value:
                  calculation_hash: a1b2c3d4e5f6...
                  output:
                    filename: informe-cliente.pdf
      responses:
        "200":
          description: PDF generado correctamente
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PdfRenderResponse"
        "402":
          description: Cuota de PDFs agotada
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: ERR_PDF_QUOTA_EXCEEDED
                  message: PDF quota exceeded for this billing period
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/pdf/jobs/{jobId}/file:
    get:
      operationId: downloadPdfJob
      summary: Descargar PDF (autenticado)
      description: |
        Descarga el PDF generado por `job_id`. Solo el partner que lo generó puede
        descargarlo. El PDF expira después del tiempo indicado en `expires_at`.

        Scope requerido: `pdf:render`
      tags: [PDF]
      security:
        - BearerAuth: []
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
          example: job_xyz789
      responses:
        "200":
          description: Archivo PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/pdf/public/{jobId}:
    get:
      operationId: downloadPdfPublic
      summary: Descargar PDF (URL firmada)
      description: |
        Descarga un PDF usando una URL firmada temporalmente. No requiere autenticación.
        Usa `public_download_url` devuelta en la respuesta de `calculate` o `pdf/render`.
      tags: [PDF]
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Token firmado incluido en `public_download_url`
      responses:
        "200":
          description: Archivo PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          description: Token inválido o expirado

  /v1/pdf/files/{fileId}:
    get:
      operationId: downloadPdfPersistent
      summary: Descargar PDF (almacenamiento persistente)
      description: |
        Descarga un PDF desde el almacenamiento persistente usando su `file_id`.
        No requiere autenticación. Disponible indefinidamente (sin expiración).
      tags: [PDF]
      parameters:
        - name: fileId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Archivo PDF
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/calculations/{hash}:
    get:
      operationId: getCalculationByHash
      summary: Recuperar cálculo por hash
      description: |
        Recupera un cálculo fiscal previamente realizado por su `calculation_hash`.
        Útil para auditoría, para regenerar PDFs o para mostrar el detalle de una
        operación guardada en el CRM.

        Scope requerido: `tax:calculate`
      tags: [Motor fiscal]
      security:
        - BearerAuth: []
      parameters:
        - name: hash
          in: path
          required: true
          schema:
            type: string
          example: a1b2c3d4e5f6789012345678901234567890123456789012345678901234
      responses:
        "200":
          description: Cálculo encontrado
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalculationByHashResponse"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/usage:
    get:
      operationId: getUsage
      summary: Uso y cuota del partner
      description: |
        Devuelve el consumo actual del partner: número de PDFs generados, cuota incluida
        en el plan, si se permite sobrecuota y precio por PDF adicional.

        Scope requerido: `usage:read`
      tags: [Cuenta]
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Estado de uso
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
              example:
                partner:
                  partner_id: partner_inmobiliaria_xyz
                  display_name: Inmobiliaria XYZ
                usage:
                  pdf_renders_used: 23
                  pdf_renders_included: 100
                  pdf_renders_remaining: 77
                  overage_enabled: true
                  price_per_pdf_cents: 200
                  currency: EUR

  # ─── HubSpot ─────────────────────────────────────────────────────────────

  /v1/integrations/hubspot/setup/manifest:
    get:
      operationId: hubspotManifest
      summary: Manifiesto de configuración HubSpot
      description: |
        Devuelve el manifiesto de propiedades que NORA necesita en HubSpot.
        Úsalo para saber exactamente qué campos crear en tus Deals/Contacts.
      tags: [HubSpot]
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Manifiesto de configuración

  /v1/integrations/hubspot/setup/properties:
    post:
      operationId: hubspotSetupProperties
      summary: Crear propiedades en HubSpot automáticamente
      description: |
        Crea automáticamente todas las propiedades personalizadas que NORA necesita
        en tu portal HubSpot. Requiere un `private_app_token` con permisos de escritura
        en propiedades de CRM.
      tags: [HubSpot]
      security:
        - BearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                private_app_token:
                  type: string
                  description: Token de HubSpot Private App con permisos crm.schemas.write
      responses:
        "200":
          description: Propiedades creadas o ya existentes

  /v1/integrations/hubspot/tax/validate:
    post:
      operationId: hubspotValidate
      summary: Validar desde HubSpot
      description: |
        Igual que `POST /v1/tax/validate` pero acepta tanto el formato canónico (`input.*`)
        como propiedades planas de HubSpot (ej. `nora_region_code`, `nora_price`).
      tags: [HubSpot]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HubSpotTaxRequest"
      responses:
        "200":
          $ref: "#/components/responses/ValidateOk"

  /v1/integrations/hubspot/tax/calculate:
    post:
      operationId: hubspotCalculate
      summary: Calcular desde HubSpot (con writeback)
      description: |
        Igual que `POST /v1/tax/calculate` pero acepta propiedades planas de HubSpot
        y puede hacer **writeback automático**: escribe los resultados fiscales de vuelta
        en el Deal de HubSpot si el partner tiene `writeback_enabled: true`.

        Requiere cabecera `Idempotency-Key`.
      tags: [HubSpot]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HubSpotTaxRequest"
      responses:
        "200":
          $ref: "#/components/responses/CalculateOk"

  /v1/integrations/hubspot/deal-sync:
    post:
      operationId: hubspotDealSync
      summary: Sincronizar Deal HubSpot completo
      description: |
        Flujo todo-en-uno para HubSpot: lee las propiedades del Deal, calcula los
        impuestos y escribe los resultados de vuelta en el mismo Deal. Ideal para
        automatizaciones de workflows en HubSpot.

        Requiere cabecera `Idempotency-Key`.
      tags: [HubSpot]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                deal_id:
                  type: string
                  description: ID del Deal en HubSpot
                  example: "12345678"
              required: [deal_id]
      responses:
        "200":
          $ref: "#/components/responses/CalculateOk"

  # ─── Admin ────────────────────────────────────────────────────────────────

  /v1/admin/partners:
    get:
      operationId: adminListPartners
      summary: Listar partners
      tags: [Admin]
      security:
        - AdminKey: []
      responses:
        "200":
          description: Lista de partners

  /v1/admin/partners/{partnerId}:
    get:
      operationId: adminGetPartner
      summary: Detalle de un partner
      tags: [Admin]
      security:
        - AdminKey: []
      parameters:
        - name: partnerId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Datos del partner

  /v1/admin/usage:
    get:
      operationId: adminUsage
      summary: Uso global de todos los partners
      tags: [Admin]
      security:
        - AdminKey: []
      responses:
        "200":
          description: Uso agregado

  /v1/admin/audit:
    get:
      operationId: adminAudit
      summary: Log de auditoría de cálculos
      tags: [Admin]
      security:
        - AdminKey: []
      responses:
        "200":
          description: Entradas de auditoría

  /v1/admin/metrics:
    get:
      operationId: adminMetrics
      summary: Métricas del servidor
      tags: [Admin]
      security:
        - AdminKey: []
      responses:
        "200":
          description: Métricas

  /v1/admin/keys:
    get:
      operationId: adminKeys
      summary: Listar API keys
      tags: [Admin]
      security:
        - AdminKey: []
      responses:
        "200":
          description: Lista de API keys

# ─────────────────────────── COMPONENTS ──────────────────────────────────────

components:

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key de partner. Formato `nora_live_XXXX` o `nora_test_XXXX`.
    AdminKey:
      type: apiKey
      in: header
      name: X-Admin-Key
      description: Clave de administrador (solo uso interno de NORA).

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        format: uuid
      description: |
        UUID v4 único por operación. Repitiendo la misma key devuelve la respuesta
        cacheada sin volver a calcular (durante 5 minutos).
      example: 550e8400-e29b-41d4-a716-446655440000

  responses:
    Unauthorized:
      description: API key inválida o sin scope suficiente
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: ERR_UNAUTHORIZED
              message: Invalid or missing API key
    NotFound:
      description: Recurso no encontrado
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: Input inválido
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: ERR_INPUT_INVALID
              message: input fiscal invalido
              details:
                errors:
                  - path: region_code
                    code: ERR_REQUIRED
                    message: region_code es obligatorio
                missing_fields:
                  blocking: [region_code]
                  recommended: []
                  conditional: []
    RateLimit:
      description: Demasiadas peticiones
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error:
              code: ERR_RATE_LIMIT
              message: Rate limit exceeded. Retry after 42 seconds.
    ValidateOk:
      description: Resultado de validación
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/TaxValidateResponse"
    CalculateOk:
      description: Resultado de cálculo
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/TaxCalculateResponse"

  schemas:

    # ── Sistema ──────────────────────────────────────────────────────────────

    HealthResponse:
      type: object
      properties:
        ok:
          type: boolean
        service:
          type: string
        api_version:
          type: string
        engine_version:
          type: string
        pdf_renderer:
          $ref: "#/components/schemas/PdfRendererStatus"

    VersionResponse:
      type: object
      properties:
        api_version:
          type: string
        engine_version:
          type: string
        auth_model:
          type: string
        pdf_renderer:
          $ref: "#/components/schemas/PdfRendererStatus"

    PdfRendererStatus:
      type: object
      properties:
        available:
          type: boolean
        mode:
          type: string
          enum: [playwright, unavailable]
        reason:
          type: ["string", "null"]

    # ── Input fiscal ─────────────────────────────────────────────────────────

    TaxInput:
      type: object
      description: |
        Datos de entrada del cálculo fiscal. Los campos marcados como **obligatorios**
        son los 4 mínimos para obtener un resultado. El resto mejora la precisión
        (beneficios autonómicos, costes hipotecarios, retenciones).
      required:
        - region_code
        - transmission
        - property_type
        - price
      properties:

        # ── Datos obligatorios ────────────────────────────────────────────

        region_code:
          type: string
          description: |
            Código ISO de la comunidad autónoma donde se firma la compraventa.
            Usa `GET /v1/regions` para el catálogo completo.
          enum:
            - ES-AN  # Andalucía
            - ES-AR  # Aragón
            - ES-AS  # Asturias
            - ES-IB  # Illes Balears
            - ES-CN  # Canarias
            - ES-CB  # Cantabria
            - ES-CM  # Castilla-La Mancha
            - ES-CL  # Castilla y León
            - ES-CT  # Cataluña
            - ES-VC  # Comunitat Valenciana
            - ES-EX  # Extremadura
            - ES-GA  # Galicia
            - ES-MD  # Comunidad de Madrid
            - ES-MC  # Región de Murcia
            - ES-NC  # Navarra
            - ES-PV  # País Vasco (legacy → usa ES-BI/ES-SS/ES-VI)
            - ES-RI  # La Rioja
            - ES-CE  # Ceuta
            - ES-ML  # Melilla
            - ES-BI  # Bizkaia
            - ES-SS  # Gipuzkoa
            - ES-VI  # Araba/Álava
          example: ES-AN

        transmission:
          type: string
          description: |
            Tipo de transmisión:
            - `used` — Segunda mano / segunda transmisión → tributa por ITP
            - `new` — Obra nueva / primera transmisión → tributa por IVA + AJD
          enum: [used, new]
          example: used

        property_type:
          type: string
          description: Tipo de inmueble
          enum:
            - vivienda  # Vivienda residencial (piso, casa, chalet…)
            - local     # Local comercial u oficina
            - suelo     # Terreno o solar
            - otros     # Garaje, trastero u otros
          default: vivienda
          example: vivienda

        price:
          type: number
          format: float
          description: Precio escriturado o precio de compra en euros
          minimum: 1
          maximum: 100000000
          example: 250000

        # ── Perfil del comprador (mejora beneficios autonómicos) ─────────

        is_habitual:
          type: boolean
          description: |
            ¿Es vivienda habitual del comprador?
            Activa bonificaciones en prácticamente todas las CCAA.
          example: true

        is_young:
          type: boolean
          description: |
            ¿Es comprador joven (generalmente <35 años)?
            Activa tipos reducidos en Andalucía, Madrid, Cataluña, etc.
          example: false

        age_years:
          type: ["number", "null"]
          description: Edad del comprador principal (para evaluar bonificación joven)
          minimum: 18
          maximum: 120
          example: 32

        is_large_family:
          type: boolean
          description: ¿Tiene condición de familia numerosa? Activa bonificaciones autonómicas
          example: false

        has_disc_33:
          type: boolean
          description: ¿Acredita discapacidad ≥33%? Activa tipos reducidos
          example: false

        has_disc_65:
          type: boolean
          description: ¿Acredita discapacidad ≥65%? Activa tipos aún más reducidos
          example: false

        is_first_home:
          type: boolean
          description: ¿Es la primera vivienda del comprador? Usado en algunas CCAA
          example: true

        is_single_parent_family:
          type: boolean
          description: ¿Es familia monoparental? Activa bonificaciones en CCAA que lo contemplan
          example: false

        is_gender_violence_victim:
          type: boolean
          description: ¿Es víctima de violencia de género? Activa exenciones específicas
          example: false

        seller_non_resident:
          type: boolean
          description: |
            ¿El vendedor es no residente fiscal en España?
            Activa la **retención del 3% art. 211 LIRNR** que el comprador retiene
            e ingresa en Hacienda.
          example: false

        # ── VPO y régimen especial ───────────────────────────────────────

        vpo:
          type: ["string", "null"]
          description: |
            ¿Es Vivienda de Protección Oficial?
            - `null` — No es VPO
            - `general` — VPO régimen general
            - `special` — VPO régimen especial / promoción pública
          enum: [null, general, special]
          example: null

        special_regime_code:
          type: ["string", "null"]
          description: Código de régimen especial autonómico si aplica
          example: null

        # ── Municipio (beneficios municipales específicos) ───────────────

        municipality_ine:
          type: ["string", "null"]
          description: |
            Código INE del municipio (5 dígitos). Permite aplicar beneficios
            fiscales específicos de municipios (ej. zonas despobladas en CLM, CYL).
          maxLength: 12
          example: "18087"

        municipality_name:
          type: ["string", "null"]
          description: Nombre del municipio (informativo, para el PDF)
          maxLength: 120
          example: Granada

        # ── Datos de hipoteca (para costes financieros completos) ────────

        hasMortgage:
          type: boolean
          description: ¿La compra lleva hipoteca?
          example: true

        mortgageState:
          type: string
          description: |
            Estado de la hipoteca:
            - `yes` — Ya tiene hipoteca aprobada
            - `no` — No lleva hipoteca (compra al contado)
            - `in_process` — En trámite / pendiente de aprobación
          enum: [yes, no, in_process]
          example: "yes"

        appraisal:
          type: number
          description: Valor de tasación del inmueble en euros
          example: 270000

        downpayment:
          type: number
          description: Entrada / efectivo inicial aportado por el comprador en euros
          example: 50000

        loan_amount:
          type: number
          description: Capital hipotecario (importe del préstamo) en euros
          example: 200000

        ltv_pct:
          type: number
          description: LTV — Porcentaje de financiación sobre tasación (0-100)
          minimum: 0
          maximum: 100
          example: 80

        fin_term:
          type: number
          description: Plazo de la hipoteca en meses
          minimum: 0
          maximum: 600
          example: 360

        rate_type:
          type: string
          description: Tipo de interés
          enum: [fixed, variable, mixed]
          example: fixed

        fixed_tin_pct:
          type: number
          description: TIN fijo anual en porcentaje (para hipoteca fija)
          example: 3.2

        index_value_pct:
          type: number
          description: Valor del índice de referencia (Euribor) en porcentaje (para variable/mixta)
          example: 2.8

        spread_pct:
          type: number
          description: Diferencial hipotecario sobre el índice en porcentaje
          example: 0.75

        revision_months:
          type: number
          description: Periodicidad de revisión de la hipoteca variable en meses
          example: 12

        mixed_fixed_years:
          type: number
          description: Duración del tramo fijo inicial en una hipoteca mixta (años)
          example: 10

        mixed_fixed_tin_pct:
          type: number
          description: TIN del tramo fijo de la hipoteca mixta
          example: 2.5

        opening_fee_pct:
          type: number
          description: Comisión de apertura en porcentaje sobre el capital
          example: 0

        opening_fee_min_eur:
          type: number
          description: Mínimo en euros de la comisión de apertura
          example: 0

        linked_products_bonus:
          type: number
          description: Bonificación en puntos porcentuales por contratación de productos vinculados
          example: 0.3

        mortgage_fixed_costs_eur:
          type: number
          description: Gastos fijos de hipoteca asumidos por el comprador en euros
          example: 0

        is_replacement_home:
          type: boolean
          description: ¿Reemplaza a la vivienda habitual anterior? (activa exenciones en algunas CCAA)
          example: false

        is_large_holder:
          type: boolean
          description: ¿El comprador es gran tenedor de inmuebles? (afecta a Catalunya)
          example: false

        is_whole_residential_building_purchase:
          type: boolean
          description: ¿Es compra de edificio residencial completo? (afecta al ITP en Cat.)
          example: false

        island_code:
          type: ["string", "null"]
          description: |
            Código de isla para Canarias o Baleares.
            Canarias: `TF` Tenerife, `LP` La Palma, `GC` Gran Canaria, `LZ` Lanzarote,
            `FV` Fuerteventura, `HI` El Hierro, `GO` La Gomera.
          example: LP

        # ── Financiero extra ─────────────────────────────────────────────

        household_monthly_net_income_eur:
          type: number
          description: Ingresos netos mensuales del hogar en euros (para esfuerzo financiero)
          example: 4500

        available_cash_eur:
          type: number
          description: Ahorro o caja disponible del comprador para cubrir firma y cierre en euros
          example: 80000

    # ── Resultado fiscal ─────────────────────────────────────────────────────

    TaxResult:
      type: object
      description: Resultado completo del motor fiscal
      properties:
        region:
          type: object
          properties:
            code:
              type: string
              example: ES-AN
            name:
              type: string
              example: Andalucia
            scope_ccaa:
              type: string
              enum: [ccaa, foral, special]
              example: ccaa
        price:
          type: number
          description: Precio escriturado en euros
          example: 250000
        transmission:
          type: string
          enum: [used, new]
        property_type:
          type: string
        vpo:
          type: ["string", "null"]
        taxes:
          type: object
          description: Impuestos en euros
          properties:
            itp:
              type: number
              description: ITP (Impuesto de Transmisiones Patrimoniales) en euros
              example: 17500
            ajd:
              type: number
              description: AJD (Actos Jurídicos Documentados) compraventa en euros
              example: 0
            iva:
              type: number
              description: IVA obra nueva en euros
              example: 0
            igic:
              type: number
              description: IGIC (Canarias) en euros
              example: 0
            ipsi:
              type: number
              description: IPSI (Ceuta/Melilla) en euros
              example: 0
            retencion211:
              type: number
              description: Retención art. 211 LIRNR (vendedor no residente) en euros
              example: 0
        totals:
          type: object
          description: Totales consolidados en euros
          properties:
            impuestos:
              type: number
              description: Total impuestos (ITP/IVA + AJD + retención + IGIC/IPSI)
              example: 17500
            gastos:
              type: number
              description: Total gastos de tramitación (notaría + registro + gestoría + tasación)
              example: 2850
            downpayment:
              type: number
              description: Entrada aportada por el comprador
              example: 50000
            coste_adquisicion:
              type: number
              description: Coste total de adquisición (impuestos + gastos)
              example: 20350
            fondos_totales:
              type: number
              description: Fondos totales necesarios (entrada + impuestos + gastos)
              example: 70350
            intereses:
              type: number
              description: Total intereses de la hipoteca a lo largo del plazo
            hipoteca_extra:
              type: number
              description: Gastos adicionales hipotecarios
            coste_total_con_intereses:
              type: number
              description: Coste total incluyendo intereses
            a_vendedor:
              type: number
              description: Importe total que va al vendedor
        costs:
          type: object
          description: Gastos de tramitación en euros (estimaciones orientativas)
          properties:
            notaria:
              type: number
              example: 1150
            registro:
              type: number
              example: 580
            gestoria:
              type: number
              example: 620
            tasacion:
              type: number
              example: 500
        notes:
          type: object
          description: Notas del motor sobre los tipos aplicados
          properties:
            itp_rate:
              type: number
              description: Tipo ITP aplicado (decimal, ej. 0.07 = 7%)
              example: 0.07
            ajd_rate:
              type: number
              description: Tipo AJD aplicado
            iva_rate:
              type: number
            igic_rate:
              type: number
            tax_note:
              type: string
              description: Explicación del tipo aplicado (beneficio activo si lo hay)
              example: Tipo reducido 3.5% jóvenes <35 años VH ≤150.000 €
            costs_note:
              type: string
        meta:
          type: object
          description: Metadatos del cálculo para auditoría e idempotencia
          properties:
            engine_version:
              type: string
              example: compute_v1.0.0
            calculation_hash:
              type: string
              description: Hash SHA-256 del cálculo. Único por combinación de input + versión del motor.
              example: a1b2c3d4e5f6789012345678901234567890abcd
            region_version:
              type: string
              description: Versión de las reglas fiscales de la comunidad aplicadas
              example: "2025-01"
            rules_version:
              type: string
            benefits_version:
              type: string
            as_of:
              type: string
              format: date
              description: Fecha de referencia usada para el cálculo
              example: "2026-03-28"

    # ── PDF ──────────────────────────────────────────────────────────────────

    PdfResult:
      type: object
      properties:
        requested:
          type: boolean
        status:
          type: string
          enum: [ready, renderer_unavailable, failed]
        billable:
          type: boolean
        usage_event_id:
          type: string
        filename:
          type: string
          example: estudio-fiscal-cliente.pdf
        job_id:
          type: string
        content_type:
          type: string
          example: application/pdf
        size_bytes:
          type: number
        expires_at:
          type: string
          format: date-time
        download_url:
          type: string
          description: URL autenticada (requiere Bearer token)
        public_download_url:
          type: string
          description: URL firmada temporalmente (sin autenticación)
        persistent_file_id:
          type: string
        persistent_download_url:
          type: string
          description: URL persistente sin expiración

    # ── Requests ─────────────────────────────────────────────────────────────

    TaxValidateRequest:
      type: object
      required: [input]
      properties:
        input:
          $ref: "#/components/schemas/TaxInput"

    TaxCalculateRequest:
      type: object
      required: [input]
      properties:
        input:
          $ref: "#/components/schemas/TaxInput"
        as_of:
          type: string
          format: date
          description: |
            Fecha de referencia para el cálculo (YYYY-MM-DD). Si no se indica,
            se usa la fecha actual. Útil para calcular con reglas de una fecha concreta.
          example: "2026-03-28"
        calculation_hash:
          type: string
          description: Hash de un cálculo previo almacenado (alternativa a `input`)
        output:
          type: object
          properties:
            pdf:
              type: boolean
              description: |
                Si `true`, genera el informe PDF en la misma llamada.
                Requiere scope `pdf:render` y cuota disponible.
              default: false
            filename:
              type: string
              description: Nombre del fichero PDF generado
              example: estudio-fiscal-cliente.pdf
            lang:
              type: string
              description: Idioma del PDF
              enum: [es, en, fr, de, it, ru]
              default: es
            pdf_options:
              type: object
              properties:
                headerRightLogoUrl:
                  type: string
                  format: uri
                  description: URL del logo de tu empresa para la cabecera del PDF
                  example: https://mi-agencia.com/logo.png
                headerRightLogoH:
                  type: number
                  description: Altura del logo en píxeles
                  default: 20
                pageNumbers:
                  type: boolean
                  default: true

    PdfRenderRequest:
      type: object
      properties:
        input:
          $ref: "#/components/schemas/TaxInput"
        calculation_hash:
          type: string
          description: Hash de cálculo previo (alternativa a `input`)
        output:
          type: object
          properties:
            filename:
              type: string
            lang:
              type: string
              enum: [es, en, fr, de, it, ru]
              default: es
            pdf_options:
              type: object
              properties:
                headerRightLogoUrl:
                  type: string
                headerRightLogoH:
                  type: number

    HubSpotTaxRequest:
      type: object
      description: |
        Acepta propiedades en formato HubSpot (planas) o en formato canónico (`input.*`).

        **Formato plano HubSpot** — propiedades tal como llegan del Deal:
        ```json
        {
          "nora_region_code": "ES-AN",
          "nora_price": "250000",
          "nora_transmission": "used",
          "nora_is_habitual": "true",
          "hs_object_id": "12345678"
        }
        ```

        **Formato canónico** — el mismo `input` que usarías en `/v1/tax/calculate`:
        ```json
        {
          "input": {
            "region_code": "ES-AN",
            "price": 250000,
            "transmission": "used",
            "is_habitual": true
          }
        }
        ```
      properties:
        input:
          $ref: "#/components/schemas/TaxInput"
        nora_region_code:
          type: string
        nora_price:
          type: ["string", "number"]
        nora_transmission:
          type: string
        nora_property_type:
          type: string
        nora_is_habitual:
          type: ["string", "boolean"]
        nora_is_young:
          type: ["string", "boolean"]
        nora_has_mortgage:
          type: ["string", "boolean"]
        hs_object_id:
          type: string
          description: ID del Deal en HubSpot (para el writeback)

    # ── Responses ────────────────────────────────────────────────────────────

    TaxValidateResponse:
      type: object
      properties:
        ok:
          type: boolean
          description: "`true` si el input es válido y el cálculo puede ejecutarse"
        errors:
          type: array
          items:
            type: object
            properties:
              path:
                type: string
              code:
                type: string
              message:
                type: string
        missing_fields:
          type: object
          properties:
            blocking:
              type: array
              items:
                type: string
              description: Campos obligatorios que faltan (el cálculo no puede ejecutarse sin ellos)
            recommended:
              type: array
              items:
                type: string
              description: Campos que mejoran la precisión y activan beneficios
            conditional:
              type: array
              items:
                type: string
              description: Campos relevantes cuando hay hipoteca
        next_questions:
          type: array
          description: Las 5 preguntas más importantes para completar el cálculo
          items:
            type: object
            properties:
              field:
                type: string
              level:
                type: string
                enum: [blocking, recommended, conditional]
              label:
                type: string
              question:
                type: string
        requirements:
          type: object

    TaxCalculateResponse:
      type: object
      properties:
        result:
          $ref: "#/components/schemas/TaxResult"
        pdf:
          $ref: "#/components/schemas/PdfResult"
          description: Solo presente si `output.pdf` era `true` en el request

    PdfRenderResponse:
      type: object
      properties:
        result:
          $ref: "#/components/schemas/TaxResult"
        pdf:
          $ref: "#/components/schemas/PdfResult"

    RegionsResponse:
      type: object
      properties:
        regions:
          type: array
          items:
            type: object
            properties:
              code:
                type: string
                example: ES-AN
              name:
                type: string
                example: Andalucia
              scope_ccaa:
                type: string
                enum: [ccaa, foral, special]

    UsageResponse:
      type: object
      properties:
        partner:
          type: object
          properties:
            partner_id:
              type: string
            display_name:
              type: string
        usage:
          type: object

    CalculationByHashResponse:
      type: object
      properties:
        calculation:
          type: object
          properties:
            calculation_hash:
              type: string
            input:
              $ref: "#/components/schemas/TaxInput"
            result:
              $ref: "#/components/schemas/TaxResult"
            latest_pdf:
              $ref: "#/components/schemas/PdfResult"
            audit:
              type: object
              properties:
                request_id:
                  type: string
                route:
                  type: string
                created_at:
                  type: string
                  format: date-time
                engine_version:
                  type: string
                region_version:
                  type: string
                benefits_version:
                  type: string

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: Código de error máquina-legible
              example: ERR_INPUT_INVALID
            message:
              type: string
              description: Descripción legible del error
            details:
              description: Información adicional (errores de validación, etc.)
            request_id:
              type: string
              description: ID de la petición para debug

# ─────────────────────────── WEBHOOKS ────────────────────────────────────────

webhooks:
  calculation.completed:
    post:
      summary: Cálculo completado
      description: |
        Se dispara cada vez que se completa un cálculo fiscal (con o sin PDF).

        **Verificación de firma:**
        ```
        X-Nora-Webhook-Signature: sha256=<hex>
        ```
        El HMAC-SHA256 se calcula sobre `<timestamp>.<body>` usando tu webhook secret.

        ```javascript
        const expected = createHmac('sha256', webhookSecret)
          .update(`${timestamp}.${body}`)
          .digest('hex');
        const isValid = `sha256=${expected}` === signature;
        ```
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            example:
              id: 550e8400-e29b-41d4-a716-446655440000
              type: calculation.completed
              partner_id: partner_inmobiliaria_xyz
              request_id: req_abc123
              calculation_hash: a1b2c3d4e5f6...
              route: POST /v1/tax/calculate
              created_at: "2026-03-28T10:30:00.000Z"
              data:
                engine_version: compute_v1.0.0
                region_version: "2025-01"
      responses:
        "200":
          description: Webhook recibido correctamente

  pdf.ready:
    post:
      summary: PDF listo para descargar
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
            example:
              id: 660f9511-f3ac-52e5-b827-557766551111
              type: pdf.ready
              partner_id: partner_inmobiliaria_xyz
              request_id: req_abc123
              calculation_hash: a1b2c3d4e5f6...
              created_at: "2026-03-28T10:30:05.000Z"
              data:
                pdf:
                  status: ready
                  job_id: job_xyz789
                  download_url: /v1/pdf/jobs/job_xyz789/file
                  public_download_url: /v1/pdf/public/job_xyz789?token=eyJ...
                  persistent_download_url: /v1/pdf/files/file_pqr456
                  size_bytes: 142350
                  filename: estudio-fiscal-cliente.pdf
      responses:
        "200":
          description: Webhook recibido

  pdf.failed:
    post:
      summary: Error al generar el PDF
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Webhook recibido

components:
  schemas:
    WebhookEvent:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: ID único del evento webhook
        type:
          type: string
          enum: [calculation.completed, pdf.ready, pdf.failed]
        partner_id:
          type: string
        request_id:
          type: string
        calculation_hash:
          type: ["string", "null"]
        route:
          type: ["string", "null"]
        created_at:
          type: string
          format: date-time
        data:
          type: ["object", "null"]
