verifactu-node-lib
Version:
Node.js library for generating VeriFacTu invoices compatible with AEAT
483 lines (482 loc) • 21.6 kB
JavaScript
;
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
};
}