UNPKG

@stafyniaksacha/facturx

Version:

Factur-X and Order-X generation library for European e-invoicing standard

211 lines (204 loc) 6.85 kB
import { XMLDocument, parseXmlAsync } from 'libxmljs'; import { PDFDocument } from 'pdf-lib'; import { parse } from 'date-fns'; import { readFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; async function resolveXml(xml, options = { encoding: "utf8" }) { if (xml instanceof XMLDocument) { return xml; } return await parseXmlAsync(xml, options); } async function resolvePdf(pdf, options = {}) { if (pdf instanceof PDFDocument) { return pdf; } return await PDFDocument.load(pdf, options); } const FACTURX_FILENAME = "factur-x.xml"; const ZUGFERD_FILENAMES = ["zugferd-invoice.xml", "ZUGFeRD-invoice.xml"]; const ORDERX_FILENAME = "order-x.xml"; const FACTURX_SCHEMA = { "basic": "./xsd/facturx/basic/FACTUR-X_BASIC.xsd", "basic-wl": "./xsd/facturx/basic-wl/FACTUR-X_BASIC-WL.xsd", "en16931": "./xsd/facturx/en16931/FACTUR-X_EN16931.xsd", "extended": "./xsd/facturx/extended/FACTUR-X_EXTENDED.xsd", "minimum": "./xsd/facturx/minimum/FACTUR-X_MINIMUM.xsd" }; const FACTURX_CONFORMANCE_LEVEL = { "basic": "BASIC", "basic-wl": "BASIC WL", "en16931": "EN 16931", "extended": "EXTENDED", "minimum": "MINIMUM" }; const ORDERX_SCHEMA = { basic: "./xsd/orderx/basic/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", comfort: "./xsd/orderx/comfort/SCRDMCCBDACIOMessageStructure_100pD20B.xsd", extended: "./xsd/orderx/extended/SCRDMCCBDACIOMessageStructure_100pD20B.xsd" }; const DOC_TYPE = { // eslint-disable-next-line style/quote-props "220": "Order", // eslint-disable-next-line style/quote-props "230": "Order Change", // eslint-disable-next-line style/quote-props "231": "Order Response", // eslint-disable-next-line style/quote-props "380": "Invoice", // eslint-disable-next-line style/quote-props "381": "Refund" }; function extractNamespaces(fileDoc) { const str = fileDoc.toString(); const namespaces = {}; let match; const xmlnsRe = /xmlns:([^=]+)="([^"]+)"/g; while ((match = xmlnsRe.exec(str)) !== null) { namespaces[match[1]] = match[2]; } return namespaces; } function getLevel(xmlDoc) { const namespaces = extractNamespaces(xmlDoc); let doc_id_xpath = xmlDoc.find([ "//rsm:ExchangedDocumentContext", "/ram:GuidelineSpecifiedDocumentContextParameter", "/ram:ID" ].join(""), namespaces); if (!doc_id_xpath.length) { doc_id_xpath = xmlDoc.find([ "//rsm:SpecifiedExchangedDocumentContext", "/ram:GuidelineSpecifiedDocumentContextParameter", "/ram:ID" ].join(""), namespaces); } if (!doc_id_xpath.length) { throw new Error("No ID found in the document"); } const xpathNode = doc_id_xpath[0]; if (!("text" in xpathNode)) { throw new Error("No text found in the ID node"); } const doc_id = xpathNode?.text()?.split(":"); let level = doc_id[doc_id.length - 1]; const possibleValues = /* @__PURE__ */ new Set([...Object.keys(FACTURX_SCHEMA), ...Object.keys(ORDERX_SCHEMA)]); if (!possibleValues.has(level)) { level = doc_id[doc_id.length - 2]; } if (!possibleValues.has(level)) { throw new Error(`Unknown level: "${level}"`); } return level; } function getFlavor(fileDoc) { const tag = fileDoc.root()?.name(); switch (tag) { case "SCRDMCCBDACIOMessageStructure": return "orderx"; case "CrossIndustryInvoice": return "facturx"; case "CrossIndustryDocument": return "zugferd"; } throw new Error(`XML not recognized as Factur-X, Order-X or ZUGFeRD`); } async function extractBaseInfo(xml) { const xmlDoc = await resolveXml(xml); const namespaces = extractNamespaces(xmlDoc); const dateEl = findXPath(xmlDoc, "//rsm:ExchangedDocument/ram:IssueDateTime/udt:DateTimeString", namespaces); const dateStr = dateEl.text(); const dateFormat = dateEl.getAttribute("format")?.value() || "102"; const formatMap = { // eslint-disable-next-line style/quote-props "102": "yyyyMMdd", // eslint-disable-next-line style/quote-props "203": "yyyyMMddHHmm" }; const date = dateStr ? parse(dateStr, formatMap[dateFormat], /* @__PURE__ */ new Date()) : /* @__PURE__ */ new Date(); const numberEl = findXPath(xmlDoc, "//rsm:ExchangedDocument/ram:ID", namespaces); const number = numberEl.text() || ""; const sellerEl = findXPath(xmlDoc, "//ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name", namespaces); const seller = sellerEl.text() || ""; const buyerEl = findXPath(xmlDoc, "//ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name", namespaces); const buyer = buyerEl.text() || ""; const docTypeEl = findXPath(xmlDoc, "//rsm:ExchangedDocument/ram:TypeCode", namespaces); const docType = docTypeEl.text() || ""; return { seller, buyer, number, date, docType }; } function findXPath(fileDoc, xpath, namespaces) { const xpathNode = fileDoc.find(xpath, namespaces); if (!xpathNode.length) { throw new Error(`No ${xpath} found in the document`); } return xpathNode[0]; } const _cache = {}; async function getXsd(flavor, level, cache = true) { if (cache && flavor in _cache && level in _cache[flavor]) { return _cache[flavor][level]; } switch (flavor) { case "facturx": { const schema = await getFacturxXsd(level); if (cache) { _cache[flavor] ||= {}; _cache[flavor][level] = schema; } return schema; } case "orderx": { const schema = await getOrderxXsd(level); if (cache) { _cache[flavor] ||= {}; _cache[flavor][level] = schema; } return schema; } default: throw new Error(`Unknown schema flavor: "${flavor}"`); } } async function getFacturxXsd(level) { if (!level || !(level in FACTURX_SCHEMA)) { throw new Error(`Unknown Factur-X level: "${level}"`); } const url = resolve(join(import.meta.dirname, FACTURX_SCHEMA[level])); const buffer = await readFile(url); return await resolveXml(buffer, { url }); } async function getOrderxXsd(level) { if (!level || !(level in ORDERX_SCHEMA)) { throw new Error(`Unknown Order-X level: "${level}"`); } const url = resolve(join(import.meta.dirname, ORDERX_SCHEMA[level])); const buffer = await readFile(url); return await resolveXml(buffer, { url }); } async function check(options) { const xml = await resolveXml(options.xml); const flavor = options.flavor || getFlavor(xml); const level = options.level || getLevel(xml); const xsd = await getXsd(flavor, level); const valid = xml.validate(xsd); const errors = xml.validationErrors; return { valid, errors, flavor, level }; } export { DOC_TYPE as D, FACTURX_FILENAME as F, ORDERX_FILENAME as O, ZUGFERD_FILENAMES as Z, resolveXml as a, getLevel as b, check as c, FACTURX_CONFORMANCE_LEVEL as d, extractBaseInfo as e, getFlavor as g, resolvePdf as r };