diff --git a/packages/mcp/src/server.spec.ts b/packages/mcp/src/server.spec.ts
index 44a5bba..9396f5c 100644
--- a/packages/mcp/src/server.spec.ts
+++ b/packages/mcp/src/server.spec.ts
@@ -180,6 +180,168 @@ describe("MCP Server", () => {
expect(text).toContain("FacturaElectronica");
});
+ it("should create an invoice with a single exempt line item (no tax)", async () => {
+ const result = await client.callTool({
+ name: "create_invoice",
+ arguments: {
+ emisor: {
+ nombre: "Exenta S.A.",
+ identificacion: { tipo: "02", numero: "3109876543" },
+ correoElectronico: "exenta@empresa.com",
+ },
+ receptor: { nombre: "Cliente Exento" },
+ codigoActividad: "620100",
+ lineItems: [
+ {
+ codigoCabys: "8310100000000",
+ cantidad: 2,
+ unidadMedida: "Unid",
+ detalle: "Producto exento de impuesto",
+ precioUnitario: 5000,
+ },
+ ],
+ },
+ });
+
+ expect(result.isError).toBeFalsy();
+ const text = getTextContent(result.content as { type: string; text: string }[]);
+ expect(text).toContain("Clave:");
+ expect(text).toContain(" 0", async () => {
+ const result = await client.callTool({
+ name: "create_invoice",
+ arguments: {
+ emisor: {
+ nombre: "Gravada S.A.",
+ identificacion: { tipo: "02", numero: "3101234567" },
+ correoElectronico: "gravada@empresa.com",
+ },
+ receptor: { nombre: "Cliente Gravado" },
+ codigoActividad: "620100",
+ lineItems: [
+ {
+ codigoCabys: "8310100000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Servicio gravado",
+ precioUnitario: 10000,
+ esServicio: true,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ },
+ ],
+ },
+ });
+
+ expect(result.isError).toBeFalsy();
+ const text = getTextContent(result.content as { type: string; text: string }[]);
+ expect(text).toContain("Tax: 1300");
+ expect(text).toContain("Total: 11300");
+ expect(text).toContain("1300");
+ });
+
+ it("should return error when emisor is missing", async () => {
+ const result = await client.callTool({
+ name: "create_invoice",
+ arguments: {
+ receptor: { nombre: "Cliente" },
+ codigoActividad: "620100",
+ lineItems: [
+ {
+ codigoCabys: "8310100000000",
+ cantidad: 1,
+ unidadMedida: "Unid",
+ detalle: "Item",
+ precioUnitario: 1000,
+ },
+ ],
+ },
+ });
+
+ expect(result.isError).toBe(true);
+ const text = getTextContent(result.content as { type: string; text: string }[]);
+ expect(text.toLowerCase()).toContain("error");
+ });
+
+ it("should create an invoice with multiple line items", async () => {
+ const result = await client.callTool({
+ name: "create_invoice",
+ arguments: {
+ emisor: {
+ nombre: "Multi S.A.",
+ identificacion: { tipo: "02", numero: "3101234567" },
+ correoElectronico: "multi@empresa.com",
+ },
+ receptor: { nombre: "Cliente Multi" },
+ codigoActividad: "620100",
+ lineItems: [
+ {
+ codigoCabys: "8310100000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Consultoria estrategica",
+ precioUnitario: 50000,
+ esServicio: true,
+ },
+ {
+ codigoCabys: "4321000000000",
+ cantidad: 3,
+ unidadMedida: "Unid",
+ detalle: "Licencia de software",
+ precioUnitario: 20000,
+ },
+ ],
+ },
+ });
+
+ expect(result.isError).toBeFalsy();
+ const text = getTextContent(result.content as { type: string; text: string }[]);
+ expect(text).toContain("Consultoria estrategica");
+ expect(text).toContain("Licencia de software");
+ expect(text).toContain("Total: 110000");
+ });
+
+ it("should create an invoice with a discount on a line item", async () => {
+ const result = await client.callTool({
+ name: "create_invoice",
+ arguments: {
+ emisor: {
+ nombre: "Descuento S.A.",
+ identificacion: { tipo: "02", numero: "3101234567" },
+ correoElectronico: "desc@empresa.com",
+ },
+ receptor: { nombre: "Cliente Descuento" },
+ codigoActividad: "620100",
+ lineItems: [
+ {
+ codigoCabys: "8310100000000",
+ cantidad: 1,
+ unidadMedida: "Unid",
+ detalle: "Producto con descuento",
+ precioUnitario: 100000,
+ descuento: [
+ {
+ montoDescuento: 10000,
+ naturalezaDescuento: "Descuento por volumen",
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ expect(result.isError).toBeFalsy();
+ const text = getTextContent(result.content as { type: string; text: string }[]);
+ expect(text).toContain("Descuento por volumen");
+ expect(text).toContain("10000");
+ expect(text).toContain("Total: 80000");
+ expect(text).toContain("10000");
+ });
+
it("should return error for missing required fields", async () => {
const result = await client.callTool({
name: "create_invoice",
diff --git a/packages/sdk/src/api/http-client.spec.ts b/packages/sdk/src/api/http-client.spec.ts
index 3166d70..b069cd2 100644
--- a/packages/sdk/src/api/http-client.spec.ts
+++ b/packages/sdk/src/api/http-client.spec.ts
@@ -211,5 +211,68 @@ describe("HttpClient", () => {
expect(response.headers.get("Location")).toBe("/recepcion/12345");
});
+
+ it("returns text for XML content-type responses", async () => {
+ const xmlBody = "ok";
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: "OK",
+ headers: new Headers({ "Content-Type": "application/xml" }),
+ text: () => Promise.resolve(xmlBody),
+ } as Response);
+ const client = createHttpClient(mockFetch);
+
+ const response = await client.get("/test");
+
+ expect(response.data).toBe(xmlBody);
+ });
+
+ it("returns undefined for empty 204 No Content responses", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 204,
+ statusText: "No Content",
+ headers: new Headers(),
+ text: () => Promise.resolve(""),
+ } as Response);
+ const client = createHttpClient(mockFetch);
+
+ const response = await client.get("/test");
+
+ expect(response.status).toBe(204);
+ expect(response.data).toBeUndefined();
+ });
+
+ it("returns text for plain text non-JSON responses", async () => {
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: "OK",
+ headers: new Headers({ "Content-Type": "text/plain" }),
+ text: () => Promise.resolve("plain text response"),
+ } as Response);
+ const client = createHttpClient(mockFetch);
+
+ const response = await client.get("/test");
+
+ expect(response.data).toBe("plain text response");
+ });
+
+ it("parses JSON from text when content-type is not application/json", async () => {
+ const jsonData = { key: "value", count: 42 };
+ const mockFetch = vi.fn().mockResolvedValue({
+ ok: true,
+ status: 200,
+ statusText: "OK",
+ headers: new Headers({ "Content-Type": "text/plain" }),
+ text: () => Promise.resolve(JSON.stringify(jsonData)),
+ } as Response);
+ const client = createHttpClient(mockFetch);
+
+ const response = await client.get("/test");
+
+ expect(response.data).toEqual(jsonData);
+ });
});
});
diff --git a/packages/sdk/src/documents/shared-xml-helpers.spec.ts b/packages/sdk/src/documents/shared-xml-helpers.spec.ts
new file mode 100644
index 0000000..e50870f
--- /dev/null
+++ b/packages/sdk/src/documents/shared-xml-helpers.spec.ts
@@ -0,0 +1,810 @@
+/**
+ * Tests for shared XML mapping helpers — covers untested optional paths.
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ buildEmisorXml,
+ buildReceptorXml,
+ buildResumenFacturaXml,
+ buildOtrosCargosXml,
+ buildInformacionReferenciaXml,
+ buildOtrosXml,
+ buildStandardDocumentBody,
+} from "./shared-xml-helpers.js";
+import type {
+ Emisor,
+ Receptor,
+ ResumenFactura,
+ InformacionReferencia,
+ OtroContenido,
+ LineaDetalle,
+} from "@hacienda-cr/shared";
+import type { CommonDocumentFields } from "./shared-xml-helpers.js";
+
+// ---------------------------------------------------------------------------
+// Minimal fixture helpers
+// ---------------------------------------------------------------------------
+
+function minimalEmisor(overrides: Partial = {}): Emisor {
+ return {
+ nombre: "Empresa Test S.A.",
+ identificacion: { tipo: "02", numero: "3101234567" },
+ correoElectronico: "test@example.com",
+ ...overrides,
+ };
+}
+
+function minimalReceptor(overrides: Partial = {}): Receptor {
+ return {
+ nombre: "Cliente Test",
+ ...overrides,
+ };
+}
+
+function minimalResumen(overrides: Partial = {}): ResumenFactura {
+ return {
+ totalServGravados: 100000,
+ totalServExentos: 0,
+ totalMercanciasGravadas: 0,
+ totalMercanciasExentas: 0,
+ totalGravado: 100000,
+ totalExento: 0,
+ totalVenta: 100000,
+ totalDescuentos: 0,
+ totalVentaNeta: 100000,
+ totalImpuesto: 13000,
+ totalComprobante: 113000,
+ ...overrides,
+ };
+}
+
+function minimalLineaDetalle(): LineaDetalle {
+ return {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Servicio",
+ precioUnitario: 1000,
+ montoTotal: 1000,
+ subTotal: 1000,
+ montoTotalLinea: 1000,
+ } as LineaDetalle;
+}
+
+function minimalDocumentFields(
+ overrides: Partial = {},
+): CommonDocumentFields {
+ return {
+ clave: "50601012600310123456700100001010000000001199999999",
+ codigoActividad: "620100",
+ numeroConsecutivo: "00100001010000000001",
+ fechaEmision: "2025-07-27T10:30:00-06:00",
+ emisor: minimalEmisor(),
+ condicionVenta: "01",
+ medioPago: ["01"],
+ detalleServicio: [minimalLineaDetalle()],
+ resumenFactura: minimalResumen(),
+ ...overrides,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// buildEmisorXml
+// ---------------------------------------------------------------------------
+
+describe("buildEmisorXml", () => {
+ it("should include Fax when present", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({ fax: { codigoPais: "506", numTelefono: "22224444" } }),
+ );
+ expect(result.Fax).toEqual({ CodigoPais: "506", NumTelefono: "22224444" });
+ });
+
+ it("should not include Fax when absent", () => {
+ const result = buildEmisorXml(minimalEmisor());
+ expect(result).not.toHaveProperty("Fax");
+ });
+
+ it("should include Telefono when present", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({ telefono: { codigoPais: "506", numTelefono: "88887777" } }),
+ );
+ expect(result.Telefono).toEqual({ CodigoPais: "506", NumTelefono: "88887777" });
+ });
+
+ it("should not include Telefono when absent", () => {
+ const result = buildEmisorXml(minimalEmisor());
+ expect(result).not.toHaveProperty("Telefono");
+ });
+
+ it("should include NombreComercial when present", () => {
+ const result = buildEmisorXml(minimalEmisor({ nombreComercial: "MiMarca" }));
+ expect(result.NombreComercial).toBe("MiMarca");
+ });
+
+ it("should not include NombreComercial when absent", () => {
+ const result = buildEmisorXml(minimalEmisor());
+ expect(result).not.toHaveProperty("NombreComercial");
+ });
+
+ it("should include Ubicacion with Barrio when present", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({
+ ubicacion: {
+ provincia: "1",
+ canton: "01",
+ distrito: "01",
+ barrio: "02",
+ },
+ }),
+ );
+ expect(result.Ubicacion).toEqual({
+ Provincia: "1",
+ Canton: "01",
+ Distrito: "01",
+ Barrio: "02",
+ });
+ });
+
+ it("should include Ubicacion with OtrasSenas when present", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({
+ ubicacion: {
+ provincia: "2",
+ canton: "03",
+ distrito: "04",
+ otrasSenas: "Frente al parque",
+ },
+ }),
+ );
+ expect(result.Ubicacion).toEqual({
+ Provincia: "2",
+ Canton: "03",
+ Distrito: "04",
+ OtrasSenas: "Frente al parque",
+ });
+ });
+
+ it("should include Ubicacion with both Barrio and OtrasSenas", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({
+ ubicacion: {
+ provincia: "1",
+ canton: "01",
+ distrito: "01",
+ barrio: "05",
+ otrasSenas: "200m norte de la iglesia",
+ },
+ }),
+ );
+ const ubicacion = result.Ubicacion as Record;
+ expect(ubicacion.Barrio).toBe("05");
+ expect(ubicacion.OtrasSenas).toBe("200m norte de la iglesia");
+ });
+
+ it("should omit Ubicacion entirely when absent", () => {
+ const result = buildEmisorXml(minimalEmisor());
+ expect(result).not.toHaveProperty("Ubicacion");
+ });
+
+ it("should include all optional fields together", () => {
+ const result = buildEmisorXml(
+ minimalEmisor({
+ nombreComercial: "Brand",
+ ubicacion: {
+ provincia: "1",
+ canton: "01",
+ distrito: "01",
+ barrio: "01",
+ otrasSenas: "Calle 1",
+ },
+ telefono: { codigoPais: "506", numTelefono: "11112222" },
+ fax: { codigoPais: "506", numTelefono: "33334444" },
+ }),
+ );
+ expect(result.NombreComercial).toBe("Brand");
+ expect(result).toHaveProperty("Ubicacion");
+ expect(result).toHaveProperty("Telefono");
+ expect(result).toHaveProperty("Fax");
+ expect(result.Nombre).toBe("Empresa Test S.A.");
+ expect(result.CorreoElectronico).toBe("test@example.com");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildReceptorXml
+// ---------------------------------------------------------------------------
+
+describe("buildReceptorXml", () => {
+ it("should include Fax when present", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({ fax: { codigoPais: "506", numTelefono: "55556666" } }),
+ );
+ expect(result.Fax).toEqual({ CodigoPais: "506", NumTelefono: "55556666" });
+ });
+
+ it("should not include Fax when absent", () => {
+ const result = buildReceptorXml(minimalReceptor());
+ expect(result).not.toHaveProperty("Fax");
+ });
+
+ it("should include IdentificacionExtranjero when present", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({ identificacionExtranjero: "US-EIN-99-1234567" }),
+ );
+ expect(result.IdentificacionExtranjero).toBe("US-EIN-99-1234567");
+ });
+
+ it("should not include IdentificacionExtranjero when absent", () => {
+ const result = buildReceptorXml(minimalReceptor());
+ expect(result).not.toHaveProperty("IdentificacionExtranjero");
+ });
+
+ it("should include NombreComercial when present", () => {
+ const result = buildReceptorXml(minimalReceptor({ nombreComercial: "Tienda XYZ" }));
+ expect(result.NombreComercial).toBe("Tienda XYZ");
+ });
+
+ it("should not include NombreComercial when absent", () => {
+ const result = buildReceptorXml(minimalReceptor());
+ expect(result).not.toHaveProperty("NombreComercial");
+ });
+
+ it("should include Ubicacion with Barrio and OtrasSenas", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({
+ ubicacion: {
+ provincia: "3",
+ canton: "02",
+ distrito: "05",
+ barrio: "03",
+ otrasSenas: "Edificio azul",
+ },
+ }),
+ );
+ expect(result.Ubicacion).toEqual({
+ Provincia: "3",
+ Canton: "02",
+ Distrito: "05",
+ Barrio: "03",
+ OtrasSenas: "Edificio azul",
+ });
+ });
+
+ it("should include Ubicacion without optional subfields", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({
+ ubicacion: { provincia: "1", canton: "01", distrito: "01" },
+ }),
+ );
+ const ubicacion = result.Ubicacion as Record;
+ expect(ubicacion).not.toHaveProperty("Barrio");
+ expect(ubicacion).not.toHaveProperty("OtrasSenas");
+ });
+
+ it("should include Telefono when present", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({ telefono: { codigoPais: "506", numTelefono: "77778888" } }),
+ );
+ expect(result.Telefono).toEqual({ CodigoPais: "506", NumTelefono: "77778888" });
+ });
+
+ it("should include CorreoElectronico when present", () => {
+ const result = buildReceptorXml(minimalReceptor({ correoElectronico: "cliente@test.com" }));
+ expect(result.CorreoElectronico).toBe("cliente@test.com");
+ });
+
+ it("should not include CorreoElectronico when absent", () => {
+ const result = buildReceptorXml(minimalReceptor());
+ expect(result).not.toHaveProperty("CorreoElectronico");
+ });
+
+ it("should include Identificacion when present", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({ identificacion: { tipo: "01", numero: "101230456" } }),
+ );
+ expect(result.Identificacion).toEqual({ Tipo: "01", Numero: "101230456" });
+ });
+
+ it("should not include Identificacion when absent", () => {
+ const result = buildReceptorXml(minimalReceptor());
+ expect(result).not.toHaveProperty("Identificacion");
+ });
+
+ it("should handle all optional fields together", () => {
+ const result = buildReceptorXml(
+ minimalReceptor({
+ identificacion: { tipo: "02", numero: "3109876543" },
+ identificacionExtranjero: "PA-RUC-555",
+ nombreComercial: "Import Co",
+ ubicacion: {
+ provincia: "7",
+ canton: "01",
+ distrito: "01",
+ barrio: "01",
+ otrasSenas: "Zona franca",
+ },
+ telefono: { codigoPais: "507", numTelefono: "12345678" },
+ fax: { codigoPais: "507", numTelefono: "87654321" },
+ correoElectronico: "import@co.pa",
+ }),
+ );
+ expect(result).toHaveProperty("Identificacion");
+ expect(result).toHaveProperty("IdentificacionExtranjero");
+ expect(result).toHaveProperty("NombreComercial");
+ expect(result).toHaveProperty("Ubicacion");
+ expect(result).toHaveProperty("Telefono");
+ expect(result).toHaveProperty("Fax");
+ expect(result).toHaveProperty("CorreoElectronico");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildResumenFacturaXml
+// ---------------------------------------------------------------------------
+
+describe("buildResumenFacturaXml", () => {
+ it("should include CodigoTipoMoneda when present", () => {
+ const result = buildResumenFacturaXml(
+ minimalResumen({
+ codigoTipoMoneda: { codigoMoneda: "USD" as never, tipoCambio: 530.5 },
+ }),
+ );
+ expect(result.CodigoTipoMoneda).toEqual({ CodigoMoneda: "USD", TipoCambio: 530.5 });
+ });
+
+ it("should not include CodigoTipoMoneda when absent", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("CodigoTipoMoneda");
+ });
+
+ it("should include TotalServExonerado when > 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalServExonerado: 50000 }));
+ expect(result.TotalServExonerado).toBe(50000);
+ });
+
+ it("should not include TotalServExonerado when 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalServExonerado: 0 }));
+ expect(result).not.toHaveProperty("TotalServExonerado");
+ });
+
+ it("should not include TotalServExonerado when undefined", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("TotalServExonerado");
+ });
+
+ it("should include TotalMercExonerada when > 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalMercExonerada: 75000 }));
+ expect(result.TotalMercExonerada).toBe(75000);
+ });
+
+ it("should not include TotalMercExonerada when 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalMercExonerada: 0 }));
+ expect(result).not.toHaveProperty("TotalMercExonerada");
+ });
+
+ it("should not include TotalMercExonerada when undefined", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("TotalMercExonerada");
+ });
+
+ it("should include TotalExonerado when > 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalExonerado: 125000 }));
+ expect(result.TotalExonerado).toBe(125000);
+ });
+
+ it("should not include TotalExonerado when 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalExonerado: 0 }));
+ expect(result).not.toHaveProperty("TotalExonerado");
+ });
+
+ it("should not include TotalExonerado when undefined", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("TotalExonerado");
+ });
+
+ it("should include TotalIVADevuelto when > 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalIVADevuelto: 5000 }));
+ expect(result.TotalIVADevuelto).toBe(5000);
+ });
+
+ it("should not include TotalIVADevuelto when 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalIVADevuelto: 0 }));
+ expect(result).not.toHaveProperty("TotalIVADevuelto");
+ });
+
+ it("should not include TotalIVADevuelto when undefined", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("TotalIVADevuelto");
+ });
+
+ it("should include TotalOtrosCargos when > 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalOtrosCargos: 2500 }));
+ expect(result.TotalOtrosCargos).toBe(2500);
+ });
+
+ it("should not include TotalOtrosCargos when 0", () => {
+ const result = buildResumenFacturaXml(minimalResumen({ totalOtrosCargos: 0 }));
+ expect(result).not.toHaveProperty("TotalOtrosCargos");
+ });
+
+ it("should not include TotalOtrosCargos when undefined", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result).not.toHaveProperty("TotalOtrosCargos");
+ });
+
+ it("should always include required summary totals", () => {
+ const result = buildResumenFacturaXml(minimalResumen());
+ expect(result.TotalServGravados).toBe(100000);
+ expect(result.TotalServExentos).toBe(0);
+ expect(result.TotalMercanciasGravadas).toBe(0);
+ expect(result.TotalMercanciasExentas).toBe(0);
+ expect(result.TotalGravado).toBe(100000);
+ expect(result.TotalExento).toBe(0);
+ expect(result.TotalVenta).toBe(100000);
+ expect(result.TotalDescuentos).toBe(0);
+ expect(result.TotalVentaNeta).toBe(100000);
+ expect(result.TotalImpuesto).toBe(13000);
+ expect(result.TotalComprobante).toBe(113000);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildOtrosCargosXml
+// ---------------------------------------------------------------------------
+
+describe("buildOtrosCargosXml", () => {
+ it("should map required fields", () => {
+ const result = buildOtrosCargosXml([
+ { tipoDocumento: "06", detalle: "Servicio de flete", montoOtroCargo: 5000 },
+ ]);
+ expect(result.OtroCargo).toEqual([
+ { TipoDocumento: "06", Detalle: "Servicio de flete", MontoOtroCargo: 5000 },
+ ]);
+ });
+
+ it("should include NumeroIdentidadTercero when present", () => {
+ const result = buildOtrosCargosXml([
+ {
+ tipoDocumento: "06",
+ numeroIdentidadTercero: "3101999999",
+ detalle: "Cargo tercero",
+ montoOtroCargo: 3000,
+ },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo.NumeroIdentidadTercero).toBe("3101999999");
+ });
+
+ it("should not include NumeroIdentidadTercero when absent", () => {
+ const result = buildOtrosCargosXml([
+ { tipoDocumento: "06", detalle: "Cargo simple", montoOtroCargo: 1000 },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo).not.toHaveProperty("NumeroIdentidadTercero");
+ });
+
+ it("should include NombreTercero when present", () => {
+ const result = buildOtrosCargosXml([
+ {
+ tipoDocumento: "01",
+ nombreTercero: "Transportes ABC",
+ detalle: "Flete internacional",
+ montoOtroCargo: 15000,
+ },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo.NombreTercero).toBe("Transportes ABC");
+ });
+
+ it("should not include NombreTercero when absent", () => {
+ const result = buildOtrosCargosXml([
+ { tipoDocumento: "01", detalle: "Cargo", montoOtroCargo: 500 },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo).not.toHaveProperty("NombreTercero");
+ });
+
+ it("should include Porcentaje when present", () => {
+ const result = buildOtrosCargosXml([
+ {
+ tipoDocumento: "02",
+ detalle: "Cargo porcentual",
+ porcentaje: 10,
+ montoOtroCargo: 10000,
+ },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo.Porcentaje).toBe(10);
+ });
+
+ it("should include Porcentaje when zero", () => {
+ const result = buildOtrosCargosXml([
+ {
+ tipoDocumento: "02",
+ detalle: "Cargo con porcentaje cero",
+ porcentaje: 0,
+ montoOtroCargo: 0,
+ },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo.Porcentaje).toBe(0);
+ });
+
+ it("should not include Porcentaje when undefined", () => {
+ const result = buildOtrosCargosXml([
+ { tipoDocumento: "02", detalle: "Sin porcentaje", montoOtroCargo: 2000 },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo).not.toHaveProperty("Porcentaje");
+ });
+
+ it("should include all optional fields together", () => {
+ const result = buildOtrosCargosXml([
+ {
+ tipoDocumento: "07",
+ numeroIdentidadTercero: "105550123",
+ nombreTercero: "Juan Perez",
+ detalle: "Comision completa",
+ porcentaje: 5,
+ montoOtroCargo: 2500,
+ },
+ ]);
+ const cargo = (result.OtroCargo as Record[])[0];
+ expect(cargo.NumeroIdentidadTercero).toBe("105550123");
+ expect(cargo.NombreTercero).toBe("Juan Perez");
+ expect(cargo.Porcentaje).toBe(5);
+ expect(cargo.Detalle).toBe("Comision completa");
+ expect(cargo.MontoOtroCargo).toBe(2500);
+ });
+
+ it("should handle multiple cargos", () => {
+ const result = buildOtrosCargosXml([
+ { tipoDocumento: "01", detalle: "Cargo 1", montoOtroCargo: 1000 },
+ { tipoDocumento: "02", detalle: "Cargo 2", montoOtroCargo: 2000, porcentaje: 5 },
+ ]);
+ const cargos = result.OtroCargo as Record[];
+ expect(cargos).toHaveLength(2);
+ expect(cargos[0].TipoDocumento).toBe("01");
+ expect(cargos[1].Porcentaje).toBe(5);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildInformacionReferenciaXml
+// ---------------------------------------------------------------------------
+
+describe("buildInformacionReferenciaXml", () => {
+ it("should map a single reference", () => {
+ const refs: InformacionReferencia[] = [
+ {
+ tipoDoc: "01" as never,
+ numero: "50601012500310123456700100001010000000099199999999",
+ fechaEmision: "2025-06-15T08:00:00-06:00",
+ codigo: "01" as never,
+ razon: "Correccion del monto",
+ },
+ ];
+ const result = buildInformacionReferenciaXml(refs);
+ expect(result).toEqual([
+ {
+ TipoDoc: "01",
+ Numero: "50601012500310123456700100001010000000099199999999",
+ FechaEmision: "2025-06-15T08:00:00-06:00",
+ Codigo: "01",
+ Razon: "Correccion del monto",
+ },
+ ]);
+ });
+
+ it("should map multiple references", () => {
+ const refs: InformacionReferencia[] = [
+ {
+ tipoDoc: "01" as never,
+ numero: "clave-001",
+ fechaEmision: "2025-01-01T00:00:00-06:00",
+ codigo: "01" as never,
+ razon: "Razon 1",
+ },
+ {
+ tipoDoc: "03" as never,
+ numero: "clave-002",
+ fechaEmision: "2025-02-01T00:00:00-06:00",
+ codigo: "02" as never,
+ razon: "Razon 2",
+ },
+ ];
+ const result = buildInformacionReferenciaXml(refs);
+ expect(result).toHaveLength(2);
+ expect(result[0].TipoDoc).toBe("01");
+ expect(result[1].TipoDoc).toBe("03");
+ expect(result[1].Codigo).toBe("02");
+ });
+
+ it("should preserve all fields exactly", () => {
+ const refs: InformacionReferencia[] = [
+ {
+ tipoDoc: "14" as never,
+ numero: "ABC-123",
+ fechaEmision: "2025-12-31T23:59:59-06:00",
+ codigo: "99" as never,
+ razon: "Otro motivo de referencia",
+ },
+ ];
+ const result = buildInformacionReferenciaXml(refs);
+ expect(result[0]).toEqual({
+ TipoDoc: "14",
+ Numero: "ABC-123",
+ FechaEmision: "2025-12-31T23:59:59-06:00",
+ Codigo: "99",
+ Razon: "Otro motivo de referencia",
+ });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildOtrosXml
+// ---------------------------------------------------------------------------
+
+describe("buildOtrosXml", () => {
+ it("should map a single OtroContenido", () => {
+ const otros: OtroContenido[] = [{ contenido: "valor" }];
+ const result = buildOtrosXml(otros);
+ expect(result).toEqual({
+ OtroContenido: [{ "#text": "valor" }],
+ });
+ });
+
+ it("should map multiple OtroContenido entries", () => {
+ const otros: OtroContenido[] = [
+ { contenido: "contenido-1" },
+ { contenido: "contenido-2" },
+ { contenido: "contenido-3" },
+ ];
+ const result = buildOtrosXml(otros);
+ const items = result.OtroContenido as Record[];
+ expect(items).toHaveLength(3);
+ expect(items[0]["#text"]).toBe("contenido-1");
+ expect(items[2]["#text"]).toBe("contenido-3");
+ });
+});
+
+// ---------------------------------------------------------------------------
+// buildStandardDocumentBody
+// ---------------------------------------------------------------------------
+
+describe("buildStandardDocumentBody", () => {
+ it("should include required fields", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result.Clave).toBe("50601012600310123456700100001010000000001199999999");
+ expect(result.CodigoActividad).toBe("620100");
+ expect(result.NumeroConsecutivo).toBe("00100001010000000001");
+ expect(result.FechaEmision).toBe("2025-07-27T10:30:00-06:00");
+ expect(result).toHaveProperty("Emisor");
+ expect(result.CondicionVenta).toBe("01");
+ expect(result.MedioPago).toEqual(["01"]);
+ expect(result).toHaveProperty("DetalleServicio");
+ expect(result).toHaveProperty("ResumenFactura");
+ });
+
+ it("should not include Receptor when absent", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result).not.toHaveProperty("Receptor");
+ });
+
+ it("should include Receptor when present", () => {
+ const result = buildStandardDocumentBody(
+ minimalDocumentFields({
+ receptor: minimalReceptor({ identificacion: { tipo: "01", numero: "101110222" } }),
+ }),
+ );
+ expect(result).toHaveProperty("Receptor");
+ const receptor = result.Receptor as Record;
+ expect(receptor.Nombre).toBe("Cliente Test");
+ });
+
+ it("should not include PlazoCredito when absent", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result).not.toHaveProperty("PlazoCredito");
+ });
+
+ it("should include PlazoCredito when present", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields({ plazoCredito: "60" }));
+ expect(result.PlazoCredito).toBe("60");
+ });
+
+ it("should not include OtrosCargos when absent", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result).not.toHaveProperty("OtrosCargos");
+ });
+
+ it("should not include OtrosCargos when empty array", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields({ otrosCargos: [] }));
+ expect(result).not.toHaveProperty("OtrosCargos");
+ });
+
+ it("should include OtrosCargos when present", () => {
+ const result = buildStandardDocumentBody(
+ minimalDocumentFields({
+ otrosCargos: [{ tipoDocumento: "06", detalle: "Flete", montoOtroCargo: 5000 }],
+ }),
+ );
+ expect(result).toHaveProperty("OtrosCargos");
+ });
+
+ it("should not include InformacionReferencia when absent", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result).not.toHaveProperty("InformacionReferencia");
+ });
+
+ it("should not include InformacionReferencia when empty array", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields({ informacionReferencia: [] }));
+ expect(result).not.toHaveProperty("InformacionReferencia");
+ });
+
+ it("should include InformacionReferencia when present", () => {
+ const result = buildStandardDocumentBody(
+ minimalDocumentFields({
+ informacionReferencia: [
+ {
+ tipoDoc: "01" as never,
+ numero: "clave-ref",
+ fechaEmision: "2025-01-01T00:00:00-06:00",
+ codigo: "01" as never,
+ razon: "Correccion",
+ },
+ ],
+ }),
+ );
+ expect(result).toHaveProperty("InformacionReferencia");
+ const refs = result.InformacionReferencia as Record[];
+ expect(refs[0].TipoDoc).toBe("01");
+ });
+
+ it("should not include Otros when absent", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields());
+ expect(result).not.toHaveProperty("Otros");
+ });
+
+ it("should not include Otros when empty array", () => {
+ const result = buildStandardDocumentBody(minimalDocumentFields({ otros: [] }));
+ expect(result).not.toHaveProperty("Otros");
+ });
+
+ it("should include Otros when present", () => {
+ const result = buildStandardDocumentBody(
+ minimalDocumentFields({ otros: [{ contenido: "data" }] }),
+ );
+ expect(result).toHaveProperty("Otros");
+ const otros = result.Otros as Record;
+ const items = otros.OtroContenido as Record[];
+ expect(items[0]["#text"]).toBe("data");
+ });
+
+ it("should include all optional fields together", () => {
+ const result = buildStandardDocumentBody(
+ minimalDocumentFields({
+ receptor: minimalReceptor(),
+ plazoCredito: "90",
+ otrosCargos: [{ tipoDocumento: "01", detalle: "Cargo", montoOtroCargo: 100 }],
+ informacionReferencia: [
+ {
+ tipoDoc: "01" as never,
+ numero: "ref-1",
+ fechaEmision: "2025-01-01T00:00:00-06:00",
+ codigo: "04" as never,
+ razon: "Referencia",
+ },
+ ],
+ otros: [{ contenido: "extra" }],
+ }),
+ );
+ expect(result).toHaveProperty("Receptor");
+ expect(result.PlazoCredito).toBe("90");
+ expect(result).toHaveProperty("OtrosCargos");
+ expect(result).toHaveProperty("InformacionReferencia");
+ expect(result).toHaveProperty("Otros");
+ });
+});
diff --git a/packages/sdk/src/signing/p12-loader.spec.ts b/packages/sdk/src/signing/p12-loader.spec.ts
new file mode 100644
index 0000000..b32fa41
--- /dev/null
+++ b/packages/sdk/src/signing/p12-loader.spec.ts
@@ -0,0 +1,81 @@
+/**
+ * Tests for PKCS#12 (.p12) loader error paths.
+ */
+
+import { describe, it, expect, beforeAll } from "vitest";
+import { execSync } from "node:child_process";
+import { readFileSync, mkdtempSync, rmSync } from "node:fs";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+
+import { loadP12 } from "./p12-loader.js";
+import { SigningError } from "../errors.js";
+
+// ---------------------------------------------------------------------------
+// Fixtures
+// ---------------------------------------------------------------------------
+
+const TEST_P12_PIN = "test1234";
+
+let p12Buffer: Buffer | undefined;
+
+beforeAll(() => {
+ const tempDir = mkdtempSync(join(tmpdir(), "hacienda-p12-test-"));
+ const keyPath = join(tempDir, "test.key");
+ const certPath = join(tempDir, "test.crt");
+ const p12Path = join(tempDir, "test.p12");
+
+ try {
+ execSync(`openssl genrsa -out "${keyPath}" 2048`, { stdio: "pipe" });
+ execSync(
+ `openssl req -new -x509 -key "${keyPath}" -out "${certPath}" -days 365 -subj "/CN=Test/O=TestOrg/C=CR"`,
+ { stdio: "pipe" },
+ );
+ try {
+ execSync(
+ `openssl pkcs12 -export -out "${p12Path}" -inkey "${keyPath}" -in "${certPath}" -passout pass:${TEST_P12_PIN} -legacy`,
+ { stdio: "pipe" },
+ );
+ } catch {
+ execSync(
+ `openssl pkcs12 -export -out "${p12Path}" -inkey "${keyPath}" -in "${certPath}" -passout pass:${TEST_P12_PIN}`,
+ { stdio: "pipe" },
+ );
+ }
+ p12Buffer = readFileSync(p12Path);
+ } catch {
+ console.warn("openssl not available, wrong-PIN test will be skipped");
+ }
+
+ return () => {
+ try {
+ rmSync(tempDir, { recursive: true, force: true });
+ } catch {
+ // Ignore cleanup errors
+ }
+ };
+});
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe("loadP12 – error paths", () => {
+ it("throws SigningError for invalid/corrupt buffer", async () => {
+ const corruptBuffer = Buffer.from("not a valid p12 file contents");
+
+ await expect(loadP12(corruptBuffer, "1234")).rejects.toThrow(SigningError);
+ });
+
+ it("throws SigningError for wrong PIN", async () => {
+ if (!p12Buffer) return;
+
+ await expect(loadP12(p12Buffer, "wrong-pin")).rejects.toThrow(SigningError);
+ });
+
+ it("throws SigningError for empty buffer", async () => {
+ const emptyBuffer = Buffer.alloc(0);
+
+ await expect(loadP12(emptyBuffer, "any-pin")).rejects.toThrow(SigningError);
+ });
+});
diff --git a/packages/sdk/src/tax/calculator.spec.ts b/packages/sdk/src/tax/calculator.spec.ts
index 10c5eaa..656a481 100644
--- a/packages/sdk/src/tax/calculator.spec.ts
+++ b/packages/sdk/src/tax/calculator.spec.ts
@@ -608,3 +608,252 @@ describe("calculateInvoiceSummary", () => {
expect(summary.totalComprobante).toBe(124240);
});
});
+
+// ---------------------------------------------------------------------------
+// round5 – IEEE 754 edge cases
+// ---------------------------------------------------------------------------
+
+describe("round5 – IEEE 754 edge cases", () => {
+ it("should round 0.123456789 to 0.12346", () => {
+ expect(round5(0.123456789)).toBe(0.12346);
+ });
+
+ it("should round 99999.999999 to 100000 (precision test)", () => {
+ expect(round5(99999.999999)).toBe(100000);
+ });
+
+ it("should round -0.123456789 to -0.12346", () => {
+ expect(round5(-0.123456789)).toBe(-0.12346);
+ });
+
+ it("should preserve precision for large number 999999999.12345", () => {
+ expect(round5(999999999.12345)).toBe(999999999.12345);
+ });
+
+ it("should round 0.000001 to 0 (below half-unit at 5th decimal)", () => {
+ expect(round5(0.000001)).toBe(0);
+ });
+
+ it("should round 0.000005 to 0.00001 (rounds half up at boundary)", () => {
+ expect(round5(0.000005)).toBe(0.00001);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// calculateLineItemTotals – edge cases
+// ---------------------------------------------------------------------------
+
+describe("calculateLineItemTotals – edge cases", () => {
+ it("should handle micro quantity (cantidad=0.001)", () => {
+ const item: LineItemInput = {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 0.001,
+ unidadMedida: "kg",
+ detalle: "Micro quantity",
+ precioUnitario: 10000,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ };
+ const result = calculateLineItemTotals(item);
+ // montoTotal = 0.001 * 10000 = 10
+ expect(result.montoTotal).toBe(10);
+ // tax = 10 * 0.13 = 1.3
+ expect(result.impuestoNeto).toBe(1.3);
+ expect(result.montoTotalLinea).toBe(11.3);
+ });
+
+ it("should handle free item (precioUnitario=0)", () => {
+ const item: LineItemInput = {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 5,
+ unidadMedida: "Unid",
+ detalle: "Free promotional item",
+ precioUnitario: 0,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ };
+ const result = calculateLineItemTotals(item);
+ expect(result.montoTotal).toBe(0);
+ expect(result.subTotal).toBe(0);
+ expect(result.impuestoNeto).toBe(0);
+ expect(result.montoTotalLinea).toBe(0);
+ });
+
+ it("should handle very large amounts (999999.99 * 100)", () => {
+ const item: LineItemInput = {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 100,
+ unidadMedida: "Unid",
+ detalle: "Expensive bulk",
+ precioUnitario: 999999.99,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ };
+ const result = calculateLineItemTotals(item);
+ // montoTotal = 100 * 999999.99 = 99999999
+ expect(result.montoTotal).toBe(99999999);
+ const tax = getFirstTax(result);
+ // tax = 99999999 * 0.13 = 12999999.87
+ expect(tax.monto).toBe(12999999.87);
+ expect(result.montoTotalLinea).toBe(112999998.87);
+ });
+
+ it("should handle multiple taxes on same line (IVA + selectivo consumo)", () => {
+ const item: LineItemInput = {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Unid",
+ detalle: "Multi-tax item",
+ precioUnitario: 10000,
+ impuesto: [
+ { codigo: "01", codigoTarifa: "08", tarifa: 13 },
+ { codigo: "02", tarifa: 10 },
+ ],
+ };
+ const result = calculateLineItemTotals(item);
+ expect(result.impuesto).toBeDefined();
+ expect(result.impuesto).toHaveLength(2);
+ // IVA: 10000 * 0.13 = 1300
+ expect(result.impuesto?.[0]?.monto).toBe(1300);
+ // Selectivo: 10000 * 0.10 = 1000
+ expect(result.impuesto?.[1]?.monto).toBe(1000);
+ // impuestoNeto only counts IVA (code "01"), not selectivo consumo ("02")
+ expect(result.impuestoNeto).toBe(1300);
+ // montoTotalLinea = subTotal + impuestoNeto (only IVA net)
+ expect(result.montoTotalLinea).toBe(11300);
+ });
+
+ it("should handle 100% exoneration", () => {
+ const item: LineItemInput = {
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Fully exonerated",
+ precioUnitario: 50000,
+ impuesto: [
+ {
+ codigo: "01",
+ codigoTarifa: "08",
+ tarifa: 13,
+ exoneracion: {
+ tipoDocumento: "03",
+ numeroDocumento: "EX-100-2025",
+ nombreInstitucion: "Ministerio",
+ fechaEmision: "2025-06-01T00:00:00-06:00",
+ porcentajeExoneracion: 100,
+ },
+ },
+ ],
+ };
+ const result = calculateLineItemTotals(item);
+ const tax = getFirstTax(result);
+ expect(tax.monto).toBe(6500); // full tax still recorded
+ expect(tax.exoneracion?.montoExoneracion).toBe(6500);
+ expect(result.impuestoNeto).toBe(0);
+ expect(result.montoTotalLinea).toBe(50000);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// calculateInvoiceSummary – edge cases
+// ---------------------------------------------------------------------------
+
+describe("calculateInvoiceSummary – edge cases", () => {
+ it("should handle invoice summary with all zeros", () => {
+ const items = [
+ calculateLineItemTotals({
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Unid",
+ detalle: "Free item",
+ precioUnitario: 0,
+ }),
+ ];
+ const summary = calculateInvoiceSummary(items);
+ expect(summary.totalServGravados).toBe(0);
+ expect(summary.totalServExentos).toBe(0);
+ expect(summary.totalServExonerado).toBe(0);
+ expect(summary.totalMercanciasGravadas).toBe(0);
+ expect(summary.totalMercanciasExentas).toBe(0);
+ expect(summary.totalMercExonerada).toBe(0);
+ expect(summary.totalGravado).toBe(0);
+ expect(summary.totalExento).toBe(0);
+ expect(summary.totalExonerado).toBe(0);
+ expect(summary.totalVenta).toBe(0);
+ expect(summary.totalDescuentos).toBe(0);
+ expect(summary.totalVentaNeta).toBe(0);
+ expect(summary.totalImpuesto).toBe(0);
+ expect(summary.totalComprobante).toBe(0);
+ });
+
+ it("should handle summary with only services", () => {
+ const items = [
+ calculateLineItemTotals({
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Service A",
+ precioUnitario: 20000,
+ esServicio: true,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ }),
+ calculateLineItemTotals({
+ numeroLinea: 2,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Sp",
+ detalle: "Service B",
+ precioUnitario: 30000,
+ esServicio: true,
+ }),
+ ];
+ const summary = calculateInvoiceSummary(items);
+ expect(summary.totalServGravados).toBe(20000);
+ expect(summary.totalServExentos).toBe(30000);
+ expect(summary.totalMercanciasGravadas).toBe(0);
+ expect(summary.totalMercanciasExentas).toBe(0);
+ expect(summary.totalGravado).toBe(20000);
+ expect(summary.totalExento).toBe(30000);
+ expect(summary.totalVenta).toBe(50000);
+ expect(summary.totalImpuesto).toBe(2600);
+ expect(summary.totalComprobante).toBe(52600);
+ });
+
+ it("should handle summary with only merchandise", () => {
+ const items = [
+ calculateLineItemTotals({
+ numeroLinea: 1,
+ codigoCabys: "4321000000000",
+ cantidad: 3,
+ unidadMedida: "Unid",
+ detalle: "Product A",
+ precioUnitario: 5000,
+ esServicio: false,
+ impuesto: [{ codigo: "01", codigoTarifa: "08", tarifa: 13 }],
+ }),
+ calculateLineItemTotals({
+ numeroLinea: 2,
+ codigoCabys: "4321000000000",
+ cantidad: 1,
+ unidadMedida: "Unid",
+ detalle: "Product B",
+ precioUnitario: 8000,
+ esServicio: false,
+ }),
+ ];
+ const summary = calculateInvoiceSummary(items);
+ expect(summary.totalServGravados).toBe(0);
+ expect(summary.totalServExentos).toBe(0);
+ expect(summary.totalMercanciasGravadas).toBe(15000);
+ expect(summary.totalMercanciasExentas).toBe(8000);
+ expect(summary.totalGravado).toBe(15000);
+ expect(summary.totalExento).toBe(8000);
+ expect(summary.totalVenta).toBe(23000);
+ expect(summary.totalImpuesto).toBe(1950);
+ expect(summary.totalComprobante).toBe(24950);
+ });
+});