All notable changes to this project are documented here. Format based on Keep a Changelog.

[0.2.0] - 2026-07-24

Added

  • Facturx.XSD.validate/2 + Facturx.validate_xsd/2: validate CII XML against the EN 16931 XSD, in pure Elixir, in-process via OTP :xmerl_xsd — no external tool, no network, no Port. Catches missing mandatory elements, wrong data types, unexpected elements, wrong order and cardinality. Bundled schema: priv/xsd/en16931/.
  • Facturx.XSD.Cache (supervised): compiles each bundled schema once and shares it via :persistent_term, so validation runs in the caller process — in parallel, ~0.6 ms/call (vs ~5 ms recompiling per call). Falls back to per-call compilation in a short-lived task when no cached schema is available.

Security

  • XSD validation treats input as untrusted: a <!DOCTYPE> is rejected and external entity/DTD fetching is disabled, preventing XXE and entity-expansion ("billion laughs") attacks.

Notes

  • :xmerl_xsd is a partial XSD 1.0 implementation; it validates the CII EN 16931 schema well but is not a guarantee of full XSD 1.0 conformance. Business-rule validation remains in Facturx.Validate (Schematron).

[0.1.0] - 2026-07-24

First public release. Pure-Elixir generation and extraction of Factur-X / ZUGFeRD invoices (EN 16931), with optional Schematron validation.

Added

  • Project skeleton and scope ADR.
  • Public API surface: Facturx.generate/3, extract/1, parse/1, build/2, validate/2.
  • Facturx.Extract.extract/1: locate and decode the embedded CII XML from a Factur-X PDF (classic xref), with profile detection and refc-binary-safe results (:binary.copy). Validated against a real EN 16931 fixture.
  • Facturx.Embed.embed/3 + Facturx.generate/3: embed CII XML into a PDF/A-2 or PDF/A-3 base via an incremental update (embedded-file stream, /Filespec, /AF, EmbeddedFiles name tree, overridden catalog + XMP). Promotes PDF/A-2 → PDF/A-3; refuses PDF/A-1 and non-PDF/A input. Output verified PDF/A-3b-compliant by veraPDF and semantically identical to the Python reference (akretion/factur-x).
  • Facturx.Xmp.promote/3: bump pdfaid:part 2 → 3 and inject the Factur-X extension schema + fx:* properties.
  • Facturx.CII.build/2 + parse/1: map Facturx.Invoice (Decimal amounts) to and from EN 16931 CII XML (header, seller/buyer/ship-to, lines, VAT breakdown, monetary totals). Output validated against the CII XSD; build/parse round-trip the modelled fields (build fills conventional defaults — unit C62, legal scheme 0002 — which parse reads back). Facturx.generate/3 now accepts an Invoice struct directly (struct → CII → embed → veraPDF-valid Factur-X).
  • Facturx.Validate.validate/2: optional EN 16931 Schematron validation. Sends the XML + bundled compiled schematron (priv/schematron/) to a Saxon server over multipart/form-data (via optional :req) and interprets the SVRL report into violations. Supports a :xsl override for custom rule sets. Proven end-to-end against a live Saxon server. Note: the EN 16931 XSLT resolves a code-list DB via document(...); the Saxon server must be configured to allow that URI (:codedb_url overrides the location).

Known limitations

  • Classic cross-reference tables only; object/xref streams (/Type /ObjStm, /Type /XRef) are not yet handled (Extract reports :object_streams_unsupported, Embed :xref_streams_unsupported).
  • Embed uses an incremental update, so the pre-promotion (part-2) XMP remains earlier in the file; conformant readers resolve the latest object (veraPDF confirms). A full-rewrite mode may be added later.
  • Embed merges /AF, /Names and /PageMode into the existing catalog (preserving e.g. /Dests). Shapes it cannot safely merge in place are refused, never corrupted: an indirect /AF//Names reference, or a base that already carries embedded files (re-embedding into a Factur-X).