UNPKG

verifactu-node-lib

Version:

Node.js library for generating VeriFacTu invoices compatible with AEAT

483 lines (482 loc) 21.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.createInvoice = createInvoice; exports.cancelInvoice = cancelInvoice; const xmldom_1 = require("@xmldom/xmldom"); const crypto = __importStar(require("crypto")); const qrcode = __importStar(require("qrcode")); const utils_1 = require("./utils"); const NS1 = `xmlns:sum="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"`; const NS2 = `xmlns="https://www2.agenciatributaria.gob.es/static_files/common/internet/dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd"`; const VERIFACTU_CANCEL_XML_BASE = ` <sum:RegistroFactura ${NS1} ${NS2}> <RegistroAnulacion> <IDVersion>1.0</IDVersion> <IDFactura> <IDEmisorFacturaAnulada>???</IDEmisorFacturaAnulada> <NumSerieFacturaAnulada>????</NumSerieFacturaAnulada> <FechaExpedicionFacturaAnulada>????</FechaExpedicionFacturaAnulada> </IDFactura> <Encadenamiento> <PrimerRegistro>S</PrimerRegistro> <RegistroAnterior> <IDEmisorFactura>????</IDEmisorFactura> <NumSerieFactura>????</NumSerieFactura> <FechaExpedicionFactura>????</FechaExpedicionFactura> <Huella>????</Huella> </RegistroAnterior> </Encadenamiento> <SistemaInformatico> <NombreRazon>????</NombreRazon> <NIF>????</NIF> <NombreSistemaInformatico>????</NombreSistemaInformatico> <IdSistemaInformatico>????</IdSistemaInformatico> <Version>????</Version> <NumeroInstalacion>????</NumeroInstalacion> <TipoUsoPosibleSoloVerifactu>????</TipoUsoPosibleSoloVerifactu> <TipoUsoPosibleMultiOT>????</TipoUsoPosibleMultiOT> <IndicadorMultiplesOT>????</IndicadorMultiplesOT> </SistemaInformatico> <FechaHoraHusoGenRegistro>????</FechaHoraHusoGenRegistro> <TipoHuella>01</TipoHuella> <Huella>????</Huella> </RegistroAnulacion> </sum:RegistroFactura>`.replace(/>\s+</g, "><").replace(/\s*xmlns/g, " xmlns"); const VERIFACTU_INVOICE_XML_BASE = ` <sum:RegistroFactura ${NS1} ${NS2}> <RegistroAlta> <IDVersion>1.0</IDVersion> <IDFactura> <IDEmisorFactura>????</IDEmisorFactura> <NumSerieFactura>????</NumSerieFactura> <FechaExpedicionFactura>????</FechaExpedicionFactura> </IDFactura> <NombreRazonEmisor>????</NombreRazonEmisor> <Subsanacion>S</Subsanacion> <RechazoPrevio>X</RechazoPrevio> <TipoFactura>F1</TipoFactura> <TipoRectificativa/> <FacturasRectificadas/> <FacturasSustituidas/> <ImporteRectificacion/> <FechaOperacion/> <DescripcionOperacion>????</DescripcionOperacion> <EmitidaPorTerceroODestinatario>????</EmitidaPorTerceroODestinatario> <Tercero> <NombreRazon>????</NombreRazon> <NIF>????</NIF> </Tercero> <Destinatarios/> <Desglose/> <CuotaTotal>????</CuotaTotal> <ImporteTotal>????</ImporteTotal> <RetencionSoportada>????</RetencionSoportada> <Encadenamiento> <PrimerRegistro>S</PrimerRegistro> <RegistroAnterior> <IDEmisorFactura>????</IDEmisorFactura> <NumSerieFactura>????</NumSerieFactura> <FechaExpedicionFactura>????</FechaExpedicionFactura> <Huella>????</Huella> </RegistroAnterior> </Encadenamiento> <SistemaInformatico> <NombreRazon>????</NombreRazon> <NIF>????</NIF> <NombreSistemaInformatico>????</NombreSistemaInformatico> <IdSistemaInformatico>????</IdSistemaInformatico> <Version>????</Version> <NumeroInstalacion>????</NumeroInstalacion> <TipoUsoPosibleSoloVerifactu>????</TipoUsoPosibleSoloVerifactu> <TipoUsoPosibleMultiOT>????</TipoUsoPosibleMultiOT> <IndicadorMultiplesOT>????</IndicadorMultiplesOT> </SistemaInformatico> <FechaHoraHusoGenRegistro>????</FechaHoraHusoGenRegistro> <TipoHuella>01</TipoHuella> <Huella>????</Huella> </RegistroAlta> </sum:RegistroFactura>`.replace(/>\s+</g, "><").replace(/\s*xmlns/g, " xmlns"); // Funciones auxiliares para validaciones básicas function validateInvoice(invoice) { if (!invoice.issuer || !invoice.issuer.irsId || !invoice.issuer.name) { throw new Error("El emisor es obligatorio y debe incluir NIF y nombre"); } if (!invoice.id || !invoice.id.number || !invoice.id.issuedTime) { throw new Error("Los datos de identificación de la factura son obligatorios"); } if (!invoice.vatLines || invoice.vatLines.length === 0) { throw new Error("Debe incluir al menos una línea de IVA"); } if (typeof invoice.total !== "number" || typeof invoice.amount !== "number") { throw new Error("El total y el importe son obligatorios"); } // Validar que no se usen retentionAmount y retentionLines al mismo tiempo // if (invoice.retentionAmount !== undefined && invoice.retentionLines && invoice.retentionLines.length > 0) { // throw new Error("No se puede especificar retentionAmount y retentionLines al mismo tiempo"); // } } function validateCancelInvoice(cancelInvoice) { if (!cancelInvoice.issuer || !cancelInvoice.issuer.irsId || !cancelInvoice.issuer.name) { throw new Error('El emisor es obligatorio y debe incluir NIF y nombre'); } if (!cancelInvoice.id || !cancelInvoice.id.number || !cancelInvoice.id.issuedTime) { throw new Error('Los datos de identificación de la factura a anular son obligatorios'); } } function validateSoftware(software) { if (!software.developerName || !software.developerIrsId || !software.name || !software.id || !software.version || !software.number) { throw new Error('Todos los datos del software son obligatorios'); } } // Funciones para agregar elementos al XML function addRecipientToXml(xml, recipient) { const destinatarios = (0, utils_1.querySelector)(xml, "Destinatarios"); if (!destinatarios) return; if (!recipient) { // Remover el elemento Destinatarios si no hay destinatario (0, utils_1.removeElement)(destinatarios); return; } const isIrs = 'irsId' in recipient; const template = isIrs ? ` <IDDestinatario ${NS2}> <NombreRazon></NombreRazon> <NIF></NIF> </IDDestinatario>` : ` <IDDestinatario ${NS2}> <NombreRazon></NombreRazon> <IDOtro> <CodigoPais></CodigoPais> <IDType></IDType> <ID></ID> </IDOtro> </IDDestinatario>`; const newXml = new xmldom_1.DOMParser().parseFromString(template.replace(/>\s+</g, "><"), "application/xml"); if (isIrs) { const irsRecipient = recipient; (0, utils_1.updateDocument)(newXml, [ ['NombreRazon', irsRecipient.name, utils_1.toStr120], ['NIF', irsRecipient.irsId, utils_1.toNifStr], ]); } else { const otherRecipient = recipient; (0, utils_1.updateDocument)(newXml, [ ['NombreRazon', otherRecipient.name, utils_1.toStr120], ['CodigoPais', otherRecipient.country, utils_1.toString], ['IDType', otherRecipient.idType, utils_1.toString], ['ID', otherRecipient.id, utils_1.toStr20], ]); } if (newXml.documentElement) { destinatarios.appendChild(newXml.documentElement); } } function addVatLinesToXml(xml, vatLines) { const desglose = (0, utils_1.querySelector)(xml, "Desglose"); if (!desglose) return; vatLines.forEach(vatLine => { const template = ` <DetalleDesglose ${NS2}> <Impuesto></Impuesto> <ClaveRegimen></ClaveRegimen> <CalificacionOperacion></CalificacionOperacion> <OperacionExenta></OperacionExenta> <TipoImpositivo></TipoImpositivo> <BaseImponible></BaseImponible> <TipoImpositivo2></TipoImpositivo2> <CuotaRecargoEquivalencia></CuotaRecargoEquivalencia> <TipoRecargoEquivalencia></TipoRecargoEquivalencia> <CuotaImpuesto></CuotaImpuesto> </DetalleDesglose>`; const newXml = new xmldom_1.DOMParser().parseFromString(template.replace(/>\s+</g, "><"), "application/xml"); (0, utils_1.updateDocument)(newXml, [ ['Impuesto', vatLine.tax || '01', utils_1.toString], ['ClaveRegimen', vatLine.vatKey, utils_1.toString], ['CalificacionOperacion', vatLine.vatOperation, utils_1.toString], ['TipoImpositivo', vatLine.rate, utils_1.round2ToString], ['BaseImponible', vatLine.base, utils_1.round2ToString], ['TipoImpositivo2', vatLine.rate2, utils_1.round2ToString], ['CuotaRecargoEquivalencia', vatLine.amount2, utils_1.round2ToString], ['CuotaImpuesto', vatLine.amount, utils_1.round2ToString], ]); if (newXml.documentElement) { desglose.appendChild(newXml.documentElement); } }); } function addSoftwareToXml(xml, software) { const selectorsToValues = [ ["SistemaInformatico>NombreRazon", software.developerName, utils_1.toStr120], ["SistemaInformatico>NIF", software.developerIrsId, utils_1.toNifStr], ["SistemaInformatico>NombreSistemaInformatico", software.name, utils_1.toStr30], ["SistemaInformatico>IdSistemaInformatico", software.id, utils_1.toStr30], ["SistemaInformatico>Version", software.version, utils_1.toStr50], ["SistemaInformatico>NumeroInstalacion", software.number, utils_1.toStr100], ["SistemaInformatico>TipoUsoPosibleSoloVerifactu", software.useOnlyVerifactu, utils_1.toBooleanString], ["SistemaInformatico>TipoUsoPosibleMultiOT", software.useMulti, utils_1.toBooleanString], ["SistemaInformatico>IndicadorMultiplesOT", software.useCurrentMulti, utils_1.toBooleanString], ]; (0, utils_1.updateDocument)(xml, selectorsToValues); } function addPreviousInvoiceToXml(xml, previousId) { if (previousId) { (0, utils_1.querySelectorAll)(xml, 'PrimerRegistro').forEach(utils_1.removeElement); const selectorsToValues = [ ["RegistroAnterior>IDEmisorFactura", previousId.issuerIrsId, utils_1.toNifStr], ["RegistroAnterior>NumSerieFactura", previousId.number, utils_1.toStr60], ["RegistroAnterior>FechaExpedicionFactura", previousId.issuedTime, utils_1.toDateString], ["RegistroAnterior>Huella", previousId.hash, utils_1.toStr64], ]; (0, utils_1.updateDocument)(xml, selectorsToValues); } else { (0, utils_1.querySelectorAll)(xml, 'RegistroAnterior').forEach(utils_1.removeElement); } } async function generateHash(invoice, dateGenReg, previousHash) { let hashString; if ('vatLines' in invoice) { // Invoice hashString = [ `IDEmisorFactura=${invoice.issuer.irsId}`, `NumSerieFactura=${invoice.id.number}`, `FechaExpedicionFactura=${(0, utils_1.toDateString)(invoice.id.issuedTime)}`, `TipoFactura=${invoice.type}`, `CuotaTotal=${(0, utils_1.round2ToString)(invoice.amount)}`, `ImporteTotal=${(0, utils_1.round2ToString)(invoice.total)}`, `Huella=${previousHash}`, `FechaHoraHusoGenRegistro=${dateGenReg}`, ].join("&"); } else { // CancelInvoice hashString = [ `IDEmisorFacturaAnulada=${invoice.issuer.irsId}`, `NumSerieFacturaAnulada=${invoice.id.number}`, `FechaExpedicionFacturaAnulada=${(0, utils_1.toDateString)(invoice.id.issuedTime)}`, `Huella=${previousHash}`, `FechaHoraHusoGenRegistro=${dateGenReg}`, ].join("&"); } const hash = crypto.createHash('sha256'); hash.update(hashString, 'utf8'); return hash.digest('hex'); } function getText(xml, selector) { const element = (0, utils_1.querySelector)(xml, selector); return element ? element.textContent || '' : ''; } function getVerifactuUrl(xml, isTesting = false) { const prefix = isTesting ? "https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR" : "https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR"; const issuer = getText(xml, "IDFactura>IDEmisorFactura"); const number = getText(xml, "IDFactura>NumSerieFactura"); const date = getText(xml, "IDFactura>FechaExpedicionFactura"); const total = getText(xml, "ImporteTotal"); const hash = getText(xml, "Huella"); const params = new URLSearchParams({ nif: issuer, num: number, fecha: date, importe: total, hash: hash.slice(-6) // últimos 6 caracteres del hash }); return `${prefix}?${params.toString()}`; } function getChainInfo(xml) { function getIssuedDate() { const d = getText(xml, "IDFactura>FechaExpedicionFactura"); return new Date(d.split("-").reverse().join("-")); } return { issuerIrsId: getText(xml, "IDFactura>IDEmisorFactura"), issuedTime: getIssuedDate(), number: getText(xml, "IDFactura>NumSerieFactura"), hash: getText(xml, "Huella").replace(/\s/g, ""), }; } function getCancelChainInfo(xml) { function getIssuedDate() { const d = getText(xml, "IDFactura>FechaExpedicionFacturaAnulada"); return new Date(d.split("-").reverse().join("-")); } return { issuerIrsId: getText(xml, "IDFactura>IDEmisorFacturaAnulada"), issuedTime: getIssuedDate(), number: getText(xml, "IDFactura>NumSerieFacturaAnulada"), hash: getText(xml, "Huella").replace(/\s/g, ""), }; } async function createInvoice(invoice, software, previousId = null, options = {}, isTesting = false) { // Validaciones validateInvoice(invoice); validateSoftware(software); const dateGenReg = new Date().toISOString(); const xml = new xmldom_1.DOMParser().parseFromString(VERIFACTU_INVOICE_XML_BASE, "application/xml"); // Datos básicos de la factura const selectorsToValues = [ ["IDFactura>IDEmisorFactura", invoice.issuer.irsId, utils_1.toNifStr], ["IDFactura>NumSerieFactura", invoice.id.number, utils_1.toStr60], ["IDFactura>FechaExpedicionFactura", invoice.id.issuedTime, utils_1.toDateString], ["NombreRazonEmisor", invoice.issuer.name, utils_1.toStr120], ["TipoFactura", invoice.type, utils_1.toString], ["DescripcionOperacion", invoice.description?.text || "", utils_1.toStr500], ["EmitidaPorTerceroODestinatario", invoice.issuedBy || "N", utils_1.toString], ["CuotaTotal", invoice.amount, utils_1.round2ToString], ["ImporteTotal", invoice.total, utils_1.round2ToString], ["FechaHoraHusoGenRegistro", dateGenReg, utils_1.toStr30], ]; (0, utils_1.updateDocument)(xml, selectorsToValues); // Remove Tercero section if invoice is not issued by third party if ((invoice.issuedBy || "N") === "N") { const terceroElement = (0, utils_1.querySelector)(xml, "Tercero"); if (terceroElement) { (0, utils_1.removeElement)(terceroElement); } } // Agregar destinatario si existe addRecipientToXml(xml, invoice.recipient); // Agregar líneas de IVA addVatLinesToXml(xml, invoice.vatLines); // Calcular y añadir información de retención IRPF let retentionAmount = 0; // if (invoice.retentionAmount !== undefined) { // retentionAmount = invoice.retentionAmount; // } else if (invoice.retentionLines && invoice.retentionLines.length > 0) { // const retentionLinesFull = completeRetentionLines(invoice.retentionLines); // retentionAmount = computeRetentionTotal(retentionLinesFull); // } // Añadir retención al XML si es mayor que 0 if (retentionAmount > 0) { const retentionElement = (0, utils_1.querySelector)(xml, "RetencionSoportada"); if (retentionElement) { retentionElement.textContent = (0, utils_1.round2ToString)(retentionAmount); } } else { // Eliminar el elemento si no hay retención (0, utils_1.querySelectorAll)(xml, "RetencionSoportada").forEach(utils_1.removeElement); } // Agregar información del software addSoftwareToXml(xml, software); // Agregar información de encadenamiento addPreviousInvoiceToXml(xml, previousId); // Generar hash const previousHash = previousId?.hash || ""; const hash = await generateHash(invoice, dateGenReg, previousHash); // Find the final Huella element (not the one inside RegistroAnterior) const huellaElements = (0, utils_1.querySelectorAll)(xml, "Huella"); const hashElement = huellaElements.find(el => { // Find the Huella that is NOT inside RegistroAnterior let parent = el.parentNode; while (parent) { if (parent.tagName === 'RegistroAnterior' || parent.localName === 'RegistroAnterior') { return false; // Skip this one, it's inside RegistroAnterior } parent = parent.parentNode; } return true; // This is the final Huella element }); if (hashElement) { hashElement.textContent = hash; } // Generar QR code const url = getVerifactuUrl(xml, isTesting); const qrcodeData = await qrcode.toDataURL(url); // Obtener información de encadenamiento const chainInfo = getChainInfo(xml); // Convertir XML a string y codificar en base64 const xmlString = new xmldom_1.XMLSerializer().serializeToString(xml); const verifactuXml = Buffer.from(xmlString).toString('base64'); return { qrcode: qrcodeData, chainInfo, verifactuXml }; } async function cancelInvoice(cancelInvoice, software, previousId = null, options = {}) { // Validaciones validateCancelInvoice(cancelInvoice); validateSoftware(software); const dateGenReg = new Date().toISOString(); const xml = new xmldom_1.DOMParser().parseFromString(VERIFACTU_CANCEL_XML_BASE, "application/xml"); // Datos básicos de la anulación const selectorsToValues = [ ["IDFactura>IDEmisorFacturaAnulada", cancelInvoice.issuer.irsId, utils_1.toNifStr], ["IDFactura>NumSerieFacturaAnulada", cancelInvoice.id.number, utils_1.toStr60], ["IDFactura>FechaExpedicionFacturaAnulada", cancelInvoice.id.issuedTime, utils_1.toDateString], ["FechaHoraHusoGenRegistro", dateGenReg, utils_1.toStr30], ]; (0, utils_1.updateDocument)(xml, selectorsToValues); // Agregar información del software addSoftwareToXml(xml, software); // Agregar información de encadenamiento addPreviousInvoiceToXml(xml, previousId); // Generar hash const previousHash = previousId?.hash || ""; const hash = await generateHash(cancelInvoice, dateGenReg, previousHash); // Find the final Huella element (not the one inside RegistroAnterior) const huellaElements = (0, utils_1.querySelectorAll)(xml, "Huella"); const hashElement = huellaElements.find(el => { // Find the Huella that is NOT inside RegistroAnterior let parent = el.parentNode; while (parent) { if (parent.tagName === 'RegistroAnterior' || parent.localName === 'RegistroAnterior') { return false; // Skip this one, it's inside RegistroAnterior } parent = parent.parentNode; } return true; // This is the final Huella element }); if (hashElement) { hashElement.textContent = hash; } // Obtener información de encadenamiento para la anulación const chainInfo = getCancelChainInfo(xml); // Convertir XML a string y codificar en base64 const xmlString = new xmldom_1.XMLSerializer().serializeToString(xml); const verifactuXml = Buffer.from(xmlString).toString('base64'); return { qrcode: null, // Las anulaciones no generan QR chainInfo, verifactuXml }; }