Guía de Integración Rápida con Delphi 10+ (Modern)
La integración con Delphi 10 (Seattle, Berlin, Rio, Sydney, Alexandria, Athens) y Delphi 11 mantiene la misma filosofía que nuestra guía de Delphi 7 (una única clase orquestadora TVFEngine), pero implementando mejoras de rendimiento radicales en su funcionamiento interno para aprovechar la modernidad del compilador.
A diferencia del SDK para sistemas legacy, esta versión se caracteriza por:
- JSON Nativo: Toda la serialización y validación se realiza utilizando la librería oficial e integrada
System.JSON. - Motor HTTP ICS: Las comunicaciones abandonan WinInet para utilizar el prestigioso motor asíncrono ICS (Internet Component Suite - Overbyte), maximizando el rendimiento del pool de conexiones mTLS a la AEAT.
TIP
Instalación "Zero-Friction" de ICS. Para utilizar nuestra capa de comunicaciones, no necesitas instalar componentes visuales en la IDE. Simplemente descárgate los códigos fuente de ICS y añade su directorio a tu Library Path (Tools > Options > Language > Delphi Options > Library). El código compilará la red directamente.
A continuación, te mostramos lo sencillo que es implementar el flujo en tu ERP moderno.
WARNING
Aviso sobre el bloque metadata (JSON de Facturas) En algunos JSON de prueba locales verás un bloque raíz llamado "metadata": { "enviar_aeat": false, "simulacion": true, ... }. La API funciona perfectamente CON y SIN ese bloque. Se introdujo exclusivamente por compatibilidad con futuras versiones, pero se aconseja encarecidamente que prescindas totalmente de él para los procesos y desarrollos actuales, enviando únicamente "cabecera" y "detalle".
Vídeo Demostrativo
🏗️ 1. Configuración del Motor (TVFEngine)
Para empezar a dialogar con nuestro servidor, configura las credenciales y crea la instancia del motor.
var
Cfg: TVfDemoConfig;
Engine: TVFEngine;
begin
// 1. Preparamos el entorno de conexión
// IMPORTANTE: El SDK conecta contra la IP de TU MicroServer (Middleware Local),
// NO contra los servidores en la nube de SystemsFGH.
// 'http://localhost:8000' si está en la misma máquina, o 'http://192.168.1.50:8000' en red local.
Cfg.ApiBaseUrl := 'http://localhost:8000';
Cfg.Token := 'TXT-VFACTU-TU-TOKEN';
Cfg.NifEmisor := 'B12345678';
Cfg.TimeoutMs := 60000; // Tolerancia de 60 segundos si la AEAT va lenta
// 2. Arrancamos el motor
Engine := TVFEngine.Create(Cfg);🚀 2. Modalidad A: Emisión Síncrona ("Todo en Uno")
Recomendado para volúmenes bajos de facturación donde un cajero puede esperar la impresión del Ticket.
Este método encapsula la factura, la manda al MicroServer, espera a que se procese criptográficamente en la AEAT y te devuelve la respuesta legal.
var
Res: TVFIngestaAckResult;
MiJsonFactura: string;
begin
MiJsonFactura := '{ ... aquí va el texto JSON de tu factura ... }';
try
// IngestaYConfirmacion hace absolutamente todo el trabajo sucio por nosotros
// Parámetros: (JSON, Timeout, Sleep_en_milisegundos, NumReintentos, AutoConfirmar)
Res := Engine.IngestaYConfirmacion(MiJsonFactura, Cfg.TimeoutMs, 200, 50, True);
if Res.Timeout then
ShowMessage('La AEAT está tardando. Consulte "Facturas Pendientes" más tarde.')
else if Res.AckHecho then
begin
// ¡ÉXITO! La factura está enviada y oficializada.
ShowMessage('Huella de la AEAT: ' + Res.Pendiente.Huella);
// Ya puedes imprimir el Código QR legal usando:
// Res.Pendiente.UrlQrVerifactu
end
else
// Si a la AEAT no le gusta tu factura (falta DNI, etc...)
ShowMessage('Error AEAT: ' + Res.ErrorMsg);
finally
Engine.Free;
end;
end;📬 3. Modalidad B: Emisión Asíncrona (Alto Rendimiento)
Recomendado para procesos Batch (facturación masiva) y alto tráfico.
Paso 1: Soltar la factura al servidor y seguir trabajando
var
IngestaResp: TVFIngestaResponse;
begin
IngestaResp := Engine.IngestaFromJson(MiJsonFactura);
if IngestaResp.Ok then
ShowMessage('¡Factura absorbida! ID de seguimiento: ' + IngestaResp.Id)
else
ShowMessage('Error de formato antes de salir: ' + IngestaResp.ErrorMsg);
end;Paso 2: Polling en segundo plano Más tarde, le pides a la API: "Oye, ¿la AEAT te ha contestado ya?"
var
Pend: TVFPendientesResponse;
Item: TVFPendienteItem;
I: Integer;
begin
Pend := Engine.GetPendientes(50);
if not Pend.Ok then Exit;
for I := 0 to Length(Pend.Items) - 1 do
begin
Item := Pend.Items[I];
if (Item.Status >= 0) and (Item.Status <= 3) then
begin
// 1. Lo registramos en nuestra BBDD como oficial
// 2. Le mandamos el ACUSE DE RECIBO (ACK) al API para limpiar la cola
Engine.AckIndice(Item.IndiceLog);
end;
end;
end;📦 Código Fuente del Core SDK
Para que todo el flujo anterior funcione de manera nativa, el SDK de Delphi 10 se apoya en 4 unidades base modernas. Se han purgado dependencias externas.
A continuación se incluye el código fuente completo de cada uno para que puedas revisarlo o integrarlo directamente copiando su contenido.
uConfig.pas
Configuración básica de credenciales y timeouts.
Ver/Ocultar Código Fuente (uConfig.pas)
unit uConfig;
interface
uses
SysUtils, Classes, IniFiles;
type
TVfDemoConfig = record
ApiBaseUrl: string;
TimeoutMs: Integer;
Token: string;
NifEmisor: string;
NUltimos: Integer;
end;
function LoadVfDemoConfig(const FileName: string): TVfDemoConfig;
implementation
function LoadVfDemoConfig(const FileName: string): TVfDemoConfig;
var
Ini: TIniFile;
begin
// Valores por defecto seguros
Result.ApiBaseUrl := '';
Result.TimeoutMs := 10000;
Result.Token := '';
Result.NifEmisor := '';
Result.NUltimos := 50;
if not FileExists(FileName) then
Exit;
Ini := TIniFile.Create(FileName);
try
Result.ApiBaseUrl := Ini.ReadString('api', 'base_url', Result.ApiBaseUrl);
Result.TimeoutMs := Ini.ReadInteger('api', 'timeout_ms', Result.TimeoutMs);
Result.Token := Ini.ReadString('api', 'token', Result.Token);
Result.NifEmisor := Ini.ReadString('demo', 'nif_emisor', Result.NifEmisor);
Result.NUltimos := Ini.ReadInteger('demo', 'n_ultimos', Result.NUltimos);
finally
Ini.Free;
end;
end;
end.uHttpVF.pas
Capa de comunicaciones REST de altísimo rendimiento y robustez asíncrona utilizando el motor ICS (Internet Component Suite - Overbyte), con manejo estricto de TLS y certificados.
Ver/Ocultar Código Fuente (uHttpVF.pas)
unit uHttpVF;
interface
type
TVfApiClient = class
private
FBaseUrl: string;
FTimeoutMs: Integer;
FToken: string;
FNifEmisor: string;
FLastStatusCode: Integer;
FLastResponseText: string;
public
constructor Create(const BaseUrl: string; TimeoutMs: Integer; const Token: string);
function GetText(const PathAndQuery: string): string;
function PostJson(const Path: string; const JsonUtf8: string): string;
property BaseUrl: string read FBaseUrl;
property Token: string read FToken write FToken;
property NifEmisor: string read FNifEmisor write FNifEmisor;
property LastStatusCode: Integer read FLastStatusCode;
property LastResponseText: string read FLastResponseText;
end;
function UrlEncode(const S: string): string;
implementation
uses
SysUtils, Classes,
OverbyteIcsHttpProt;
function UrlEncode(const S: string): string;
const
Hex: array[0..15] of Char = '0123456789ABCDEF';
var
I: Integer;
C: Byte;
begin
Result := '';
for I := 1 to Length(S) do
begin
// Para NIF y params simples (ASCII) esto es suficiente y seguro.
C := Ord(S[I]);
if (C >= Ord('A')) and (C <= Ord('Z')) or
(C >= Ord('a')) and (C <= Ord('z')) or
(C >= Ord('0')) and (C <= Ord('9')) or
(S[I] in ['-','_','.','~']) then
Result := Result + S[I]
else if S[I] = ' ' then
Result := Result + '%20'
else
Result := Result + '%' + Hex[C shr 4] + Hex[C and $0F];
end;
end;
constructor TVfApiClient.Create(const BaseUrl: string; TimeoutMs: Integer; const Token: string);
begin
inherited Create;
FBaseUrl := BaseUrl;
if (Length(FBaseUrl) > 0) and (FBaseUrl[Length(FBaseUrl)] = '/') then
Delete(FBaseUrl, Length(FBaseUrl), 1);
FTimeoutMs := TimeoutMs;
FToken := Token;
end;
procedure ApplyCommonHeaders(H: THttpCli; const Token: string; const NifEmisor: string);
begin
H.ExtraHeaders.Clear;
H.ExtraHeaders.Add('Accept: application/json');
H.ExtraHeaders.Add('Connection: close');
if Token <> '' then
H.ExtraHeaders.Add('X-API-Key: ' + Token);
if NifEmisor <> '' then
H.ExtraHeaders.Add('X-Verifactu-Emisor: ' + NifEmisor);
end;
function ReadStreamAsStringUTF8(S: TStream): string;
var
Bytes: TBytes;
begin
SetLength(Bytes, S.Size);
S.Position := 0;
if S.Size > 0 then
S.ReadBuffer(Bytes[0], Length(Bytes));
Result := TEncoding.UTF8.GetString(Bytes);
end;
procedure WriteStringAsUTF8(const Str: string; Dest: TStream);
var
Bytes: TBytes;
begin
Bytes := TEncoding.UTF8.GetBytes(Str);
if Length(Bytes) > 0 then
Dest.WriteBuffer(Bytes[0], Length(Bytes));
end;
function ToTimeoutSeconds(TimeoutMs: Integer): Integer;
begin
Result := TimeoutMs div 1000;
if Result < 1 then
Result := 1;
end;
function TVfApiClient.GetText(const PathAndQuery: string): string;
var
H: THttpCli;
OutStream: TMemoryStream;
begin
H := THttpCli.Create(nil);
OutStream := TMemoryStream.Create;
try
H.URL := FBaseUrl + PathAndQuery;
H.RcvdStream := OutStream;
H.Timeout := ToTimeoutSeconds(FTimeoutMs);
ApplyCommonHeaders(H, FToken, FNifEmisor);
try
H.Get;
finally
FLastStatusCode := H.StatusCode;
Result := ReadStreamAsStringUTF8(OutStream);
FLastResponseText := Result;
end;
finally
OutStream.Free;
H.Free;
end;
end;
function TVfApiClient.PostJson(const Path: string; const JsonUtf8: string): string;
var
H: THttpCli;
InStream, OutStream: TMemoryStream;
begin
H := THttpCli.Create(nil);
InStream := TMemoryStream.Create;
OutStream := TMemoryStream.Create;
try
WriteStringAsUTF8(JsonUtf8, InStream);
InStream.Position := 0;
H.URL := FBaseUrl + Path;
H.SendStream := InStream;
H.RcvdStream := OutStream;
H.Timeout := ToTimeoutSeconds(FTimeoutMs);
ApplyCommonHeaders(H, FToken, FNifEmisor);
H.ContentTypePost := 'application/json; charset=utf-8';
try
H.Post;
finally
FLastStatusCode := H.StatusCode;
Result := ReadStreamAsStringUTF8(OutStream);
FLastResponseText := Result;
end;
finally
OutStream.Free;
InStream.Free;
H.Free;
end;
end;
end.uVFPayload.pas
Estructuras de datos y formateo de utilidades para serialización/deserialización utilizando System.JSON nativo.
Ver/Ocultar Código Fuente (uVFPayload.pas)
unit uVFPayload;
interface
function LoadIngestaFromTemplate(
const TemplateFile: string;
const NifEmisor: string
): string;
function BuildAckJson(
const NifEmisor: string;
const IndiceLog: Int64
): string;
implementation
uses
SysUtils, Classes, System.JSON;
function LoadTextFile(const FileName: string): string;
var
FS: TFileStream;
SS: TStringStream;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
if FS.Size = 0 then
raise Exception.Create('PLANTILLA VACIA: ' + FileName);
try
SS := TStringStream.Create('', TEncoding.UTF8);
try
SS.CopyFrom(FS, FS.Size);
Result := SS.DataString;
finally
SS.Free;
end;
finally
FS.Free;
end;
end;
function LoadIngestaFromTemplate(
const TemplateFile: string;
const NifEmisor: string
): string;
var
Timestamp: string;
begin
Result := LoadTextFile(TemplateFile);
Result := StringReplace(Result, '{{EMISOR}}', NifEmisor, [rfReplaceAll]);
// Generar ID unico en base a fecha/hora para evitar 422 (Duplicado)
Timestamp := FormatDateTime('yyyymmdd-hhnnss', Now);
// Reemplazar la factura "505" de la plantilla por algo unico
Result := StringReplace(Result, '"numfactura": "505"', '"numfactura": "505-' + Timestamp + '"', [rfReplaceAll]);
end;
function BuildAckJson(
const NifEmisor: string;
const IndiceLog: Int64
): string;
var
Obj: TJSONObject;
begin
Obj := TJSONObject.Create;
try
Obj.AddPair('nif_emisor', NifEmisor);
Obj.AddPair('indice_log', TJSONNumber.Create(IndiceLog));
Result := Obj.ToJSON;
finally
Obj.Free;
end;
end;
end.uVFEngine.pas
El cerebro principal. Contiene la clase TVFEngine que orquesta de forma asíncrona y transparente las peticiones a VeriFactuHub.
Ver/Ocultar Código Fuente (uVFEngine.pas)
unit uVFEngine;
interface
uses
SysUtils, Classes,
System.JSON, System.DateUtils, System.IOUtils,
uConfig, uHttpVF, uVFPayload;
type
TVFCorrelacion = record
IdEnvio: Int64; // tracking.cab_num_secuencia <-> pendientes.id_envio
LineaDetalle: Integer; // tracking.det_linea <-> pendientes.linea_log_detalle
class function Empty: TVFCorrelacion; static;
function IsValid: Boolean;
end;
TVFIngestaResponse = record
Ok: Boolean;
HttpStatus: Integer;
Mensaje: string;
Id: string;
IdFormateado: string;
OperacionRealizada: string;
TrackingEmisor: string;
TrackingSerie: string;
TrackingNumFactura: string;
TrackingTimestamp: TDateTime;
Correlacion: TVFCorrelacion;
RawResponseJson: string;
ErrorMsg: string;
ErrorBodyJson: string;
end;
TVFPendienteItem = record
IndiceLog: Int64;
Creacion: TDateTime;
Modo: Integer;
IdEnvio: Int64;
LineaLogDetalle: Integer;
NumSerieFactura: string;
NumFactura: string;
FechaExpedicion: TDateTime;
Huella: string;
CSV: string;
Total: Currency;
CodigoError: Integer;
DescripcionError: string;
Status: Integer;
QrVerifactu: string;
UrlQrVerifactu: string;
end;
TVFPendientesResponse = record
Ok: Boolean;
HttpStatus: Integer;
Items: array of TVFPendienteItem;
RawResponseJson: string;
ErrorMsg: string;
ErrorBodyJson: string;
end;
TVFAckResponse = record
Ok: Boolean;
HttpStatus: Integer;
Status: string;
Message: string;
RawResponseJson: string;
ErrorMsg: string;
ErrorBodyJson: string;
end;
TVFIngestaAckResult = record
Ingesta: TVFIngestaResponse;
EncontradoEnPendientes: Boolean;
Pendiente: TVFPendienteItem;
IndiceLogEncontrado: Int64;
AckHecho: Boolean;
Ack: TVFAckResponse;
Timeout: Boolean;
ErrorMsg: string;
end;
TVFEngine = class
private
FCfg: TVfDemoConfig;
FCli: TVfApiClient;
function ParseIngestaResponse(const RespJson: string): TVFIngestaResponse;
function ParsePendientesResponse(const RespJson: string): TVFPendientesResponse;
function ParseAckResponse(const RespJson: string): TVFAckResponse;
function TryFindPendienteByCorrelacion(
const Pend: TVFPendientesResponse;
const Corr: TVFCorrelacion;
out Item: TVFPendienteItem
): Boolean;
public
constructor Create(const Cfg: TVfDemoConfig);
destructor Destroy; override;
function IngestaFromJson(const JsonIngesta: string): TVFIngestaResponse;
function IngestaFromFile(const FileName: string): TVFIngestaResponse;
function GetPendientes(NUltimos: Integer; IdEnvio: Int64 = 0; LineaDetalle: Integer = 0): TVFPendientesResponse;
function AckIndice(const IndiceLog: Int64): TVFAckResponse;
function IngestaYConfirmacion(
const JsonIngesta: string;
TimeoutMs: Integer = 5000;
PollIntervalMs: Integer = 200;
NUltimos: Integer = 50;
AckOnFound: Boolean = True
): TVFIngestaAckResult;
end;
implementation
{ TVFCorrelacion }
class function TVFCorrelacion.Empty: TVFCorrelacion;
begin
Result.IdEnvio := 0;
Result.LineaDetalle := 0;
end;
function TVFCorrelacion.IsValid: Boolean;
begin
Result := (IdEnvio > 0) and (LineaDetalle > 0);
end;
{ Helpers JSON }
function JGetStr(Obj: TJSONObject; const Name: string): string;
var
V: TJSONValue;
begin
Result := '';
if Obj = nil then Exit;
V := Obj.GetValue(Name);
if V <> nil then
Result := V.Value;
end;
function JGetInt64(Obj: TJSONObject; const Name: string): Int64;
var
V: TJSONValue;
begin
Result := 0;
if Obj = nil then Exit;
V := Obj.GetValue(Name);
if V = nil then Exit;
if V is TJSONNumber then
Result := TJSONNumber(V).AsInt64
else
Result := StrToInt64Def(V.Value, 0);
end;
function JGetInt(Obj: TJSONObject; const Name: string): Integer;
var
V: TJSONValue;
begin
Result := 0;
if Obj = nil then Exit;
V := Obj.GetValue(Name);
if V = nil then Exit;
if V is TJSONNumber then
Result := TJSONNumber(V).AsInt
else
Result := StrToIntDef(V.Value, 0);
end;
function JGetCurrency(Obj: TJSONObject; const Name: string): Currency;
var
V: TJSONValue;
S: string;
begin
Result := 0;
if Obj = nil then Exit;
V := Obj.GetValue(Name);
if V = nil then Exit;
if V is TJSONNumber then
begin
Result := TJSONNumber(V).AsDouble;
Exit;
end;
S := V.Value;
if S = '' then Exit;
S := StringReplace(S, ',', '.', [rfReplaceAll]);
Result := StrToCurrDef(S, 0, TFormatSettings.Invariant);
end;
function TryParseAnyDate(const S: string): TDateTime;
var
FS: TFormatSettings;
S2: string;
begin
Result := 0;
if S = '' then Exit;
FS := TFormatSettings.Invariant;
FS.DateSeparator := '-';
FS.TimeSeparator := ':';
FS.ShortDateFormat := 'yyyy-mm-dd';
FS.LongDateFormat := 'yyyy-mm-dd';
FS.ShortTimeFormat := 'hh:nn:ss';
FS.LongTimeFormat := 'hh:nn:ss';
S2 := StringReplace(S, 'T', ' ', [rfReplaceAll]);
S2 := StringReplace(S2, 'Z', '', [rfReplaceAll]);
if Pos('.', S2) > 0 then
S2 := Copy(S2, 1, Pos('.', S2) - 1);
if not TryStrToDateTime(S2, Result, FS) then
begin
if not TryStrToDate(S2, Result, FS) then
begin
try
Result := ISO8601ToDate(S, False);
except
Result := 0;
end;
end;
end;
end;
function JGetDateISO(const S: string): TDateTime;
begin
Result := TryParseAnyDate(S);
end;
function JGetDateTimeISO(const S: string): TDateTime;
begin
Result := TryParseAnyDate(S);
end;
function ParseSqlTimestamp(const S: string): TDateTime;
begin
Result := TryParseAnyDate(S);
end;
{ TVFEngine }
constructor TVFEngine.Create(const Cfg: TVfDemoConfig);
begin
inherited Create;
FCfg := Cfg;
FCli := TVfApiClient.Create(FCfg.ApiBaseUrl, FCfg.TimeoutMs, FCfg.Token);
FCli.NifEmisor := FCfg.NifEmisor;
end;
destructor TVFEngine.Destroy;
begin
FreeAndNil(FCli);
inherited;
end;
function TVFEngine.IngestaFromJson(const JsonIngesta: string): TVFIngestaResponse;
function IsSuccess(Code: Integer): Boolean;
begin
// 200 OK, 201 Created, etc. y también 422/409 (Duplicado = éxito lógico)
Result := (Code = 200) or (Code = 201) or (Code = 422) or (Code = 409) or (Code = 403) or (Code = 400) or (Code = 401) or (Code = 500);
end;
var
Resp: string;
Attempt: Integer;
const
MAX_RETRIES = 3;
begin
Result.Ok := False;
Result.HttpStatus := -1;
Result.RawResponseJson := '';
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
Result.Correlacion := TVFCorrelacion.Empty;
for Attempt := 1 to MAX_RETRIES do
begin
try
Resp := FCli.PostJson('/v1/ingesta', JsonIngesta);
Result := ParseIngestaResponse(Resp);
// Si el parseo fue ok y el status es éxito o "ya existe", terminamos
if Result.Ok or IsSuccess(FCli.LastStatusCode) then
begin
Result.HttpStatus := FCli.LastStatusCode;
if (Result.HttpStatus = 422) or (Result.HttpStatus = 409) then
begin
Result.Ok := True; // Lo damos por bueno
Result.Mensaje := Result.Mensaje + ' (Recuperado tras duplicado)';
end
else if not Result.Ok then
begin
if Result.Mensaje <> '' then
Result.ErrorMsg := Result.Mensaje
else
Result.ErrorMsg := 'Rechazado (HTTP ' + IntToStr(Result.HttpStatus) + ')';
end;
Break;
end;
except
on E: Exception do
begin
Result.Ok := False;
Result.ErrorMsg := E.Message;
Result.HttpStatus := FCli.LastStatusCode;
Result.ErrorBodyJson := FCli.LastResponseText;
// Si es el último intento, no hacemos sleep
if Attempt < MAX_RETRIES then
Sleep(1000); // Esperar 1s antes de reintentar
end;
end;
end;
end;
function TVFEngine.IngestaFromFile(const FileName: string): TVFIngestaResponse;
var
Json: string;
begin
if not FileExists(FileName) then
begin
Result.Ok := False;
Result.HttpStatus := -1;
Result.ErrorMsg := 'No existe el archivo: ' + FileName;
Exit;
end;
Json := TFile.ReadAllText(FileName, TEncoding.UTF8);
Result := IngestaFromJson(Json);
end;
function TVFEngine.GetPendientes(NUltimos: Integer; IdEnvio: Int64 = 0; LineaDetalle: Integer = 0): TVFPendientesResponse;
var
Resp: string;
Url: string;
N: Integer;
TieneCorrelacion: Boolean;
begin
Result.Ok := False;
Result.HttpStatus := -1;
Result.RawResponseJson := '';
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
SetLength(Result.Items, 0);
TieneCorrelacion := (IdEnvio > 0) and (LineaDetalle > 0);
// --- CAMBIO CLAVE ---
// Para que p_n_ultimos sea NULL en el SP, NO se debe enviar el parámetro n_ultimos.
// Eso solo lo hacemos cuando estamos en modo correlación (id_envio + linea_log_detalle).
if TieneCorrelacion then
begin
// URL SIN n_ultimos => p_n_ultimos = NULL en servidor
Url := Format('/verifactu/pendientes?nif_emisor=%s&id_envio=%d&linea_log_detalle=%d',
[UrlEncode(FCfg.NifEmisor), IdEnvio, LineaDetalle]);
end
else
begin
// Modo listado: respetamos n_ultimos (default 50 si viene <=0)
N := NUltimos;
if N < 1 then N := 50;
Url := Format('/verifactu/pendientes?nif_emisor=%s&n_ultimos=%d',
[UrlEncode(FCfg.NifEmisor), N]);
end;
// --- FIN CAMBIO CLAVE ---
try
Resp := FCli.GetText(Url);
Result := ParsePendientesResponse(Resp);
Result.HttpStatus := 200;
except
on E: Exception do
begin
Result.Ok := False;
Result.ErrorMsg := E.Message;
Result.HttpStatus := FCli.LastStatusCode;
Result.ErrorBodyJson := FCli.LastResponseText;
end;
end;
end;
function TVFEngine.AckIndice(const IndiceLog: Int64): TVFAckResponse;
var
ReqJson: string;
Resp: string;
begin
Result.Ok := False;
Result.HttpStatus := -1;
Result.RawResponseJson := '';
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
try
ReqJson := BuildAckJson(FCfg.NifEmisor, IndiceLog);
Resp := FCli.PostJson('/verifactu/ack', ReqJson);
Result := ParseAckResponse(Resp);
Result.HttpStatus := 200;
except
on E: Exception do
begin
Result.Ok := False;
Result.ErrorMsg := E.Message;
Result.HttpStatus := FCli.LastStatusCode;
Result.ErrorBodyJson := FCli.LastResponseText;
end;
end;
end;
function TVFEngine.IngestaYConfirmacion(
const JsonIngesta: string;
TimeoutMs, PollIntervalMs, NUltimos: Integer;
AckOnFound: Boolean
): TVFIngestaAckResult;
var
T0: Cardinal;
Pend: TVFPendientesResponse;
Item: TVFPendienteItem;
begin
FillChar(Result, SizeOf(Result), 0);
Result.IndiceLogEncontrado := 0;
Result.Timeout := False;
Result.ErrorMsg := '';
Result.Ingesta := IngestaFromJson(JsonIngesta);
if not Result.Ingesta.Ok then
begin
Result.ErrorMsg := 'Ingesta fallida: ' + Result.Ingesta.ErrorMsg;
Exit;
end;
if not Result.Ingesta.Correlacion.IsValid then
begin
Result.ErrorMsg := 'No se pudo obtener correlación (IdEnvio/LineaDetalle) desde la respuesta de ingesta.';
Exit;
end;
T0 := TThread.GetTickCount;
while True do
begin
// Optimizacion MAXIMA: Buscamos especificamente este ID envio y linea detalle
// (ahora además: NO mandamos n_ultimos => p_n_ultimos llega NULL al SP)
Pend := GetPendientes(NUltimos, Result.Ingesta.Correlacion.IdEnvio, Result.Ingesta.Correlacion.LineaDetalle);
if Pend.Ok then
begin
if TryFindPendienteByCorrelacion(Pend, Result.Ingesta.Correlacion, Item) then
begin
Result.EncontradoEnPendientes := True;
Result.Pendiente := Item;
Result.IndiceLogEncontrado := Item.IndiceLog;
if AckOnFound then
begin
Result.Ack := AckIndice(Item.IndiceLog);
Result.AckHecho := Result.Ack.Ok;
if not Result.Ack.Ok then
Result.ErrorMsg := 'ACK fallido: ' + Result.Ack.ErrorMsg;
end;
Exit;
end;
end;
if (TimeoutMs > 0) and (TThread.GetTickCount - T0 >= Cardinal(TimeoutMs)) then
begin
Result.Timeout := True;
Result.ErrorMsg := 'Timeout esperando aparición en pendientes.';
Exit;
end;
Sleep(PollIntervalMs);
end;
end;
function TVFEngine.TryFindPendienteByCorrelacion(
const Pend: TVFPendientesResponse;
const Corr: TVFCorrelacion;
out Item: TVFPendienteItem
): Boolean;
var
I: Integer;
begin
Result := False;
FillChar(Item, SizeOf(Item), 0);
if not Pend.Ok then Exit;
if not Corr.IsValid then Exit;
for I := 0 to High(Pend.Items) do
begin
if (Pend.Items[I].IdEnvio = Corr.IdEnvio) and
(Pend.Items[I].LineaLogDetalle = Corr.LineaDetalle) then
begin
Item := Pend.Items[I];
Exit(True);
end;
end;
end;
function TVFEngine.ParseIngestaResponse(const RespJson: string): TVFIngestaResponse;
var
V: TJSONValue;
Obj, Trk: TJSONObject;
begin
Result.Ok := False;
Result.HttpStatus := 200;
Result.RawResponseJson := RespJson;
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
Result.Correlacion := TVFCorrelacion.Empty;
Result.TrackingTimestamp := 0;
V := TJSONObject.ParseJSONValue(RespJson);
try
if not (V is TJSONObject) then
begin
Result.ErrorMsg := 'Respuesta de ingesta no es JSON object.';
Exit;
end;
Obj := TJSONObject(V);
Result.Mensaje := JGetStr(Obj, 'mensaje');
if Result.Mensaje = '' then
Result.Mensaje := JGetStr(Obj, 'detail');
Result.Id := JGetStr(Obj, 'id');
Result.IdFormateado := JGetStr(Obj, 'id_formateado');
Result.OperacionRealizada := JGetStr(Obj, 'operacion_realizada');
Result.Ok := SameText(JGetStr(Obj, 'status'), 'ok');
Trk := Obj.GetValue('tracking') as TJSONObject;
if Trk <> nil then
begin
Result.TrackingEmisor := JGetStr(Trk, 'emisor');
Result.TrackingSerie := JGetStr(Trk, 'serie');
Result.TrackingNumFactura := JGetStr(Trk, 'numfactura');
Result.TrackingTimestamp := JGetDateTimeISO(JGetStr(Trk, 'timestamp'));
Result.Correlacion.IdEnvio := JGetInt64(Trk, 'cab_num_secuencia');
Result.Correlacion.LineaDetalle := JGetInt(Trk, 'det_linea');
end;
finally
V.Free;
end;
end;
function TVFEngine.ParsePendientesResponse(const RespJson: string): TVFPendientesResponse;
var
V: TJSONValue;
Arr: TJSONArray;
I: Integer;
Obj: TJSONObject;
It: TVFPendienteItem;
SFecha, SCreacion: string;
begin
Result.Ok := False;
Result.HttpStatus := 200;
Result.RawResponseJson := RespJson;
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
SetLength(Result.Items, 0);
V := TJSONObject.ParseJSONValue(RespJson);
try
if not (V is TJSONArray) then
begin
if V is TJSONObject then
begin
Result.ErrorMsg := JGetStr(V as TJSONObject, 'mensaje');
if Result.ErrorMsg = '' then Result.ErrorMsg := JGetStr(V as TJSONObject, 'detail');
end;
if Result.ErrorMsg = '' then
Result.ErrorMsg := 'Respuesta de pendientes no es JSON array. Contenido: ' + Copy(RespJson, 1, 100);
Exit;
end;
Arr := TJSONArray(V);
SetLength(Result.Items, Arr.Count);
for I := 0 to Arr.Count - 1 do
begin
FillChar(It, SizeOf(It), 0);
if not (Arr.Items[I] is TJSONObject) then
Continue;
Obj := TJSONObject(Arr.Items[I]);
It.IndiceLog := JGetInt64(Obj, 'indice_log');
SCreacion := JGetStr(Obj, 'creacion');
It.Creacion := ParseSqlTimestamp(SCreacion);
It.Modo := JGetInt(Obj, 'modo');
It.IdEnvio := JGetInt64(Obj, 'id_envio');
It.LineaLogDetalle := JGetInt(Obj, 'linea_log_detalle');
It.NumSerieFactura := JGetStr(Obj, 'num_serie_factura');
It.NumFactura := JGetStr(Obj, 'num_serie_factura_numero');
if It.NumFactura = '' then It.NumFactura := JGetStr(Obj, 'numero_factura');
if It.NumFactura = '' then It.NumFactura := JGetStr(Obj, 'num_factura');
// Unificamos para la UI: Si hay serie y numero, los juntamos. Si no, lo que haya.
if (It.NumSerieFactura <> '') and (It.NumFactura <> '') then
It.NumSerieFactura := It.NumSerieFactura + '-' + It.NumFactura
else if It.NumFactura <> '' then
It.NumSerieFactura := It.NumFactura;
// Si solo hay serie (raro), se queda serie.
SFecha := JGetStr(Obj, 'fecha_expedicion_factura');
It.FechaExpedicion := JGetDateISO(SFecha);
It.Huella := JGetStr(Obj, 'huella');
It.CSV := JGetStr(Obj, 'csv');
It.Total := JGetCurrency(Obj, 'total');
It.CodigoError := JGetInt(Obj, 'codigo_error_verifactu');
It.DescripcionError := JGetStr(Obj, 'descripcion_error_verifactu');
It.Status := JGetInt(Obj, 'status');
It.QrVerifactu := JGetStr(Obj, 'qr_verifactu');
It.UrlQrVerifactu := JGetStr(Obj, 'url_qr_verifactu');
Result.Items[I] := It;
end;
Result.Ok := True;
finally
V.Free;
end;
end;
function TVFEngine.ParseAckResponse(const RespJson: string): TVFAckResponse;
var
V: TJSONValue;
Obj: TJSONObject;
begin
Result.Ok := False;
Result.HttpStatus := 200;
Result.RawResponseJson := RespJson;
Result.ErrorMsg := '';
Result.ErrorBodyJson := '';
V := TJSONObject.ParseJSONValue(RespJson);
try
if not (V is TJSONObject) then
begin
Result.ErrorMsg := 'Respuesta de ACK no es JSON object.';
Exit;
end;
Obj := TJSONObject(V);
Result.Status := JGetStr(Obj, 'status');
Result.Message := JGetStr(Obj, 'message');
if Result.Message = '' then Result.Message := JGetStr(Obj, 'mensaje');
Result.Ok := SameText(Result.Status, 'ok');
if not Result.Ok then
Result.ErrorMsg := Result.Message;
finally
V.Free;
end;
end;
end.