@stafyniaksacha/facturx
Version:
Factur-X and Order-X generation library for European e-invoicing standard
211 lines (204 loc) • 6.85 kB
JavaScript
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 };