{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "https://munincloud.com/api/ocr/v1/respuesta.schema.json",
 "title": "Respuesta v1 de la API de auditoría de facturas de MuninCloud",
 "description": "El documento que devuelve la API por cada factura: la respuesta natural del motor de lectura, sin lo que es interno del motor o de nuestra plataforma, sin el cierre interno de la auditoría, con los avisos propios de la API en `avisos` y, desde v1.1, con la `direccion` (desde v1.3, la que se declara al subir) y el `factura.recargo_total`. Regla v1: se añaden campos, nunca se quitan ni se renombran; un cliente debe ignorar los campos que no conozca (los añadirá v1.x). El sobre (estado del trabajo, pool, identificador) NO es de este esquema: está en openapi.yaml. Este esquema es ESTRICTO (additionalProperties false) porque describe exactamente lo que emite la API hoy.",
 "anyOf": [
  {
   "$ref": "#/$defs/factura_auditada"
  },
  {
   "$ref": "#/$defs/clasificacion"
  }
 ],
 "$defs": {
  "tercero": {
   "description": "Una parte de la factura. Ojo, nombre heredado del motor de lectura: 'proveedor' es SIEMPRE quien emite y 'cliente' SIEMPRE quien recibe, en compras y en ventas (medido). Para saber quién es quién según el papel, leer lectura_papel.",
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "nombre": {
     "type": "string"
    },
    "cif_nif": {
     "type": "string",
     "description": "Tal cual consta en el papel. Centinela 'NO_VISIBLE' si no se encontró."
    },
    "direccion": {
     "type": "string"
    },
    "territorio": {
     "type": "string",
     "description": "Solo en ventas leídas por el modelo; suele ir vacío."
    },
    "pais": {
     "type": "string",
     "description": "Solo en ventas leídas por el modelo."
    },
    "codigo_postal": {
     "type": "string",
     "description": "Solo en ventas leídas por el modelo."
    },
    "ciudad": {
     "type": "string",
     "description": "Solo en ventas leídas por el modelo."
    },
    "email": {
     "type": "string",
     "description": "Solo en ventas leídas por el modelo."
    }
   }
  },
  "cabecera": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "numero",
    "recargo_total"
   ],
   "properties": {
    "numero": {
     "type": "string",
     "description": "'S/N' o un centinela ('NO NUMERO DE FACTURA VISIBLE') si no se leyó."
    },
    "fecha": {
     "type": "string"
    },
    "divisa": {
     "type": "string",
     "description": "ISO 4217, en mayúsculas."
    },
    "base_total": {
     "type": "number"
    },
    "iva_total": {
     "type": "number"
    },
    "total_factura": {
     "type": "number",
     "description": "Total = base + IVA + RE + impuestos especiales - IRPF + descuentos. El RE, si el motor lo da, va en recargo_total."
    },
    "es_rectificativa": {
     "type": "boolean"
    },
    "tiene_recargo_equivalencia": {
     "type": "boolean"
    },
    "factura_rectificada": {
     "type": "object",
     "additionalProperties": false,
     "properties": {
      "numero": {
       "type": "string"
      },
      "fecha": {
       "type": [
        "string",
        "null"
       ]
      }
     }
    },
    "recargo_total": {
     "type": [
      "number",
      "null"
     ],
     "description": "Recargo de equivalencia total de la factura, tal como lo da el motor de lectura. null si no lo ha dado: la API no lo calcula ni lo supone. Añadido en v1.1."
    }
   }
  },
  "impuesto": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "tipo": {
     "type": "string",
     "description": "Hoy 'IRPF'."
    },
    "rate": {
     "type": "number",
     "description": "Porcentaje; negativo si es retención."
    },
    "importe": {
     "type": "number",
     "description": "Negativo si es retención."
    }
   }
  },
  "linea": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "amount"
   ],
   "properties": {
    "descripcion_original": {
     "type": "string"
    },
    "short_name": {
     "type": "string"
    },
    "qty": {
     "type": "number"
    },
    "rate": {
     "type": "number"
    },
    "amount": {
     "type": "number"
    },
    "tax_percent": {
     "type": "number"
    },
    "es_descuento": {
     "type": "boolean"
    },
    "es_no_sujeto": {
     "type": "boolean",
     "description": "Suplido o importe no sujeto: va fuera de la base."
    }
   }
  },
  "hallazgo": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "codigo",
    "gravedad",
    "mensaje"
   ],
   "properties": {
    "codigo": {
     "type": "string",
     "description": "Vocabulario cerrado que crece (ocr_tools/dictamen.py): se añaden códigos, no se renombran."
    },
    "gravedad": {
     "enum": [
      "rechazado",
      "corregido",
      "aviso"
     ]
    },
    "mensaje": {
     "type": "string"
    },
    "base_legal": {
     "type": "string"
    },
    "evidencia": {
     "type": "string"
    },
    "efecto": {
     "type": "string"
    }
   }
  },
  "verificacion_identidad": {
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "verificado_por": {
     "type": "string"
    },
    "identificador": {
     "type": "string"
    },
    "razon_social_oficial": {
     "type": [
      "string",
      "null"
     ]
    },
    "situacion": {
     "type": "string",
     "description": "IDENTIFICADO, IDENTIFICADO-BAJA o IDENTIFICADO-REVOCADO."
    },
    "fecha_verificacion": {
     "type": "string"
    },
    "advertencia": {
     "type": "string"
    }
   }
  },
  "lectura_papel": {
   "description": "Quién emite y quién recibe según el papel, leído a ciegas.",
   "type": "object",
   "additionalProperties": false,
   "properties": {
    "emisor": {
     "type": [
      "string",
      "null"
     ]
    },
    "receptor": {
     "type": [
      "string",
      "null"
     ]
    },
    "identificadores": {
     "type": "array",
     "items": {
      "type": "string"
     }
    },
    "confianza": {
     "type": "string"
    },
    "motivo": {
     "type": "string"
    },
    "fuente_texto": {
     "enum": [
      "capa_texto",
      "ocr",
      "sin_texto"
     ]
    }
   }
  },
  "trazabilidad": {
   "description": "Lo que se comprobó del desglose al cerrar. El cierre interno de la auditoría NO se publica: la API lo traduce a `avisos`.",
   "type": "object",
   "additionalProperties": false,
   "required": [
    "desglose_cuadra",
    "errores_finales"
   ],
   "properties": {
    "desglose_cuadra": {
     "type": [
      "boolean",
      "null"
     ]
    },
    "errores_finales": {
     "type": "integer"
    }
   }
  },
  "factura_auditada": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "cliente",
    "proveedor",
    "factura",
    "lineas",
    "lectura_papel",
    "trazabilidad",
    "avisos",
    "direccion"
   ],
   "properties": {
    "direccion": {
     "enum": [
      "RECIBIDA",
      "EMITIDA"
     ],
     "description": "Si la factura es RECIBIDA (una compra de la empresa de la llave) o EMITIDA (una venta). Desde v1.3 es la que se declara al subir la factura (`tipo`: compra → RECIBIDA, venta → EMITIDA); si en el papel la empresa de la llave aparece en el lado contrario, el documento lleva el aviso TIPO_DISCREPANTE. Añadido en v1.1."
    },
    "cliente": {
     "$ref": "#/$defs/tercero"
    },
    "proveedor": {
     "$ref": "#/$defs/tercero"
    },
    "factura": {
     "$ref": "#/$defs/cabecera"
    },
    "impuestos_y_retenciones": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/impuesto"
     }
    },
    "lineas": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/linea"
     }
    },
    "dictamen_auditoria": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/hallazgo"
     }
    },
    "verificacion_identidad": {
     "$ref": "#/$defs/verificacion_identidad"
    },
    "lectura_papel": {
     "$ref": "#/$defs/lectura_papel"
    },
    "trazabilidad": {
     "$ref": "#/$defs/trazabilidad"
    },
    "avisos": {
     "type": "array",
     "items": {
      "$ref": "#/$defs/aviso_api"
     },
     "description": "Avisos propios de la API; lista vacía si no hay ninguno."
    }
   }
  },
  "clasificacion": {
   "description": "Respuesta del triaje (tipo SOLO_CLASIFICAR): qué es el documento. La API la usa para rechazar lo que no es factura.",
   "type": "object",
   "additionalProperties": false,
   "required": [
    "doc_type",
    "factura_tipo",
    "origen"
   ],
   "properties": {
    "doc_type": {
     "type": "string",
     "description": "FACTURA, NOMINA, ONBOARDING, bancos, TICKET o DESCONOCIDO."
    },
    "factura_tipo": {
     "type": [
      "string",
      "null"
     ],
     "description": "EMITIDA o RECIBIDA, solo si doc_type es FACTURA."
    },
    "origen": {
     "type": "string",
     "description": "De dónde sale la clasificación: regla_cpu, salamandra, lectura_papel, emergencia, texto_vacio o desconocido."
    }
   }
  },
  "aviso_api": {
   "type": "object",
   "additionalProperties": false,
   "required": [
    "codigo",
    "mensaje"
   ],
   "description": "Aviso propio de la API (no del motor de lectura). Vocabulario que crece: se añaden códigos, nunca se renombran; un cliente debe tratar un código que no conoce como «revise el documento». Códigos de hoy: LECTURA_INCOMPLETA, DESCUADRE, NIF_CONTRAPARTE_NO_LOCALIZADO y CIERRE_DESCONOCIDO (los traduce la API desde el cierre interno de la auditoría); PAGINAS_NO_LEIDAS (el documento tiene más páginas de las que se leen) EMPRESA_NO_APARECE (el NIF de la llave no está entre los identificadores leídos) y, desde v1.3, TIPO_DISCREPANTE (se leyó con el tipo declarado al subir, compra o venta, pero en el papel la empresa de la llave aparece en el papel contrario).",
   "properties": {
    "codigo": {
     "type": "string",
     "pattern": "^[A-Z][A-Z0-9_]*$"
    },
    "mensaje": {
     "type": "string",
     "minLength": 1
    }
   }
  }
 }
}
