@stafyniaksacha/facturx
Version:
Factur-X and Order-X generation library for European e-invoicing standard
220 lines (211 loc) • 8.78 kB
JavaScript
import { randomBytes } from 'node:crypto';
import { PDFName, PDFHexString, AFRelationship } from 'pdf-lib';
import { p as pkg } from './facturx.DVqxPZ0W.mjs';
import { D as DOC_TYPE, a as resolveXml, g as getFlavor, b as getLevel, c as check, e as extractBaseInfo, r as resolvePdf, Z as ZUGFERD_FILENAMES, O as ORDERX_FILENAME, F as FACTURX_FILENAME, d as FACTURX_CONFORMANCE_LEVEL } from './facturx.BLNAZCZq.mjs';
import { format } from 'date-fns';
function baseInfo2PdfMetadata(info) {
let title = "";
let subject = "";
let doc_x = "";
let author = "";
const doc_type_name = DOC_TYPE[info.docType] || "Invoice";
const date = format(info.date, "yyyyMMdd");
if (info.docType === "231") {
title = `${info.seller}: Order Response on Order ${info.number} from ${info.buyer}`;
subject = `Response of ${info.seller} on ${date} to order ${info.number} from ${info.buyer}`;
doc_x = `Order-X`;
author = info.seller;
} else if (["220", "230"].includes(info.docType)) {
title = `${info.buyer}: ${doc_type_name} ${info.number}`;
subject = `${doc_type_name} ${info.number} issued by ${info.buyer} on ${date}`;
doc_x = `Order-X`;
author = info.buyer;
} else {
title = `${info.seller}: ${doc_type_name} ${info.number}`;
subject = `${doc_type_name} ${info.number} dated ${date} issued by ${info.seller}`;
doc_x = `Factur-X`;
author = info.seller;
}
return {
title,
subject,
author,
keywords: [doc_type_name, doc_x],
date: info.date
};
}
function setPDFA3BMetadata({ date, documentId, title, subject, author, producer, creator, filename, conformanceLevel }, pdf) {
const metadataXML = `
<?xpacket begin="" id="${documentId}"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
<pdfaid:part>3</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:creator>
<rdf:Seq>
<rdf:li>${author}</rdf:li>
</rdf:Seq>
</dc:creator>
<dc:title>
<rdf:Alt>
<rdf:li xml:lang="x-default">${title}</rdf:li>
</rdf:Alt>
</dc:title>
<dc:description>
<rdf:Alt>
<rdf:li xml:lang="x-default">${subject}</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<pdf:Producer>${producer}</pdf:Producer>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
<xmp:CreatorTool>${creator}</xmp:CreatorTool>
<xmp:CreateDate>${formatDateMetadata({ date })}</xmp:CreateDate>
<xmp:ModifyDate>${formatDateMetadata({ date })}</xmp:ModifyDate>
<xmp:MetadataDate>${formatDateMetadata({ date })}</xmp:MetadataDate>
</rdf:Description>
<rdf:Description xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/" xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#" xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#" rdf:about="">
<pdfaExtension:schemas>
<rdf:Bag>
<rdf:li rdf:parseType="Resource">
<pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
<pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
<pdfaSchema:prefix>fx</pdfaSchema:prefix>
<pdfaSchema:property>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
</rdf:li>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>DocumentType</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>INVOICE</pdfaProperty:description>
</rdf:li>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>Version</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>The actual version of the Factur-X XML schema</pdfaProperty:description>
</rdf:li>
<rdf:li rdf:parseType="Resource">
<pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
<pdfaProperty:valueType>Text</pdfaProperty:valueType>
<pdfaProperty:category>external</pdfaProperty:category>
<pdfaProperty:description>The conformance level of the embedded Factur-X data</pdfaProperty:description>
</rdf:li>
</rdf:Seq>
</pdfaSchema:property>
</rdf:li>
</rdf:Bag>
</pdfaExtension:schemas>
</rdf:Description>
<rdf:Description xmlns:fx="urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#" rdf:about="">
<fx:DocumentType>INVOICE</fx:DocumentType>
<fx:DocumentFileName>${filename}</fx:DocumentFileName>
<fx:Version>1.0</fx:Version>
<fx:ConformanceLevel>${conformanceLevel}</fx:ConformanceLevel>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>
`.trim();
const metadataStream = pdf.context.stream(metadataXML, {
Type: "Metadata",
Subtype: "XML",
Length: metadataXML.length
});
const metadataStreamRef = pdf.context.register(metadataStream);
pdf.catalog.set(PDFName.of("Metadata"), metadataStreamRef);
}
function formatDateMetadata({ date }) {
return `${date.toISOString().split(".")[0]}Z`;
}
async function generate(options) {
const xml = await resolveXml(options.xml);
const flavor = options.flavor || getFlavor(xml);
const level = options.level || getLevel(xml);
if (options.check === true) {
const result = await check({
xml,
flavor,
level
});
if (!result.valid) {
throw new Error(`Invalid XML format (${flavor} - ${level})`);
}
}
let meta = options.meta;
if (!meta) {
const info = await extractBaseInfo(xml);
meta = baseInfo2PdfMetadata(info);
}
meta.date ||= /* @__PURE__ */ new Date();
let description = "";
let filename = "";
let conformanceLevel = "";
switch (flavor) {
case "facturx":
filename = FACTURX_FILENAME;
description = "Factur-X XML file";
conformanceLevel = FACTURX_CONFORMANCE_LEVEL[level] || "";
break;
case "orderx":
filename = ORDERX_FILENAME;
description = "Order-X XML file";
break;
case "zugferd":
filename = ZUGFERD_FILENAMES[0];
description = "ZUGFeRD XML file";
break;
default:
throw new Error(`Unknown schema flavor: "${options.flavor}"`);
}
const pdf = await resolvePdf(options.pdf);
let documentId;
if (pdf.context.trailerInfo.ID) {
throw new Error("Not implemented yet: Document ID already set");
} else {
documentId = randomBytes(16).toString("hex");
const id = PDFHexString.of(documentId);
pdf.context.trailerInfo.ID = pdf.context.obj([id, id]);
}
const encoder = new TextEncoder();
const uint8Array = encoder.encode(xml.toString());
await pdf.attach(uint8Array, filename, {
afRelationship: AFRelationship.Data,
mimeType: "text/xml",
creationDate: meta.date,
modificationDate: meta.date,
description
});
const creator = `${pkg.name} npm lib v${pkg.version} (https://github.com/${pkg.repository})`;
if (options.language) {
pdf.setLanguage(options.language);
}
pdf.setCreationDate(meta.date);
pdf.setModificationDate(meta.date);
pdf.setTitle(meta.title);
pdf.setSubject(meta.subject);
pdf.setAuthor(meta.author);
pdf.setKeywords(meta.keywords);
pdf.setCreator(creator);
setPDFA3BMetadata({
...meta,
documentId,
filename,
conformanceLevel,
producer: creator,
creator
}, pdf);
return await pdf.save();
}
export { generate as g };