Skip to content

Initial support for e-invoices: German ZUGFeRD v2.x and French Factur-X#1705

Open
andreiaugustin wants to merge 1 commit intofoliojs:masterfrom
andreiaugustin:einvoice
Open

Initial support for e-invoices: German ZUGFeRD v2.x and French Factur-X#1705
andreiaugustin wants to merge 1 commit intofoliojs:masterfrom
andreiaugustin:einvoice

Conversation

@andreiaugustin
Copy link
Contributor

@andreiaugustin andreiaugustin commented Mar 19, 2026

In the last few years, a handful of European countries have started adopting legislations encouraging businesses to adopt electronic invoicing practices, going as far as being a mandatory requirement for some public establishments. Furthermore, the United States Treasury estimated that implementing e-invoicing across the entire federal government would reduce costs by 50%.

Various standards exists - global, national, regional and proprietary. However, none of them prevail and most of them are not interoperable with one another. Source.

In my personal experience, we have great demand for the German ZUGFeRD and the French Factur-X standards (which in the latest versions, are very, very similar).

PDFKit is already in an excellent position to produce compliant PDFs with the aforementioned European, national, e-invoice standards since it already supports XMP metadata, file embedding and PDF/A-3, which is the PDF subset these standard are built upon.

With this feature, I am hoping we can introduce a high-level API to make it easier to create ZUGFeRD and Factur-X e-invoice PDFs and have a good structure for extending support to other e-invoice standards in the future.

Feature

What kind of change does this PR introduce?

This PR introduces a structure for e-invoice mixins and initial support for ZUGFeRD v2.X and Factur-X through a new API:

doc.einvoice(format, src, options = {})

Both formats are implemented separately behind a common entry point. Although nearly identical, keeping them independent at least initially would allow us to easily diverge if their specs starts differing in the near future as the EU may attempt establishing a bloc-wide standard?

The possible values for format are: 'zugferd' or 'facturx' at the moment.

The src parameter can be a Buffer, ArrayBuffer or string as it is directly passed down to the doc.file().

The options are optional as most internal logic is defaulted for what I understand to be best practices at the time of writing.
Both formats accept similar options: profile, version (overrides) and documentType (defaults to 'INVOICE'). These mostly change what is pasted into the XMP metadata: the embedded XML file has varying profiles e.g. minimum, basic, extended, en 16931 etc. and these define what and how the information is structured in the XML file. Validators require the profile in the PDF's metadata.

I have been using the following service to verify the ZUGFeRD compliance.

I would love to get some feedback and thoughts, especially on the API naming?

Also, very important to note the refactoring of PDFMetadata is the most significant change outside the new feature. It now has separate concerns where _body accumulates metadata blocks as they are appended and end() assembles the complete XML in one shot with all registered namespace interpolated into the rdf:RDF opening tag. Its existing interface is unchanged, but very important to note is _metadata is now only populated after end() - before that, it is an empty string rather than a partial document like before. As far as I can tell at first glance, nothing seems to call getXML() or getLength() before end(), but it's worth a second pair of eyes just in case.

Another thing to note is the guard against PDF/A-3 subset relies on initPDFA() normalising the subset string to an integer (PDF/A-3b, PDF/A-3a and PDF/A-3 all become 3). This isn't a documented contract anywhere, it just happens to be how it works today and if that normalisation ever changes, the guard would silently break.

Finally, worth mentioning: the metadata.registerNamespace(prefix, uri) and its mixin counterpart registerXMPNamespace were added to support the xmlns:fx namespace requirement, but they are no restricted to e-invoice, so they can be used in the future if anybody else wishes to inject a namespace onto rdf:RDF.

I am leaving the 'ready to be merged' option unchecked for now as I am working on a couple examples we could add for this, especially since we have the xml files we use in the unit tests, it would be quite cool.

Checklist:

  • Unit Tests
  • Documentation
  • Update CHANGELOG.md
  • Ready to be merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant