Guía de Integración Rápida con C# (.NET)
El SDK de C# (.NET Core / .NET 5+) está concebido como una aplicación de consola que sirve de ejemplo y esqueleto para integrarlo de forma cristalina en tu ERP nativo de Windows (WPF, WinForms) o en tu backend ASP.NET. La lógica principal asíncrona recae en VFEngine.cs.
🛠️ Entorno: Instalación y Compilación
Como este SDK se entrega directamente como código fuente transparente en lugar de una aburrida DLL cerrada, la forma de prepararlo y probarlo es muy sencilla utilizando la CLI de .NET integrada o Visual Studio.
- Asegúrate de tener instalado el .NET SDK en tu Windows.
- Abre tu terminal (PowerShell, CMD, o la terminal de VSCode) y navega a la carpeta donde has volcado estos archivos (
Program.cs,VFEngine.cs,CSharpDemo.csproj, etc). - Compila el binario con el siguiente comando:bash
dotnet build - Para arrancar el cliente interactivo y comunicarte con tu API, ejecuta:bash
dotnet run
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
TIP
Contexto de la demostración: Observa la pantalla dividida. En la mitad de la izquierda se muestra el panel de control del Micro Server en vivo. En la mitad derecha verás una ventana nativa de PowerShell corriendo precisamente esta aplicación de consola C# recién compilada (dotnet run). Serás testigo de cómo el cliente C# envía lotes de facturas a ráfagas y el Micro Server las devora procesándolas y devolviendo los QRs a la misma velocidad.
🏗️ 1. Configuración del Contexto (Config.json)
Dentro de la aplicación C#, hemos delegado los ajustes estáticos a Config.json para facilitar el cambio rápido de entornos (Desarrollo VS Producción).
{
"api": {
"base_url": "http://localhost:8000",
"timeout_sec": 60,
"token": "TXT-VFACTU-TU-TOKEN",
"ssl_verify": false
},
"demo": {
"nif_emisor": "B12345678",
"serie": "TEST",
"num_inicio": "1"
}
}Manejando ssl_verify : false es como logramos probar las APIs auto-firmadas nativas desde el host local de IIS/Kestrel o desde la pasarela sin que .NET nos propague la clásica excepción de Untrusted Root Certificate.
🚀 2. Ingesta Asíncrona con TPL
Nuestro SDK abraza firmemente el patrón async/await (TPL - Task Parallel Library). Emitir una factura es una llamada asíncrona sin bloqueos usando HttpClient.
// Instanciamos el Motor con la clase principal (VFEngine.cs)
var engine = new VFEngine("http://localhost:8000", "MI-TOKEN", "B12345678", sslVerify: false, 60);
// Construimos o leemos el JSON nativo (System.Text.Json.Nodes)
var factura = JsonNode.Parse("{ \"cabecera\": { ... } }");
// Transmisión Asíncrona al Micro Server
var res = await engine.IngestaJsonAsync(factura);
// Validamos el resultado (200 OK y Body devuelto)
if (res?["ok"]?.GetValue<bool>() == true)
{
Console.WriteLine("¡Factura subida y procesando en la cola de la AEAT!");
}📦 Código Fuente de la Integración
Esta arquitectura sin DLLs, donde tú retienes el control total, se compone de los siguientes ficheros limpios y validados. Extrae estos elementos para incrustarlos en tu Solución C#.
A continuación se incluye el código fuente completo de cada uno para que puedas integrarlo sin perder tiempo.
CSharpDemo.csproj
Archivo de definición del proyecto .NET Core.
Ver/Ocultar Código Fuente (CSharpDemo.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Update="Config.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="factura_ejemplo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>Config.json
Configuración centralizada.
Ver/Ocultar Código Fuente (Config.json)
{
"api": {
"base_url": "http://192.168.1.100:8000",
"timeout_sec": 60,
"token": "vf_sys_fgh_BEwEMUFqXWEfPOBKcY9INWdmaRFoAhLV",
"ssl_verify": false
},
"demo": {
"nif_emisor": "",
"serie": "",
"num_inicio": ""
}
}VFEngine.cs
Clase principal asíncrona (async/await) utilizando HttpClient nativo.
Ver/Ocultar Código Fuente (VFEngine.cs)
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace CSharpDemo
{
public class VFEngine
{
private readonly HttpClient _client;
private readonly string _baseUrl;
private readonly string _token;
private string _nifEmisor;
public VFEngine(string baseUrl, string token, string nifEmisor, bool sslVerify, int timeoutSec)
{
_baseUrl = baseUrl.TrimEnd('/');
_token = token;
_nifEmisor = nifEmisor;
var handler = new HttpClientHandler();
if (!sslVerify)
{
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
}
_client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(timeoutSec)
};
// Set default headers
_client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
if (!string.IsNullOrEmpty(_token))
{
_client.DefaultRequestHeaders.Add("X-API-Key", _token);
}
SetEmisorHeader(_nifEmisor);
}
public void SetEmisorHeader(string nif)
{
if (!string.IsNullOrEmpty(nif))
{
_nifEmisor = nif;
_client.DefaultRequestHeaders.Remove("X-Verifactu-Emisor");
_client.DefaultRequestHeaders.Add("X-Verifactu-Emisor", nif);
}
}
public async Task<JsonNode?> IngestaJsonAsync(JsonNode json)
{
try
{
var cab = json["cabecera"];
var nif = cab?["emisor"]?.ToString() ?? cab?["nif_emisor"]?.ToString();
if (!string.IsNullOrEmpty(nif)) SetEmisorHeader(nif);
var url = $"{_baseUrl}/v1/ingesta";
var response = await _client.PostAsJsonAsync(url, json);
return await ParseResponse(response);
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
public async Task<JsonNode?> CheckStatusAsync(string emisor, string serie, string num)
{
try
{
SetEmisorHeader(emisor);
var url = $"{_baseUrl}/v1/check_status?emisor={emisor}&serie={serie}&num={num}";
var response = await _client.GetAsync(url);
return await ParseResponse(response);
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
public async Task<JsonNode?> GetPendientesAsync(string nifEmisor, int limit = 50, string? idEnvio = null, string? lineaLogDetalle = null)
{
SetEmisorHeader(nifEmisor);
try
{
var url = $"{_baseUrl}/verifactu/pendientes?nif_emisor={nifEmisor}";
if (!string.IsNullOrEmpty(idEnvio) && !string.IsNullOrEmpty(lineaLogDetalle))
{
url += $"&id_envio={idEnvio}&linea_log_detalle={lineaLogDetalle}";
}
else
{
url += $"&n_ultimos={limit}";
}
var response = await _client.GetAsync(url);
return await ParseResponse(response);
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
public async Task<JsonNode?> GetEstadoFacturaAsync(string nif, string num, string serie = "")
{
try
{
SetEmisorHeader(nif);
var url = $"{_baseUrl}/v1/facturas/estado?emisor={nif}&numfactura={num}&serie={serie}";
var response = await _client.GetAsync(url);
return await ParseResponse(response);
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
public async Task<JsonNode?> AckIndiceAsync(string nifEmisor, int indiceLog)
{
SetEmisorHeader(nifEmisor);
try
{
var url = $"{_baseUrl}/verifactu/ack";
var payload = new
{
nif_emisor = nifEmisor,
indice_log = indiceLog
};
var response = await _client.PostAsJsonAsync(url, payload);
return await ParseResponse(response);
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
private async Task<JsonNode?> ParseResponse(HttpResponseMessage response)
{
try
{
var content = await response.Content.ReadAsStringAsync();
// Try parsing as JSON
try
{
var node = JsonNode.Parse(content);
if (node is JsonObject obj)
{
// Inject HTTP Status code for debugging
obj["http_code"] = (int)response.StatusCode;
}
return node;
}
catch (JsonException)
{
// Not JSON (e.g. 502 Proxy Error HTML)
return new JsonObject
{
["ok"] = false,
["error"] = "Non-JSON Response",
["raw"] = content.Length > 500 ? content.Substring(0, 500) : content,
["http_code"] = (int)response.StatusCode
};
}
}
catch (Exception ex)
{
return CreateErrorNode(ex.Message);
}
}
private JsonNode CreateErrorNode(string msg)
{
return new JsonObject
{
["ok"] = false,
["error"] = msg
};
}
}
}Program.cs
Bucle interactivo de consola que demuestra los 6 casos de uso del motor.
Ver/Ocultar Código Fuente (Program.cs)
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using System.Linq;
namespace CSharpDemo
{
class Program
{
static VFEngine? _engine;
static JsonNode? _config;
static string _nif = "";
static string _serie = "";
static string _num = "";
static async Task Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine("========================================");
Console.WriteLine(" VERIFACTU MICRO SERVER - C# CONSOLE SDK");
Console.WriteLine("========================================");
// 1. Cargar Configuración Inicial
string configPath = "Config.json";
if (!File.Exists(configPath))
{
Error("Fichero Config.json no encontrado.");
return;
}
try
{
var jsonString = File.ReadAllText(configPath);
_config = JsonNode.Parse(jsonString);
var api = _config?["api"];
var demo = _config?["demo"];
if (api == null) throw new Exception("Configuración inválida: falta sección 'api'.");
string baseUrl = api["base_url"]?.ToString() ?? "http://localhost:8000";
string token = api["token"]?.ToString() ?? "";
int timeout = api["timeout_sec"]?.GetValue<int>() ?? 60;
bool sslVerify = api["ssl_verify"]?.GetValue<bool>() ?? false;
_nif = demo?["nif_emisor"]?.ToString() ?? "00000000T";
_serie = demo?["serie"]?.ToString() ?? "TEST";
_num = demo?["num_inicio"]?.ToString() ?? "1";
_engine = new VFEngine(baseUrl, token, _nif, sslVerify, timeout);
Success($"[*] API Conectada URL: {baseUrl}");
}
catch (Exception ex)
{
Error("Error cargando configuración: " + ex.Message);
return;
}
// 1.1 Sincronizar con el JSON de Ejemplo obligatoriamente
SyncWithJsonTemplate();
// Main Loop del Programa
while (true)
{
Console.WriteLine("\n================= MENÚ =================");
Console.WriteLine($" Emisor: {_nif} | Serie: {_serie} | Prox. Núm: {_num}");
Console.WriteLine("----------------------------------------");
Console.WriteLine(" 1. 🚀 Envío de una operacion -> Espera de respuesta");
Console.WriteLine(" 2. 📦 Ingestar Lote de Facturas (Masivo)");
Console.WriteLine(" 3. 🔍 Consultar Estado de una Factura");
Console.WriteLine(" 4. 📬 Ver Facturas Pendientes (AEAT)");
Console.WriteLine(" 5. ✔️ Confirmar (ACK) MASIVO (Todas)");
Console.WriteLine(" 6. ✔️ Confirmar (ACK) Individual (Por ID)");
Console.WriteLine(" 7. 🚪 Salir");
Console.Write("Seleccione opción: ");
var key = Console.ReadKey().KeyChar;
Console.WriteLine("\n");
try
{
switch (key)
{
case '1': await IngestaIndividual(); break;
case '2': await IngestaLote(); break;
case '3': await CheckStatus(); break;
case '4': await GetPendientes(); break;
case '5': await AckMasivo(); break;
case '6': await AckManual(); break;
case '7': return;
default: Console.WriteLine("Opción no válida."); break;
}
}
catch (Exception ex)
{
Error($"Excepción global capturada: {ex.Message}");
}
}
}
// --- Funciones Base ---
static JsonNode? LoadTemplate()
{
string tplPath = "factura_ejemplo.json";
if (!File.Exists(tplPath))
{
Error($"No se encuentra el archivo principal de ingesta: {tplPath}");
return null;
}
try
{
return JsonNode.Parse(File.ReadAllText(tplPath));
}
catch (Exception ex)
{
Error($"Error parseando {tplPath}: {ex.Message}");
return null;
}
}
static void SaveTemplate(JsonNode factura)
{
// Guarda el nuevo estado de la factura (con el numerador incrementado) en el fichero json
var options = new JsonSerializerOptions { WriteIndented = true };
File.WriteAllText("factura_ejemplo.json", factura.ToJsonString(options));
SyncWithJsonTemplate(); // Para refrescar las variables de consola
}
static void SyncWithJsonTemplate()
{
try
{
var factura = LoadTemplate();
if (factura != null)
{
var cab = factura["cabecera"];
if (cab != null)
{
var cabEmisor = cab["emisor"]?.ToString() ?? cab["nif_emisor"]?.ToString();
if (!string.IsNullOrEmpty(cabEmisor)) _nif = cabEmisor.Trim();
var cabSerie = cab["serie"]?.ToString();
if (!string.IsNullOrEmpty(cabSerie)) _serie = cabSerie.Trim();
var cabNum = cab["numfactura"]?.ToString();
if (!string.IsNullOrEmpty(cabNum)) _num = cabNum.Trim();
}
}
}
catch { }
}
static string IncrementNum(string currentNum)
{
currentNum = currentNum?.Trim() ?? "";
if (int.TryParse(currentNum, out int numericNum))
{
return (numericNum + 1).ToString();
}
// Fallback para strings complejos (ej: F2024-100 -> F2024-100-1)
return currentNum + "-1";
}
static void InjectMetadataAndDate(JsonNode factura)
{
factura["cabecera"]!["emisor"] = _nif;
factura["cabecera"]!["fecha"] = DateTime.Now.ToString("dd/MM/yyyy");
factura["cabecera"]!["hora"] = DateTime.Now.ToString("HH:mm:ss");
if (factura["metadata"] == null) factura["metadata"] = new JsonObject();
factura["metadata"]!["enviar_aeat"] = true;
}
// --- Casos de Uso del Menú ---
static async Task IngestaIndividual()
{
Info($"Preparando factura {_serie}-{_num}...");
var factura = LoadTemplate();
if (factura == null) return;
InjectMetadataAndDate(factura);
Info($"Enviando...");
var res = await _engine!.IngestaJsonAsync(factura);
Console.WriteLine(res?.ToString());
string sentSerie = _serie;
string sentNum = _num;
// Incrementar y guardar en fichero para la próxima vez
factura["cabecera"]!["numfactura"] = IncrementNum(_num);
SaveTemplate(factura);
Success($"Factura enviada. Json guardado preparado para número {_num}.");
Info("Esperando respuesta de la AEAT (Polling)...");
for (int i = 0; i < 15; i++)
{
await Task.Delay(1000);
var st = await _engine.CheckStatusAsync(_nif, sentSerie, sentNum);
if (st != null)
{
string statusTxt = st["status"]?.ToString()?.ToLower() ?? "";
if (!string.IsNullOrEmpty(statusTxt) && statusTxt != "pending" && statusTxt != "unknown" && statusTxt != "error" && statusTxt != "processing" && statusTxt != "en cola")
{
Console.WriteLine("\n✅ \x1b[92mRespuesta de la AEAT recibida!\x1b[0m");
Console.WriteLine($" Estado: {st["status"]?.ToString() ?? "???"}");
var det = st["detalles_aeat"];
string csvVal = st["csv"]?.ToString() ?? det?["cab_csv_verifactu"]?.ToString() ?? "N/A";
Console.WriteLine($" CSV: {csvVal}");
string huella = det?["huella"]?.ToString() ?? "N/A";
Console.WriteLine($" Huella: {huella}");
string qr = det?["cab_url_qr"]?.ToString() ?? det?["url_qr_verifactu"]?.ToString() ?? "N/A";
Console.WriteLine($" URL QR: {qr}\n");
return;
}
}
}
Error("⏳ Timeout esperando la respuesta de la AEAT. Puede seguir en cola.");
}
static async Task IngestaLote()
{
Console.Write("¿Cuántas facturas deseas enviar en lote? (ej: 5): ");
var input = Console.ReadLine();
if (!int.TryParse(input, out int cantidad) || cantidad <= 0)
{
Error("Cantidad inválida.");
return;
}
var facturaBase = LoadTemplate();
if (facturaBase == null) return;
Info($"--- INICIO INGESTA LOTE ({cantidad} FACTURAS) ---");
string currentNum = _num;
for (int i = 0; i < cantidad; i++)
{
// Clonar objeto json
var factura = JsonNode.Parse(facturaBase.ToJsonString());
factura!["cabecera"]!["numfactura"] = currentNum;
InjectMetadataAndDate(factura);
Info($"-> Enviando {_serie}-{currentNum} (Factura {i+1}/{cantidad})...");
var res = await _engine!.IngestaJsonAsync(factura);
var status = res?["status"]?.ToString() ?? res?["ok"]?.ToString();
if (status == "ok" || status == "True" || status == "True")
Success($" OK: {currentNum}");
else
Error($" Error en {currentNum}: " + res?.ToString());
// Avanzar numerador
currentNum = IncrementNum(currentNum);
await Task.Delay(200); // Pequeña cortesia al servidor
}
// Actualizar el archivo final para futuros envíos manuales
facturaBase["cabecera"]!["numfactura"] = currentNum;
SaveTemplate(facturaBase);
Success($"--- FIN DE LOTE ---. Preparado el json para la siguiente: {currentNum}");
}
static async Task CheckStatus()
{
Console.Write($"Serie (actual: {_serie}): ");
var s = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(s)) s = _serie;
Console.Write($"Núm (ej: 100): ");
var n = Console.ReadLine()?.Trim();
if (string.IsNullOrEmpty(n)) { Error("Debe introducir un número"); return; }
Info($"Consultando {_nif} - {s}-{n}...");
var res = await _engine!.CheckStatusAsync(_nif, s, n);
Console.WriteLine(res?.ToString());
}
static async Task GetPendientes()
{
Info("Consultando Bandeja de Pendientes AEAT (Últimas 50)...");
var res = await _engine!.GetPendientesAsync(_nif, 50);
if (res is JsonArray array)
{
DrawPendientesTable(array);
}
else if (res?["items"] != null && res["items"] is JsonArray itemsArr)
{
DrawPendientesTable(itemsArr);
}
else
{
// Podría estar devolviendo un JSON Object/Dict
Console.WriteLine(res?.ToString());
}
}
static void DrawPendientesTable(JsonArray array)
{
if (array.Count == 0)
{
Success("\n 🎉 BANDEJA LIMPIA: 0 facturas pendientes.");
return;
}
Console.WriteLine("\n--- LISTADO DE PENDIENTES ---");
Console.WriteLine($"{"ID Log",-8} | {"Factura",-15} | {"Importe",-10} | {"Fecha",-10} | {"Status",-6} | {"Cod. Error",-10} | {"Error",-30}");
Console.WriteLine(new String('-', 110));
foreach (var item in array)
{
var id = item["indice_log"]?.ToString() ?? "???";
var idFact = item["num_serie_factura"]?.ToString() ?? "???";
var total = item["total"]?.ToString() ?? "0";
var date = item["fecha_expedicion_factura"]?.ToString() ?? "???";
var status = item["status"]?.ToString() ?? "-";
var codError = item["codigo_error_verifactu"]?.ToString() ?? "-";
var errorDesc = item["descripcion_error_verifactu"]?.ToString() ?? "-";
// Truncamos la descripción del error si es muy larga para que no nos rompa la tabla de la consola
if (errorDesc.Length > 28) errorDesc = errorDesc.Substring(0, 25) + "...";
Console.WriteLine($"{id,-8} | {idFact,-15} | {total,-10} | {date,-10} | {status,-6} | {codError,-10} | {errorDesc,-30}");
}
Console.WriteLine("\nTotal Pendientes: " + array.Count);
}
static async Task AckMasivo()
{
Info("Consiguiendo IDs pendientes para vaciar bandeja...");
var res = await _engine!.GetPendientesAsync(_nif, 50);
JsonArray? itemsToAck = null;
if (res is JsonArray a) itemsToAck = a;
else if (res?["items"] != null && res["items"] is JsonArray b) itemsToAck = b;
if (itemsToAck == null || itemsToAck.Count == 0)
{
Success("No hay facturas pendientes para hacer ACK.");
return;
}
Info($"Iniciando confirmación masiva de {itemsToAck.Count} elementos...");
int successCount = 0;
foreach (var item in itemsToAck)
{
if (item["indice_log"] == null) continue;
int id = item["indice_log"]!.GetValue<int>();
var fact = item["num_serie_factura"]?.ToString() ?? id.ToString();
Console.Write($" -> Confirmando {fact} (ID: {id})... ");
var ackRes = await _engine.AckIndiceAsync(_nif, id);
string ok = ackRes?["ok"]?.ToString() ?? ackRes?["status"]?.ToString() ?? "";
if (ok == "True" || ok == "true" || ok == "ok")
{
Console.WriteLine("✅ OK");
successCount++;
}
else
{
Console.WriteLine($"❌ FALLO: {ackRes?.ToString()}");
}
}
Success($"Proceso Finalizado. Confirmadas {successCount} de {itemsToAck.Count}.");
}
static async Task AckManual()
{
Console.Write("Introduce el ID (indice_log) interno a confirmar: ");
var s = Console.ReadLine();
if (int.TryParse(s, out int id))
{
Info($"Enviando ACK aislado al ID {id}...");
var ack = await _engine!.AckIndiceAsync(_nif, id);
Console.WriteLine(ack?.ToString());
}
else
{
Error("ID inválido. Debe ser un número entero.");
}
}
// --- Utilidades Logs ---
static void Info(string msg) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine(msg); Console.ResetColor(); }
static void Success(string msg) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(msg); Console.ResetColor(); }
static void Error(string msg) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(msg); Console.ResetColor(); }
}
}factura_ejemplo.json
Plantilla JSON validada esquemáticamente lista para usarse.
Ver/Ocultar Código Fuente (factura_ejemplo.json)
{
"metadata": {
"enviar_aeat": false,
"generar_xml": false,
"insertar_en_db": true,
"simulacion": true
},
"cabecera": {
"emisor": "",
"numfactura": "313",
"serie": "V",
"rectificativa": "N",
"tipofacturarectificativa": "",
"fecharectificada": "",
"facturarectificada": "",
"rectificativasustitucion": "",
"facturaf3": "N",
"numseriesustituidaf3": "",
"fechafactsustituidaf3": "",
"descripcionoperacion": "Venta mercaderías",
"eliminacion": "",
"tipodoc": "01",
"pais": "ES",
"nif": "B99999999",
"nombre": "EMPRESA DE PRUEBAS, S.L.",
"fecha": "15/02/2026",
"totaliva": "63.55",
"totalrecargo": "0.00",
"base": "438.31",
"base1": "103.07",
"piva1": "10.00",
"iva1": "10.31",
"precargo1": "",
"recargo1": "",
"base2": "234.27",
"piva2": "21.00",
"iva2": "49.20",
"precargo2": "",
"recargo2": "",
"base3": "100.97",
"piva3": "4.00",
"iva3": "4.04",
"precargo3": "",
"recargo3": "",
"base4": "",
"piva4": "",
"iva4": "",
"precargo4": "",
"recargo4": ""
},
"detalle": {
"base": [
"103.07",
"234.27",
"100.97",
""
],
"piva": [
"10.00",
"21.00",
"4.00",
""
],
"iva": [
"10.31",
"49.20",
"4.04",
""
],
"precargo": [
"",
"",
"",
""
],
"recargo": [
"",
"",
"",
""
]
}
}