Guía de Integración VeriFactu para Delphi 10 (Seattle/Tokyo/Rio/Athens)

Resumen: Esta guía documenta la arquitectura técnica del adaptador "Micro Server" para entornos Delphi modernos (D10 en adelante). Permite cumplir con la normativa VeriFactu delegando la criptografía y comunicación compleja a un Backend intermedio.


1. Arquitectura del Motor (Engine D10)

El motor ha sido diseñado bajo la filosofía "Lightweight Integration" para facilitar su uso sin necesidad de instalar pesadas suites de componentes en el IDE.

Componentes Principales

El núcleo se encuentra en la carpeta src/vfengine y consta de tres unidades esenciales:

  1. uHttpVF.pas (Capa de Transporte con ICS)

  2. uVFEngine.pas (Capa de Negocio y JSON)


2. Garantía de Compatibilidad API (D7 vs D10)

Entendemos el riesgo que conlleva cambiar de librerías base (de WinInet a ICS). Por ello, hemos diseñado las unidades uVFEngine y uHttpVF manteniendo estrictamente la misma interfaz pública en ambas versiones.

¿Qué significa esto?

Significa que los métodos que usted llama desde sus formularios NO cambian: * Engine.IngestaFromJson(...) tiene la misma firma. * Engine.GetPendientes(...) acepta los mismos parámetros opcionales. * Engine.IngestaYConfirmacion(...) mantiene el mismo orden de argumentos y valores por defecto.

Puede copiar la lógica de negocio de su aplicación D7 y pegarla en D10 con la tranquilidad de que el compilador resolverá las llamadas correctamente, mientras que "por debajo" el motor aprovechará las mejoras de rendimiento y seguridad de ICS y System.JSON sin que usted tenga que reescribir su código de integración.


3. Configuración del JSON (Altas, Subsanaciones y Anulaciones)

No encontrará aquí una documentación exhaustiva sobre cómo construir el JSON manual. ¡No es necesario!

El Panel de Administración Web del Micro Server (sección Envíos) incluye una herramienta de Generación de Payloads interactiva: 1. Seleccione el tipo de operación. 2. Rellene los datos en el formulario. 3. Obtenga el JSON perfectamente validado y listo para copiar a su código Delphi.

Nota sobre metadata: El esquema JSON exige un bloque metadata en la raíz. Aunque el backend actual ignore su contenido, debe estar presente (puede ir vacío).


3. Flujo de Trabajo Típico: "Alta de Factura"

El Backend actúa como un buffer inteligente. Su aplicación D10 habla con este Micro Server, no con la AEAT directamente.

Paso 1: Ingesta (IngestaFromJson)

Su ERP envía el JSON al endpoint /v1/ingesta. * Respuesta: El Backend valida y guarda. Retorna 200 OK. * Estado: La factura AÚN NO ha ido a la AEAT. Un "Worker" en background se encarga de firmarla, encadenarla y enviarla.

Paso 2: Espera Activa (GetPendientes)

El ERP pregunta periódicamente ("Polling") a /verifactu/pendientes. * Cuando el worker termina, la factura aparece en esta lista con su Huella, CSV y UrlQR. * Correlación: El motor busca automáticamente la factura usando IdEnvio y LineaDetalle.

Paso 3: Confirmación (AckIndice)

Una vez recuperados los datos (Huella/QR): 1. El ERP los guarda en su BD local. 2. Envía un ACK al Backend (/verifactu/ack). 3. El Backend marca la operación como "Entregada" y la saca de la cola.

Función "Todo en Uno": La unidad uVFEngine incluye IngestaYConfirmacion(...) que automatiza estos 3 pasos en una sola llamada bloqueante (con timeout).


4. Configuración (config.ini) y Autenticación (API Key)

El motor busca el archivo config.ini junto al ejecutable:

[api]
; URL del VeriFactu Micro Server
base_url=http://localhost:8000
; Tiempo máximo (ms)
timeout_ms=60000
; Token de seguridad (API Key). Si el backend exige autenticación, este campo es OBLIGATORIO.
token=mi_clave_api_secreta_123

[demo]
; NIF del emisor asociado a esta instancia. El API Key debe tener permisos sobre este NIF.
nif_emisor=B12345678
n_ultimos=50

4.1. Autenticación y Seguridad Multi-Tenant (RBAC)

El Micro Server VeriFactu incluye un sistema de seguridad avanzado para entornos donde un mismo backend atiende a múltiples empresas (Multi-Tenant).

Si la opción require_auth=True está activada en el backend, la API exigirá dos elementos de seguridad obligatorios:

  1. X-API-Key (Token): Identifica al usuario o integración ERP. Se genera desde la Gestión de Usuarios en el Panel de Administración Web.
  2. X-Verifactu-Emisor (NIF): Declara el NIF sobre el que se va a operar. El servidor comprueba si la API Key facilitada tiene permisos concedidos explícitamente sobre este NIF.

Prevención de Suplantación en la Ingesta: Como medida de máxima seguridad, durante una operación de Ingesta, el servidor abre el JSON enviado y verifica que el campo cabecera.emisor coincida exactamente con el NIF para el que el usuario se ha autenticado. Si un usuario intenta enviar (o modificar) una factura camuflando un NIF ajeno en el JSON, la operación se abortará inmediatamente devolviendo un error de seguridad HTTP 403 Forbidden. Lo mismo ocurrirá si intenta consultar facturas pendientes de otro emisor.

Implementación Automática en componentes

El motor Delphi se encarga automáticamente de inyectar las cabeceras HTTP de seguridad (X-API-Key y X-Verifactu-Emisor) de forma invisible suministrando la configuración en cada envío:

  // Configurado automáticamente a través del record:
  Cfg.Token := 'mi_clave_api_secreta_123'; // Inyecta la cabecera X-API-Key
  Cfg.NifEmisor := 'B12345678';            // Inyecta la cabecera X-Verifactu-Emisor

  Engine := TVFEngine.Create(Cfg);

5. Visualización de QR en Delphi 10

Utilizamos soporte nativo para PNG y JPEG (Vcl.Imaging). El motor ofrece dos opciones en TVFPendienteItem:

  1. QrVerifactu (Base64): La imagen real del QR en Base64.
  2. UrlQrVerifactu (URL): Enlace público a la sede de la AEAT.

La demo incluye TryLoadBase64ToImage, que decodifica el Base64 directamente a un TImage en memoria, siendo más eficiente que descargar el archivo temporalmente.


6. Solución de Problemas

Error de Compilación "File not found: OverbyteIcs..."

Excepciones SSL / "Protocol not supported"

Timeout en "Pendientes"


7. Ejemplo de Integración: Alta Síncrona (Todo en Uno)

Este enfoque implementa un ciclo completo e inmediato para una sola operación. La función IngestaYConfirmacion realiza internamente todo el trabajo sucio: envía el JSON, espera haciendo polling activo a que la AEAT responda, y retorna solo cuando tiene el resultado final (o agota el tiempo de espera).

Pros: * Simplicidad: El código del ERP es lineal y fácil de entender. * Inmediatez: El cliente obtiene su factura impresa con el QR al momento (si la AEAT responde rápido). * Certeza: Sabes si la factura fue aceptada o rechazada en el mismo hilo de ejecución.

Contras: * Latencia: La interfaz de usuario puede quedarse "congelada" unos segundos mientras espera a la AEAT (se recomienda usar un thread o mostrar un "Espere..."). * Dependencia Externa: Si la AEAT está lenta, la experiencia de usuario se degrada.

uses uVFEngine;

procedure TForm1.EmitirAltaSincrona;
var
  Cfg: TVfDemoConfig;
  Engine: TVFEngine;
  Res: TVFIngestaAckResult;
  MiJson: string;
begin
  // 1. Configurar
  Cfg.ApiBaseUrl := 'http://localhost:8000';
  Cfg.TimeoutMs := 60000; // 60s máximo
  // ... resto de config

  Engine := TVFEngine.Create(Cfg);
  try
    MiJson := MemoJson.Text; // JSON generado

    // 2. Ejecutar Ciclo (Ingesta -> Polling -> ACK)
    // El parámetro True final activa el ACK automático si se encuentra éxito
    Res := Engine.IngestaYConfirmacion(MiJson, Cfg.TimeoutMs, 200, 50, True);

    if Res.Timeout then
      ShowMessage('Factura enviada, pero la AEAT tarda. Consulte Pendientes más tarde.')

    else if Res.EncontradoEnPendientes and Res.AckHecho then
    begin
      // 3. Éxito: Guardar y Mostrar
      // Res.Pendiente tiene todos los datos: Huella, QR, CSV, etc.
      GuardarEnBD(Res.Pendiente.Huella, Res.Pendiente.UrlQrVerifactu);

      // Mostrar QR
      if Res.Pendiente.QrVerifactu <> '' then
        CargarBase64EnImage(Res.Pendiente.QrVerifactu, MyImageComponent);

      ShowMessage('Factura Aceptada. Huella: ' + Res.Pendiente.Huella);
    end
    else
      ShowMessage('Error: ' + Res.ErrorMsg);

  finally
    Engine.Free;
  end;
end;

8. Casos de Uso Avanzados

8.1. Ingesta Asíncrona (Solo Envío)

Si su ERP prefiere no esperar la respuesta de la AEAT (bloqueante) y prefiere procesar las confirmaciones en un proceso batch separado.

Reglas de Negocio Importantes:

Antelación: Las facturas pueden enviarse con fecha anterior a su emisión oficial; el backend las guardará en espera hasta que corresponda procesarlas. Modificación (Pre-AEAT): Si envía una factura (mismo ID) y aún no ha sido procesada por la AEAT, esta nueva versión sobrescribe a la anterior. La "vigente" será siempre la última recibida antes del envío. Subsanación (Post-AEAT): Si envía una factura que YA fue procesada anteriormente: Se intenta enviar como una subsanación según la configuración en el backend y se procesará como tal. De lo contrario, la AEAT devolverá el error correspondiente solicitando rectificativa. Validaciones Inmediatas: La ingesta puede devolver errores (HTTP 4xx/5xx o mensaje de error en JSON) si detecta: Errores de estructura del JSON. Incoherencias en su composición (datos faltantes o tipos erróneos). Incongruencia en los cálculos (bases, cuotas y totales no coinciden).

procedure TForm1.EnviarFacturaAsync;
var
  IngestaResp: TVFIngestaResponse;
begin
  // Solo enviamos al Backend. Reciben OK (guardado) y nos vamos.
  IngestaResp := Engine.IngestaFromJson(MiJson);

  if IngestaResp.Ok then
    ShowMessage('Factura encolada correctamente. ID Op: ' + IngestaResp.Id)
  else
    ShowMessage('Error al encolar: ' + IngestaResp.ErrorMsg);
end;

8.2. Recuperación de Pendientes (Polling de Confirmaciones)

Para un proceso en segundo plano (Timer) que recupera facturas ya procesadas por la AEAT.

  1. Llamar a GetPendientes (últimas N).
  2. Iterar lista.Si tiene resultado final, guardar y hacer ACK.
procedure TForm1.ProcesarColaPendientes;
var
  Pend: TVFPendientesResponse;
  Ack: TVFAckResponse;
  I: Integer;
  Item: TVFPendienteItem;
begin
  // Recuperar las ultimas 50
  Pend := Engine.GetPendientes(50);

  if not Pend.Ok then Exit; 

  for I := 0 to Length(Pend.Items) - 1 do
  begin
    Item := Pend.Items[I];

    // Status 0..3: Respuestas Finales de la AEAT
    // 0: Aceptada, 1: Aceptada con Errores
    // 2: Rechazada, 3: Rechazada (Otro)
    if (Item.Status >= 0) and (Item.Status <= 3) then
    begin
       try
          // A. Procesar respuesta en el ERP local
          // MiBD.Actualizar(Item.IdEnvio, Item.Status, Item.Huella, ...);

          // B. Confirmar recepción al Backend (ACK)
          // Esto la borra de la lista de pendientes del backend
          Ack := Engine.AckIndice(Item.IndiceLog);

          if not Ack.Ok then
            Log('Error al hacer ACK: ' + Ack.ErrorMsg);

       except
          // Si falla mi BD local, NO hago ACK para que vuelva a salir en el siguiente polling
          Log('Error guardando en BD local, reintentaremos luego.');
       end;
    end;
  end;
end;