openapi: 3.0.3
info:
  title: VeriFactu SaaS API
  version: 1.0.0
  description: |
    API REST para la plataforma VeriFactu SaaS de ALMC.es.

    Permite a desarrolladores integrar la facturacion electronica conforme al sistema VeriFactu de la AEAT
    directamente desde sus aplicaciones. Soporta la creacion, envio, anulacion y consulta de facturas,
    gestion de certificados digitales, series de facturacion, claves API y monitorizacion de uso.

    ## Autenticacion

    Todas las peticiones autenticadas requieren una cabecera `X-Api-Key` con el formato:

    ```
    X-Api-Key: <key>.<secret>
    ```

    Las credenciales se obtienen al registrarse o creando nuevas claves API desde el endpoint correspondiente.

    ## Entornos

    | Entorno    | URL base                                       | Descripcion                          |
    |------------|------------------------------------------------|--------------------------------------|
    | Produccion | `https://almc.es/api/verifactu/v1`             | Envio real a la AEAT                 |
    | Local dev  | `https://almc.es.test/api/verifactu/v1`        | Entorno local Laragon                |

    ## Formato de respuestas

    Todas las respuestas exitosas siguen esta estructura:

    ```json
    {
      "success": true,
      "data": { ... },
      "meta": {
        "request_id": "uuid",
        "timestamp": "ISO-8601",
        "version": "1.0"
      }
    }
    ```

    Los errores siguen el formato RFC 7807:

    ```json
    {
      "success": false,
      "error": {
        "type": "https://api.verifactu.almc.es/errors/...",
        "title": "Error Title",
        "status": 422,
        "detail": "Descripcion detallada del error",
        "aeat_code": "codigo AEAT si aplica",
        "errors": { "campo": ["mensaje"] }
      },
      "meta": {
        "request_id": "uuid",
        "timestamp": "ISO-8601"
      }
    }
    ```

    ## Limites de tasa (Rate Limiting)

    - Registro: 5 peticiones/minuto
    - Login: 10 peticiones/minuto
    - Endpoints autenticados: segun plan contratado

    ## Procesamiento asincrono

    Las operaciones de envio a la AEAT (submit, cancel, rectify, query) se procesan de forma asincrona.
    Se devuelve un `job_id` que puede consultarse en `GET /v1/jobs/{uuid}` para conocer el estado.

  contact:
    name: ALMC Soporte API
    email: api@almc.es
    url: https://almc.es/verifactu
  license:
    name: Propietaria
    url: https://almc.es/verifactu/terminos

servers:
  - url: https://almc.es/api/verifactu/v1
    description: Produccion (URL canonica)
  - url: https://almc.es.test/api/verifactu/v1
    description: Local (Laragon dev)

tags:
  - name: Health
    description: Estado del servicio
  - name: Auth
    description: Registro y autenticacion de tenants
  - name: API Keys
    description: Gestion de claves API
  - name: NIF Validation
    description: Validacion de NIF / CIF / DNI / NIE espanol (algoritmo local + VIES opcional)
  - name: Settings
    description: Configuracion del tenant
  - name: Certificates
    description: Certificados digitales (.pfx/.p12) para firma electronica
  - name: Series
    description: Series de facturacion
  - name: Invoices
    description: Facturas electronicas VeriFactu
  - name: Jobs
    description: Estado de trabajos asincronos
  - name: Usage
    description: Consumo y limites del plan
  - name: Events
    description: Registro de eventos y auditoria

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: "Clave API en formato `key.secret`. Se obtiene al registrarse o creando nuevas claves."

  schemas:
    # ── Meta ──
    Meta:
      type: object
      properties:
        request_id:
          type: string
          format: uuid
          description: Identificador unico de la peticion
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        timestamp:
          type: string
          format: date-time
          description: Marca de tiempo ISO 8601
          example: "2026-04-06T10:30:00+02:00"
        version:
          type: string
          description: Version de la API
          example: "1.0"

    Pagination:
      type: object
      properties:
        current_page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 25
        total:
          type: integer
          example: 150
        last_page:
          type: integer
          example: 6

    # ── Error ──
    ErrorResponse:
      type: object
      required:
        - success
        - error
        - meta
      properties:
        success:
          type: boolean
          enum: [false]
        error:
          $ref: '#/components/schemas/ErrorDetail'
        meta:
          type: object
          properties:
            request_id:
              type: string
              format: uuid
            timestamp:
              type: string
              format: date-time

    ErrorDetail:
      type: object
      required:
        - type
        - title
        - status
        - detail
      properties:
        type:
          type: string
          format: uri
          description: URI que identifica el tipo de error
          example: "https://api.verifactu.almc.es/errors/validation-error"
        title:
          type: string
          description: Titulo corto del error
          example: "Validation Error"
        status:
          type: integer
          description: Codigo HTTP del error
          example: 422
        detail:
          type: string
          description: Descripcion detallada del error
          example: "El campo recipient_nif es obligatorio."
        aeat_code:
          type: string
          nullable: true
          description: Codigo de error de la AEAT si aplica
          example: "4100"
        errors:
          type: object
          nullable: true
          description: Errores de validacion por campo
          additionalProperties:
            type: array
            items:
              type: string
          example:
            recipient_nif: ["El campo recipient_nif es obligatorio."]

    # ── Health ──
    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [healthy, unhealthy]
          description: Estado general del servicio
        checks:
          type: object
          properties:
            database:
              type: string
              enum: [ok, error]
            redis:
              type: string
              enum: [ok, error]
            queue:
              type: string
              enum: [ok, degraded, unknown]
            aeat_sandbox:
              type: string
              enum: [ok, degraded]
            aeat_production:
              type: string
              enum: [ok, degraded]
        timestamp:
          type: string
          format: date-time

    # ── Tenant ──
    TenantInfo:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        company_name:
          type: string
          example: "Mi Empresa S.L."
        email:
          type: string
          format: email
          example: "admin@miempresa.es"
        status:
          type: string
          enum: [trial, active, suspended, cancelled]
          example: "trial"
        trial_ends_at:
          type: string
          format: date-time
          nullable: true
        plan:
          type: string
          example: "free"

    # ── API Key ──
    ApiKeyFull:
      type: object
      description: Clave API completa (solo visible al crearla)
      properties:
        key:
          type: string
          description: Clave publica
          example: "vf_sb_a1b2c3d4e5f6"
        secret:
          type: string
          description: Secreto (solo se muestra una vez)
          example: "sk_1234567890abcdef1234567890abcdef"
        id:
          type: integer
          example: 1
        name:
          type: string
          example: "Default Sandbox Key"
        prefix:
          type: string
          description: Prefijo de la clave para identificarla
          example: "vf_sb_a1b2"
        environment:
          type: string
          enum: [sandbox, production]
          example: "sandbox"
        note:
          type: string
          example: "Store these credentials securely. The secret will not be shown again."

    ApiKeySummary:
      type: object
      description: Resumen de clave API (sin secreto)
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: "Production Key"
        prefix:
          type: string
          example: "vf_pk_a1b2"
        environment:
          type: string
          enum: [sandbox, production]
        scopes:
          type: array
          nullable: true
          items:
            type: string
          example: ["invoices:write", "invoices:read"]
        last_used_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    # ── Certificate ──
    Certificate:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: "Certificado FNMT Empresa"
        subject_cn:
          type: string
          nullable: true
          example: "MI EMPRESA SL - B12345678"
        issuer_cn:
          type: string
          nullable: true
          example: "AC FNMT Usuarios"
        serial_number:
          type: string
          nullable: true
        fingerprint:
          type: string
          nullable: true
          description: SHA-256 fingerprint del certificado
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_to:
          type: string
          format: date-time
          nullable: true
        days_to_expiry:
          type: integer
          nullable: true
          example: 365
        is_active:
          type: boolean
          example: true
        is_expired:
          type: boolean
          example: false
        last_validated_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    CertificateValidation:
      type: object
      properties:
        valid:
          type: boolean
          example: true
        subject:
          type: string
          nullable: true
          example: "MI EMPRESA SL - B12345678"
        issuer:
          type: string
          nullable: true
          example: "AC FNMT Usuarios"
        valid_to:
          type: string
          format: date-time
          nullable: true
        days_to_expiry:
          type: integer
          nullable: true
          example: 365
        is_expired:
          type: boolean
          example: false

    CertificateExpiry:
      type: object
      properties:
        valid_from:
          type: string
          format: date-time
          nullable: true
        valid_to:
          type: string
          format: date-time
          nullable: true
        days_to_expiry:
          type: integer
          nullable: true
          example: 365
        is_expired:
          type: boolean
          example: false
        is_expiring_soon:
          type: boolean
          description: "true si expira en los proximos 30 dias"
          example: false

    # ── Series ──
    Series:
      type: object
      properties:
        id:
          type: integer
          example: 1
        code:
          type: string
          description: Codigo de la serie (unico por tenant)
          example: "F-2026"
        description:
          type: string
          nullable: true
          example: "Facturas 2026"
        current_counter:
          type: integer
          description: Ultimo numero asignado en esta serie
          example: 42
        is_active:
          type: boolean
          example: true
        created_at:
          type: string
          format: date-time

    # ── Invoice ──
    InvoiceSummary:
      type: object
      description: Resumen de factura (listados)
      properties:
        uuid:
          type: string
          format: uuid
        external_id:
          type: string
          nullable: true
          description: ID externo del sistema del cliente
          example: "ERP-2026-001"
        series_code:
          type: string
          example: "F-2026"
        number:
          type: integer
          example: 42
        invoice_number:
          type: string
          description: "Numero completo (serie + numero)"
          example: "F-2026/42"
        issue_date:
          type: string
          format: date
          example: "2026-04-06"
        invoice_type:
          type: string
          description: "Tipo de factura AEAT (F1=completa, F2=simplificada, R1=rectificativa...)"
          example: "F1"
        recipient_name:
          type: string
          example: "Cliente Ejemplo S.L."
        recipient_nif:
          type: string
          example: "B98765432"
        tax_base:
          type: number
          format: double
          example: 1000.00
        tax_rate:
          type: number
          format: double
          example: 21.00
        tax_amount:
          type: number
          format: double
          example: 210.00
        total:
          type: number
          format: double
          example: 1210.00
        status:
          type: string
          enum: [draft, queued, submitted, accepted, rejected, cancelled, error]
          description: |
            Estados de una factura:
            - `draft`: Borrador, editable
            - `queued`: En cola de procesamiento
            - `submitted`: Enviada a la AEAT
            - `accepted`: Aceptada por la AEAT
            - `rejected`: Rechazada por la AEAT
            - `cancelled`: Anulada
            - `error`: Error en el procesamiento
          example: "draft"
        submitted_at:
          type: string
          format: date-time
          nullable: true
        accepted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    InvoiceDetail:
      allOf:
        - $ref: '#/components/schemas/InvoiceSummary'
        - type: object
          properties:
            description:
              type: string
              nullable: true
              example: "Servicios de consultoria marzo 2026"
            operation_date:
              type: string
              format: date
              nullable: true
            recipient_country:
              type: string
              description: "Codigo pais ISO 3166-1 alpha-2"
              example: "ES"
            huella:
              type: string
              nullable: true
              description: "Hash de encadenamiento VeriFactu (huella digital)"
            retry_count:
              type: integer
              example: 0
            last_error:
              type: string
              nullable: true
            aeat_response:
              type: object
              nullable: true
              description: "Respuesta raw de la AEAT"
            metadata:
              type: object
              nullable: true
              description: "Metadatos personalizados del cliente"
              example:
                erp_id: "INV-2026-001"
                department: "ventas"
            items:
              type: array
              items:
                $ref: '#/components/schemas/InvoiceItem'
            tax_lines:
              type: array
              items:
                $ref: '#/components/schemas/TaxLine'

    InvoiceItem:
      type: object
      properties:
        description:
          type: string
          example: "Servicio de consultoria"
        quantity:
          type: number
          format: double
          example: 10.0
        unit_price:
          type: number
          format: double
          example: 100.00
        subtotal:
          type: number
          format: double
          example: 1000.00
        tax_rate:
          type: number
          format: double
          nullable: true
          example: 21.00

    TaxLine:
      type: object
      properties:
        regime_key:
          type: string
          description: "Clave de regimen fiscal (01=general, 02=exportacion...)"
          example: "01"
        operation_qualification:
          type: string
          description: "Calificacion de la operacion (S1=sujeta no exenta...)"
          example: "S1"
        tax_rate:
          type: number
          format: double
          example: 21.00
        tax_base:
          type: number
          format: double
          example: 1000.00
        tax_amount:
          type: number
          format: double
          example: 210.00
        surcharge_rate:
          type: number
          format: double
          nullable: true
          description: "Tipo de recargo de equivalencia"
        surcharge_amount:
          type: number
          format: double
          nullable: true
          description: "Cuota de recargo de equivalencia"

    # ── Invoice Create Request ──
    InvoiceCreateRequest:
      type: object
      required:
        - series_code
        - issue_date
        - recipient_name
        - recipient_nif
        - items
      properties:
        series_code:
          type: string
          maxLength: 20
          description: "Codigo de serie activa"
          example: "F-2026"
        issue_date:
          type: string
          format: date
          description: "Fecha de emision"
          example: "2026-04-06"
        operation_date:
          type: string
          format: date
          nullable: true
          description: "Fecha de operacion (si difiere de la de emision)"
        invoice_type:
          type: string
          maxLength: 10
          default: "F1"
          description: |
            Tipo de factura segun AEAT:
            - `F1`: Factura completa
            - `F2`: Factura simplificada (ticket)
            - `R1`: Factura rectificativa por diferencias
            - `R2`: Factura rectificativa por sustitucion
            - `R3`: Factura rectificativa por devolucion
            - `R4`: Factura rectificativa otros
            - `R5`: Factura rectificativa en facturas simplificadas
          example: "F1"
        description:
          type: string
          maxLength: 500
          nullable: true
          description: "Descripcion de la factura"
          example: "Servicios de consultoria marzo 2026"
        recipient_name:
          type: string
          maxLength: 255
          description: "Nombre o razon social del destinatario"
          example: "Cliente Ejemplo S.L."
        recipient_nif:
          type: string
          maxLength: 20
          description: "NIF/CIF del destinatario"
          example: "B98765432"
        recipient_country:
          type: string
          minLength: 2
          maxLength: 2
          default: "ES"
          description: "Codigo pais ISO 3166-1 alpha-2"
          example: "ES"
        external_id:
          type: string
          maxLength: 255
          nullable: true
          description: "ID externo para correlacionar con tu sistema"
          example: "ERP-2026-001"
        metadata:
          type: object
          nullable: true
          description: "Metadatos personalizados (JSON libre)"
          example:
            erp_id: "INV-2026-001"
            department: "ventas"
        items:
          type: array
          minItems: 1
          description: "Lineas de la factura (minimo 1)"
          items:
            $ref: '#/components/schemas/InvoiceItemInput'
        tax_lines:
          type: array
          nullable: true
          description: "Lineas de desglose fiscal. Si no se envian, se generan automaticamente."
          items:
            $ref: '#/components/schemas/TaxLineInput'
        auto_submit:
          type: boolean
          description: "Si es true, envia la factura a la AEAT automaticamente tras crearla. Si no se especifica, usa la config del tenant."

    InvoiceItemInput:
      type: object
      required:
        - description
        - quantity
        - unit_price
      properties:
        description:
          type: string
          maxLength: 500
          description: "Descripcion de la linea"
          example: "Servicio de consultoria"
        quantity:
          type: number
          format: double
          minimum: 0.001
          description: "Cantidad (minimo 0.001)"
          example: 10.0
        unit_price:
          type: number
          format: double
          description: "Precio unitario"
          example: 100.00
        tax_rate:
          type: number
          format: double
          nullable: true
          description: "Tipo impositivo de esta linea. Si no se indica, se calcula globalmente."
          example: 21.00

    TaxLineInput:
      type: object
      required:
        - tax_rate
        - tax_base
        - tax_amount
      properties:
        regime_key:
          type: string
          maxLength: 5
          default: "01"
          description: "Clave de regimen fiscal"
          example: "01"
        operation_qualification:
          type: string
          maxLength: 5
          default: "S1"
          description: "Calificacion de la operacion"
          example: "S1"
        tax_rate:
          type: number
          format: double
          description: "Tipo impositivo"
          example: 21.00
        tax_base:
          type: number
          format: double
          description: "Base imponible"
          example: 1000.00
        tax_amount:
          type: number
          format: double
          description: "Cuota tributaria"
          example: 210.00

    InvoiceUpdateRequest:
      type: object
      description: "Solo se pueden actualizar facturas en estado `draft`."
      properties:
        issue_date:
          type: string
          format: date
        operation_date:
          type: string
          format: date
          nullable: true
        description:
          type: string
          maxLength: 500
          nullable: true
        recipient_name:
          type: string
          maxLength: 255
        recipient_nif:
          type: string
          maxLength: 20
        recipient_country:
          type: string
          minLength: 2
          maxLength: 2
        external_id:
          type: string
          maxLength: 255
          nullable: true
        metadata:
          type: object
          nullable: true
        items:
          type: array
          minItems: 1
          description: "Si se envian items, se reemplazan todos los existentes."
          items:
            $ref: '#/components/schemas/InvoiceItemInput'

    # ── Job ──
    JobStatus:
      type: object
      properties:
        job_id:
          type: string
          format: uuid
        type:
          type: string
          enum: [submit, cancel, query, rectify]
          description: |
            Tipo de trabajo:
            - `submit`: Envio de factura a la AEAT
            - `cancel`: Anulacion de factura
            - `query`: Consulta de estado en la AEAT
            - `rectify`: Rectificacion de factura rechazada
        status:
          type: string
          enum: [pending, processing, completed, failed]
        attempts:
          type: integer
          example: 1
        result:
          type: object
          nullable: true
          description: "Resultado del trabajo (respuesta AEAT, errores, etc.)"
        invoice_uuid:
          type: string
          format: uuid
          nullable: true
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    # ── Settings ──
    Settings:
      type: object
      properties:
        environment:
          type: string
          enum: [sandbox, production]
          example: "sandbox"
        company_name:
          type: string
          example: "Mi Empresa S.L."
        company_nif:
          type: string
          example: "B12345678"
        certificate_id:
          type: integer
          nullable: true
          description: "ID del certificado digital activo"
        auto_submit:
          type: boolean
          description: "Enviar facturas automaticamente a la AEAT al crearlas"
          example: false
        webhook_url:
          type: string
          format: uri
          nullable: true
          description: "URL para recibir webhooks de eventos"
          example: "https://miapp.com/webhooks/verifactu"
        webhook_events:
          type: array
          nullable: true
          items:
            type: string
          description: "Tipos de evento a enviar por webhook"
          example: ["invoice.accepted", "invoice.rejected", "certificate.expiring"]
        system_name:
          type: string
          nullable: true
          description: "Nombre del software que genera las facturas"
        system_version:
          type: string
          nullable: true
          description: "Version del software"

    SettingsUpdateRequest:
      type: object
      properties:
        environment:
          type: string
          enum: [sandbox, production]
        company_name:
          type: string
          maxLength: 255
        company_nif:
          type: string
          maxLength: 20
        certificate_id:
          type: integer
          nullable: true
          description: "ID del certificado. Debe existir en vf_certificates."
        auto_submit:
          type: boolean
        webhook_url:
          type: string
          format: uri
          maxLength: 500
          nullable: true
        webhook_events:
          type: array
          nullable: true
          items:
            type: string

    # ── Usage ──
    UsageCurrent:
      type: object
      properties:
        period:
          type: string
          description: "Periodo en formato YYYY-MM"
          example: "2026-04"
        invoices_submitted:
          type: integer
          example: 45
        invoices_accepted:
          type: integer
          example: 43
        invoices_rejected:
          type: integer
          example: 2
        api_calls:
          type: integer
          example: 320
        limits:
          type: object
          properties:
            invoice_limit_monthly:
              type: integer
              description: "Limite mensual de facturas segun plan"
              example: 100
            remaining:
              type: integer
              nullable: true
              description: "Facturas restantes en el periodo (null si plan ilimitado)"
              example: 55

    UsageHistory:
      type: object
      properties:
        period:
          type: string
          example: "2026-03"
        invoices_submitted:
          type: integer
        invoices_accepted:
          type: integer
        invoices_rejected:
          type: integer
        api_calls:
          type: integer

    # ── Event ──
    EventSummary:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          description: "Tipo de evento"
          example: "invoice.submitted"
        severity:
          type: string
          enum: [info, warning, error, critical]
          example: "info"
        message:
          type: string
          example: "Factura F-2026/42 enviada a la AEAT"
        invoice_id:
          type: integer
          nullable: true
        request_id:
          type: string
          nullable: true
        ip_address:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    EventDetail:
      allOf:
        - $ref: '#/components/schemas/EventSummary'
        - type: object
          properties:
            payload:
              type: object
              nullable: true
              description: "Datos completos del evento (respuesta AEAT, etc.)"

    # ── Register ──
    RegisterRequest:
      type: object
      required:
        - company_name
        - nif
        - email
        - password
      properties:
        company_name:
          type: string
          maxLength: 255
          description: "Nombre o razon social de la empresa"
          example: "Mi Empresa S.L."
        nif:
          type: string
          maxLength: 20
          description: "NIF/CIF de la empresa"
          example: "B12345678"
        email:
          type: string
          format: email
          description: "Email del administrador (unico en el sistema)"
          example: "admin@miempresa.es"
        password:
          type: string
          format: password
          minLength: 8
          description: "Contrasena (minimo 8 caracteres)"
          example: "MiPassword123!"
        phone:
          type: string
          maxLength: 20
          nullable: true
          description: "Telefono de contacto"
          example: "+34 612 345 678"
        address:
          type: string
          maxLength: 1000
          nullable: true
          description: "Direccion fiscal"
          example: "Calle Gran Via 1, 28001 Madrid"

    LoginRequest:
      type: object
      required:
        - email
        - password
      properties:
        email:
          type: string
          format: email
          example: "admin@miempresa.es"
        password:
          type: string
          format: password
          example: "MiPassword123!"

    # ── API Key Create Request ──
    ApiKeyCreateRequest:
      type: object
      required:
        - name
        - environment
      properties:
        name:
          type: string
          maxLength: 100
          description: "Nombre descriptivo de la clave"
          example: "Backend Production"
        environment:
          type: string
          enum: [sandbox, production]
          description: "Entorno de la clave"
          example: "production"
        scopes:
          type: array
          nullable: true
          items:
            type: string
          description: "Permisos especificos (null = todos los permisos)"
          example: ["invoices:write", "invoices:read", "series:read"]
        ip_whitelist:
          type: array
          nullable: true
          items:
            type: string
          description: "Lista de IPs permitidas para esta clave"
          example: ["203.0.113.10", "198.51.100.0/24"]

    # ── Series Create Request ──
    SeriesCreateRequest:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          maxLength: 20
          description: "Codigo de la serie (unico por tenant)"
          example: "F-2026"
        description:
          type: string
          maxLength: 255
          nullable: true
          example: "Facturas ordinarias 2026"

    SeriesUpdateRequest:
      type: object
      properties:
        description:
          type: string
          maxLength: 255
          nullable: true
        is_active:
          type: boolean
          description: "Activar/desactivar la serie"

  responses:
    Unauthorized:
      description: "No autorizado - Clave API invalida o ausente"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              type: "https://api.verifactu.almc.es/errors/unauthorized"
              title: "Unauthorized"
              status: 401
              detail: "Invalid or missing API key."
              aeat_code: null
            meta:
              request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timestamp: "2026-04-06T10:30:00+02:00"

    Forbidden:
      description: "Prohibido - Sin permisos o limite alcanzado"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    NotFound:
      description: "Recurso no encontrado"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            success: false
            error:
              type: "https://api.verifactu.almc.es/errors/not-found"
              title: "Not Found"
              status: 404
              detail: "Resource not found."
              aeat_code: null
            meta:
              request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timestamp: "2026-04-06T10:30:00+02:00"

    ValidationError:
      description: "Error de validacion"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    Conflict:
      description: "Conflicto - La operacion no es posible en el estado actual"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

    TooManyRequests:
      description: "Demasiadas peticiones - Limite de tasa excedido"
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

paths:
  # ═══════════════════════════════════════════════════════════════════
  # HEALTH
  # ═══════════════════════════════════════════════════════════════════
  /health:
    get:
      tags: [Health]
      summary: Verificar estado del servicio
      description: |
        Endpoint publico (no requiere autenticacion) que devuelve el estado
        de los componentes del sistema: base de datos, Redis, colas de trabajo
        y conectividad con la AEAT.
      operationId: healthCheck
      security: []
      responses:
        '200':
          description: "Servicio operativo"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: "healthy"
                checks:
                  database: "ok"
                  redis: "ok"
                  queue: "ok"
                  aeat_sandbox: "ok"
                  aeat_production: "ok"
                timestamp: "2026-04-06T10:30:00+02:00"
        '503':
          description: "Servicio degradado o no disponible"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: "unhealthy"
                checks:
                  database: "ok"
                  redis: "error"
                  queue: "unknown"
                  aeat_sandbox: "ok"
                  aeat_production: "degraded"
                timestamp: "2026-04-06T10:30:00+02:00"

  # ═══════════════════════════════════════════════════════════════════
  # AUTH
  # ═══════════════════════════════════════════════════════════════════
  /auth/register:
    post:
      tags: [Auth]
      summary: Registrar nuevo tenant
      description: |
        Crea una nueva cuenta de tenant con plan gratuito y genera la primera
        clave API en entorno sandbox. El tenant comienza en estado `trial`
        con 14 dias de prueba.

        **Limite de tasa:** 5 peticiones por minuto.
      operationId: registerTenant
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
            example:
              company_name: "Mi Empresa S.L."
              nif: "B12345678"
              email: "admin@miempresa.es"
              password: "MiPassword123!"
              phone: "+34 612 345 678"
              address: "Calle Gran Via 1, 28001 Madrid"
      responses:
        '201':
          description: "Registro exitoso"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      tenant:
                        $ref: '#/components/schemas/TenantInfo'
                      api_key:
                        $ref: '#/components/schemas/ApiKeyFull'
                  meta:
                    $ref: '#/components/schemas/Meta'
              example:
                success: true
                data:
                  tenant:
                    uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    company_name: "Mi Empresa S.L."
                    email: "admin@miempresa.es"
                    status: "trial"
                    trial_ends_at: "2026-04-20T00:00:00+02:00"
                    plan: "free"
                  api_key:
                    key: "vf_sb_a1b2c3d4e5f6"
                    secret: "sk_1234567890abcdef1234567890abcdef"
                    name: "Default Sandbox Key"
                    prefix: "vf_sb_a1b2"
                    environment: "sandbox"
                    note: "Store these credentials securely. The secret will not be shown again."
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"
                  version: "1.0"
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          description: "Error de configuracion (plan gratuito no encontrado)"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /auth/login:
    post:
      tags: [Auth]
      summary: Autenticar tenant
      description: |
        Verifica las credenciales del tenant y devuelve informacion de la cuenta.
        No genera tokens de sesion; la autenticacion de la API se hace por claves API.

        **Limite de tasa:** 10 peticiones por minuto.
      operationId: loginTenant
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            example:
              email: "admin@miempresa.es"
              password: "MiPassword123!"
      responses:
        '200':
          description: "Autenticacion exitosa"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      tenant:
                        $ref: '#/components/schemas/TenantInfo'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          description: "Credenciales invalidas"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  type: "https://api.verifactu.almc.es/errors/authentication-failed"
                  title: "Authentication Failed"
                  status: 401
                  detail: "Invalid email or password."
                  aeat_code: null
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"
        '403':
          description: "Cuenta inactiva"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  # ═══════════════════════════════════════════════════════════════════
  # API KEYS
  # ═══════════════════════════════════════════════════════════════════
  /auth/api-keys:
    get:
      tags: [API Keys]
      summary: Listar claves API
      description: "Devuelve todas las claves API activas (no revocadas) del tenant."
      operationId: listApiKeys
      responses:
        '200':
          description: "Lista de claves API"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ApiKeySummary'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [API Keys]
      summary: Crear nueva clave API
      description: |
        Crea una nueva clave API. El numero maximo de claves depende del plan contratado.
        La clave secreta solo se muestra en esta respuesta; almacenala de forma segura.
      operationId: createApiKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreateRequest'
            example:
              name: "Backend Production"
              environment: "production"
              scopes: ["invoices:write", "invoices:read"]
              ip_whitelist: ["203.0.113.10"]
      responses:
        '201':
          description: "Clave API creada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/ApiKeyFull'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /auth/api-keys/{id}:
    delete:
      tags: [API Keys]
      summary: Revocar clave API
      description: "Revoca una clave API. La clave dejara de funcionar inmediatamente."
      operationId: revokeApiKey
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID de la clave API"
      responses:
        '200':
          description: "Clave revocada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "API key revoked."
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /auth/api-keys/{id}/rotate:
    post:
      tags: [API Keys]
      summary: Rotar clave API
      description: |
        Revoca la clave actual y genera una nueva con los mismos permisos, entorno y
        lista de IPs permitidas. Util para rotacion periodica de credenciales.
      operationId: rotateApiKey
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID de la clave API a rotar"
      responses:
        '201':
          description: "Nueva clave generada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      key:
                        type: string
                      secret:
                        type: string
                      id:
                        type: integer
                      name:
                        type: string
                      prefix:
                        type: string
                      old_key_id:
                        type: integer
                        description: "ID de la clave revocada"
                      note:
                        type: string
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ═══════════════════════════════════════════════════════════════════
  # NIF / CIF VALIDATION
  # ═══════════════════════════════════════════════════════════════════
  /validate-nif:
    post:
      tags: [NIF Validation]
      summary: Validar NIF / CIF / DNI / NIE espanol
      description: |
        Algoritmo local instantaneo (mod 23 para DNI/NIE, algoritmo AEAT para CIF)
        + consulta VIES opcional (operador IVA intracomunitario UE).

        - Acepta espacios, guiones y puntos (se normalizan).
        - Cache 24h para hits validos.
        - No consume cuota de facturas.
      operationId: validateNif
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nif]
              properties:
                nif:
                  type: string
                  example: "B12345674"
                check_vies:
                  type: boolean
                  default: false
                  description: Si true, ademas consulta VIES (solo CIF/NIE de operador intracomunitario).
      responses:
        '200':
          description: Resultado de la validacion
          content:
            application/json:
              schema:
                type: object
                properties:
                  valid:       { type: boolean,  example: true }
                  normalized:  { type: string,   example: "B12345674" }
                  type:        { type: string,   enum: [dni, nie, cif, unknown], example: "cif" }
                  reason:      { type: string,   nullable: true, example: null }
                  source:      { type: string,   example: "algoritmo-local" }
                  vies:
                    type: object
                    nullable: true
                    properties:
                      valid:   { type: boolean }
                      name:    { type: string, nullable: true }
                      address: { type: string, nullable: true }
              examples:
                cifValido:
                  summary: CIF valido
                  value:
                    valid: true
                    normalized: "B12345674"
                    type: cif
                    reason: null
                    source: algoritmo-local
                    vies: null
                dniInvalido:
                  summary: DNI con letra mal
                  value:
                    valid: false
                    normalized: "12345678A"
                    type: dni
                    reason: "Letra de control DNI incorrecta (esperada: Z)"
                    source: algoritmo-local
                    vies: null
        '401': { description: API key invalida o ausente }
        '422': { description: Datos de entrada invalidos }
        '429': { description: Rate limit excedido }

  # ═══════════════════════════════════════════════════════════════════
  # SETTINGS
  # ═══════════════════════════════════════════════════════════════════
  /settings:
    get:
      tags: [Settings]
      summary: Obtener configuracion
      description: "Devuelve la configuracion actual del tenant."
      operationId: getSettings
      responses:
        '200':
          description: "Configuracion del tenant"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/Settings'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Settings]
      summary: Actualizar configuracion
      description: |
        Actualiza la configuracion del tenant. Solo se modifican los campos enviados.
        Al configurar `webhook_url` por primera vez, se genera automaticamente un
        `webhook_secret` que se devuelve en la respuesta (solo una vez).
      operationId: updateSettings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SettingsUpdateRequest'
            example:
              environment: "production"
              certificate_id: 1
              auto_submit: true
              webhook_url: "https://miapp.com/webhooks/verifactu"
              webhook_events: ["invoice.accepted", "invoice.rejected"]
      responses:
        '200':
          description: "Configuracion actualizada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Settings'
                      - type: object
                        properties:
                          webhook_secret:
                            type: string
                            description: "Solo se devuelve al generar por primera vez"
                          webhook_secret_note:
                            type: string
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'

  # ═══════════════════════════════════════════════════════════════════
  # CERTIFICATES
  # ═══════════════════════════════════════════════════════════════════
  /certificates:
    get:
      tags: [Certificates]
      summary: Listar certificados
      description: "Devuelve todos los certificados digitales del tenant."
      operationId: listCertificates
      responses:
        '200':
          description: "Lista de certificados"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Certificate'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Certificates]
      summary: Subir certificado digital
      description: |
        Sube un certificado digital en formato `.pfx` o `.p12`. El certificado se
        cifra en reposo con HKDF. El numero maximo de certificados depende del plan.
        Tamano maximo: 10 MB.
      operationId: uploadCertificate
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - name
                - file
                - password
              properties:
                name:
                  type: string
                  maxLength: 255
                  description: "Nombre descriptivo del certificado"
                  example: "Certificado FNMT 2026"
                file:
                  type: string
                  format: binary
                  description: "Archivo .pfx o .p12 (max 10 MB)"
                password:
                  type: string
                  description: "Contrasena del certificado"
      responses:
        '201':
          description: "Certificado subido y procesado"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/Certificate'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: "Limite de certificados alcanzado"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: "Error de validacion o certificado invalido"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /certificates/{id}:
    get:
      tags: [Certificates]
      summary: Obtener certificado
      description: "Devuelve los detalles de un certificado especifico."
      operationId: getCertificate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID del certificado"
      responses:
        '200':
          description: "Detalle del certificado"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/Certificate'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Certificates]
      summary: Eliminar certificado
      description: "Elimina un certificado y su archivo cifrado asociado."
      operationId: deleteCertificate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID del certificado"
      responses:
        '200':
          description: "Certificado eliminado"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Certificate deleted."
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /certificates/{id}/validate:
    post:
      tags: [Certificates]
      summary: Validar certificado
      description: |
        Descifra el certificado almacenado y verifica que es legible, no esta corrupto
        y comprueba su fecha de validez.
      operationId: validateCertificate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID del certificado"
      responses:
        '200':
          description: "Resultado de validacion"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/CertificateValidation'
                  meta:
                    $ref: '#/components/schemas/Meta'
              example:
                success: true
                data:
                  valid: true
                  subject: "MI EMPRESA SL - B12345678"
                  issuer: "AC FNMT Usuarios"
                  valid_to: "2027-06-15T00:00:00+02:00"
                  days_to_expiry: 435
                  is_expired: false
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"
                  version: "1.0"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: "Certificado corrupto o no se puede leer"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /certificates/{id}/expiry:
    get:
      tags: [Certificates]
      summary: Consultar expiracion del certificado
      description: "Devuelve informacion sobre las fechas de validez y si esta proximo a expirar."
      operationId: getCertificateExpiry
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID del certificado"
      responses:
        '200':
          description: "Informacion de expiracion"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/CertificateExpiry'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ═══════════════════════════════════════════════════════════════════
  # SERIES
  # ═══════════════════════════════════════════════════════════════════
  /series:
    get:
      tags: [Series]
      summary: Listar series
      description: "Devuelve todas las series de facturacion del tenant."
      operationId: listSeries
      responses:
        '200':
          description: "Lista de series"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Series'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Series]
      summary: Crear serie
      description: |
        Crea una nueva serie de facturacion. El codigo debe ser unico dentro del tenant.
        El numero maximo de series depende del plan contratado.
      operationId: createSeries
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SeriesCreateRequest'
            example:
              code: "F-2026"
              description: "Facturas ordinarias 2026"
      responses:
        '201':
          description: "Serie creada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/Series'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /series/{id}:
    put:
      tags: [Series]
      summary: Actualizar serie
      description: "Actualiza la descripcion o estado activo de una serie."
      operationId: updateSeries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID de la serie"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SeriesUpdateRequest'
            example:
              description: "Facturas ordinarias 2026 (actualizada)"
              is_active: true
      responses:
        '200':
          description: "Serie actualizada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/Series'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [Series]
      summary: Eliminar serie
      description: "Elimina una serie. Solo se puede eliminar si no tiene facturas asociadas."
      operationId: deleteSeries
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID de la serie"
      responses:
        '200':
          description: "Serie eliminada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Series deleted."
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "La serie tiene facturas y no puede eliminarse"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  type: "https://api.verifactu.almc.es/errors/cannot-delete"
                  title: "Cannot Delete"
                  status: 409
                  detail: "Series has invoices and cannot be deleted."
                  aeat_code: null
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"

  # ═══════════════════════════════════════════════════════════════════
  # INVOICES
  # ═══════════════════════════════════════════════════════════════════
  /invoices:
    get:
      tags: [Invoices]
      summary: Listar facturas
      description: |
        Devuelve las facturas del tenant con paginacion. Soporta filtros por
        estado, rango de fechas e ID externo.
      operationId: listInvoices
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, queued, submitted, accepted, rejected, cancelled, error]
          description: "Filtrar por estado"
        - name: from
          in: query
          schema:
            type: string
            format: date
          description: "Fecha de emision desde (YYYY-MM-DD)"
          example: "2026-01-01"
        - name: to
          in: query
          schema:
            type: string
            format: date
          description: "Fecha de emision hasta (YYYY-MM-DD)"
          example: "2026-12-31"
        - name: external_id
          in: query
          schema:
            type: string
          description: "Filtrar por ID externo exacto"
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          description: "Resultados por pagina"
      responses:
        '200':
          description: "Lista paginada de facturas"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/InvoiceSummary'
                  meta:
                    $ref: '#/components/schemas/Meta'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Invoices]
      summary: Crear factura
      description: |
        Crea una nueva factura en estado `draft`. Se asigna automaticamente el siguiente
        numero de la serie indicada.

        Si `auto_submit` es `true` (o la configuracion del tenant lo tiene activado),
        la factura se envia automaticamente a la AEAT y se devuelve un `job_id`
        para seguimiento.

        Si no se proporcionan `tax_lines`, se generan automaticamente a partir de los totales.

        **Consume cuota:** Esta operacion cuenta contra el limite mensual si auto_submit esta activo.
      operationId: createInvoice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceCreateRequest'
            example:
              series_code: "F-2026"
              issue_date: "2026-04-06"
              invoice_type: "F1"
              description: "Servicios de consultoria marzo 2026"
              recipient_name: "Cliente Ejemplo S.L."
              recipient_nif: "B98765432"
              recipient_country: "ES"
              external_id: "ERP-2026-001"
              items:
                - description: "Consultoria estrategica"
                  quantity: 10
                  unit_price: 80.00
                  tax_rate: 21.0
                - description: "Desarrollo web"
                  quantity: 20
                  unit_price: 65.00
                  tax_rate: 21.0
              auto_submit: false
              metadata:
                erp_id: "INV-2026-001"
      responses:
        '201':
          description: "Factura creada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    allOf:
                      - $ref: '#/components/schemas/InvoiceDetail'
                      - type: object
                        properties:
                          job_id:
                            type: string
                            format: uuid
                            description: "ID del trabajo de envio (solo si auto_submit=true)"
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: "Error de validacion o serie no encontrada/inactiva"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  type: "https://api.verifactu.almc.es/errors/validation-error"
                  title: "Validation Error"
                  status: 422
                  detail: "Series 'X-2026' not found or inactive."
                  aeat_code: null
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"

  /invoices/{uuid}:
    get:
      tags: [Invoices]
      summary: Obtener factura
      description: "Devuelve el detalle completo de una factura incluyendo lineas, impuestos y respuesta AEAT."
      operationId: getInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Detalle de la factura"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/InvoiceDetail'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Invoices]
      summary: Actualizar factura
      description: |
        Actualiza una factura en estado `draft`. Si se envian `items`, se reemplazan
        todos los items existentes y se recalculan los totales.

        No se pueden editar facturas en otros estados (queued, submitted, accepted, etc.).
      operationId: updateInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceUpdateRequest'
            example:
              description: "Servicios actualizados"
              items:
                - description: "Consultoria estrategica (revisada)"
                  quantity: 12
                  unit_price: 85.00
                  tax_rate: 21.0
      responses:
        '200':
          description: "Factura actualizada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/InvoiceDetail'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "La factura no esta en estado editable"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                success: false
                error:
                  type: "https://api.verifactu.almc.es/errors/cannot-edit"
                  title: "Cannot Edit"
                  status: 409
                  detail: "Invoice in status 'accepted' cannot be edited."
                  aeat_code: null
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"
        '422':
          $ref: '#/components/responses/ValidationError'

    delete:
      tags: [Invoices]
      summary: Eliminar factura
      description: "Elimina una factura. Solo se pueden eliminar facturas en estado `draft`."
      operationId: deleteInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Factura eliminada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Invoice deleted."
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "Solo borradores pueden eliminarse"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /invoices/{uuid}/submit:
    post:
      tags: [Invoices]
      summary: Enviar factura a la AEAT
      description: |
        Encola la factura para envio a la AEAT via VeriFactu. La operacion es asincrona;
        se devuelve un `job_id` para consultar el estado del procesamiento.

        Solo se pueden enviar facturas en estado `draft` o `error`.

        **Consume cuota:** Cuenta contra el limite mensual de facturas.
      operationId: submitInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Factura encolada para envio"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Invoice queued for submission to AEAT."
                      job_id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        example: "queued"
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "La factura no puede enviarse en su estado actual"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /invoices/{uuid}/cancel:
    post:
      tags: [Invoices]
      summary: Anular factura
      description: |
        Encola la anulacion de una factura previamente enviada a la AEAT.
        Solo se pueden anular facturas en estado `accepted` o `submitted`.
      operationId: cancelInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Anulacion encolada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Cancellation queued."
                      job_id:
                        type: string
                        format: uuid
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "Solo facturas aceptadas/enviadas pueden anularse"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /invoices/{uuid}/query:
    post:
      tags: [Invoices]
      summary: Consultar estado en AEAT
      description: |
        Encola una consulta del estado de la factura directamente en la AEAT.
        Util para verificar si una factura ha sido procesada o para obtener
        informacion actualizada.
      operationId: queryInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Consulta encolada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Query queued."
                      job_id:
                        type: string
                        format: uuid
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /invoices/{uuid}/rectify:
    post:
      tags: [Invoices]
      summary: Rectificar factura rechazada
      description: |
        Reintenta el envio de una factura que fue rechazada por la AEAT.
        Solo se pueden rectificar facturas en estado `rejected`.

        **Consume cuota:** Cuenta contra el limite mensual de facturas.
      operationId: rectifyInvoice
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID de la factura"
      responses:
        '200':
          description: "Rectificacion encolada"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                        example: "Rectification queued."
                      job_id:
                        type: string
                        format: uuid
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: "Solo facturas rechazadas pueden rectificarse"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ═══════════════════════════════════════════════════════════════════
  # JOBS
  # ═══════════════════════════════════════════════════════════════════
  /jobs/{uuid}:
    get:
      tags: [Jobs]
      summary: Consultar estado de trabajo
      description: |
        Devuelve el estado de un trabajo asincrono (envio, anulacion, consulta o rectificacion).
        Utiliza el `job_id` devuelto por las operaciones de facturas.
      operationId: getJobStatus
      parameters:
        - name: uuid
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: "UUID del trabajo"
      responses:
        '200':
          description: "Estado del trabajo"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/JobStatus'
                  meta:
                    $ref: '#/components/schemas/Meta'
              example:
                success: true
                data:
                  job_id: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                  type: "submit"
                  status: "completed"
                  attempts: 1
                  result:
                    csv: "1234"
                    estado: "Correcto"
                  invoice_uuid: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  started_at: "2026-04-06T10:30:01+02:00"
                  completed_at: "2026-04-06T10:30:03+02:00"
                  created_at: "2026-04-06T10:30:00+02:00"
                meta:
                  request_id: "c3d4e5f6-a7b8-9012-cdef-123456789012"
                  timestamp: "2026-04-06T10:30:05+02:00"
                  version: "1.0"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ═══════════════════════════════════════════════════════════════════
  # USAGE
  # ═══════════════════════════════════════════════════════════════════
  /usage:
    get:
      tags: [Usage]
      summary: Consumo del periodo actual
      description: "Devuelve las estadisticas de uso del periodo actual y los limites del plan."
      operationId: getCurrentUsage
      responses:
        '200':
          description: "Uso actual"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/UsageCurrent'
                  meta:
                    $ref: '#/components/schemas/Meta'
              example:
                success: true
                data:
                  period: "2026-04"
                  invoices_submitted: 45
                  invoices_accepted: 43
                  invoices_rejected: 2
                  api_calls: 320
                  limits:
                    invoice_limit_monthly: 100
                    remaining: 55
                meta:
                  request_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  timestamp: "2026-04-06T10:30:00+02:00"
                  version: "1.0"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /usage/history:
    get:
      tags: [Usage]
      summary: Historial de consumo
      description: "Devuelve el historial de consumo mensual. Maximo 24 meses."
      operationId: getUsageHistory
      parameters:
        - name: months
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 24
            default: 12
          description: "Numero de meses a consultar"
      responses:
        '200':
          description: "Historial de uso"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/UsageHistory'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ═══════════════════════════════════════════════════════════════════
  # EVENTS
  # ═══════════════════════════════════════════════════════════════════
  /events:
    get:
      tags: [Events]
      summary: Listar eventos
      description: |
        Devuelve el registro de eventos del tenant con paginacion.
        Soporta filtros por tipo, severidad y factura asociada.
      operationId: listEvents
      parameters:
        - name: type
          in: query
          schema:
            type: string
          description: "Filtrar por tipo de evento (ej: invoice.submitted, certificate.expiring)"
          example: "invoice.submitted"
        - name: severity
          in: query
          schema:
            type: string
            enum: [info, warning, error, critical]
          description: "Filtrar por severidad"
        - name: invoice_uuid
          in: query
          schema:
            type: string
            format: uuid
          description: "Filtrar por UUID de factura"
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
          description: "Resultados por pagina"
      responses:
        '200':
          description: "Lista paginada de eventos"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventSummary'
                  meta:
                    $ref: '#/components/schemas/Meta'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /events/{id}:
    get:
      tags: [Events]
      summary: Obtener detalle de evento
      description: "Devuelve el detalle completo de un evento, incluyendo el payload con datos de la AEAT."
      operationId: getEvent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: "ID del evento"
      responses:
        '200':
          description: "Detalle del evento"
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    enum: [true]
                  data:
                    $ref: '#/components/schemas/EventDetail'
                  meta:
                    $ref: '#/components/schemas/Meta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
